package wang.yaojia.webterm.tlsandroid

import android.content.Context
import android.content.SharedPreferences
import android.util.Base64
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.RegistryConfiguration
import com.google.crypto.tink.aead.AeadConfig
import com.google.crypto.tink.integration.android.AndroidKeysetManager

/**
 * At-rest storage for the encrypted device-identity "live pointer" ([StoredIdentityMetadata]).
 * Abstracted behind an interface so [AndroidIdentityRepository] depends on the operation contract
 * (repository pattern) — the storage backend is swappable and a fault can be injected in tests. The
 * single [save] is the atomic COMMIT that flips which identity is live (plan §8, A11).
 */
public interface CertStore {
    /** Encrypt and persist [metadata], atomically replacing any previously stored blob (the commit). */
    public fun save(metadata: StoredIdentityMetadata)

    /** Load + decrypt the stored pointer, or null if none is stored. */
    public fun load(): StoredIdentityMetadata?

    /** Remove the stored pointer. Idempotent. */
    public fun clear()
}

/**
 * At-rest storage for the device identity's cert-chain + metadata blob ([StoredIdentityMetadata]),
 * AEAD-encrypted with Tink and an AndroidKeystore-wrapped master key (plan §2 persistence-secret row,
 * §8 cert storage).
 *
 * What is and is NOT stored here:
 *  - STORED (encrypted): the cert chain + alias + key algorithm — the material the re-reading
 *    KeyManager and the summary UI need.
 *  - NOT stored: the private key (non-exportable in AndroidKeyStore — the one key home), the raw
 *    `.p12` bytes, and the passphrase.
 *
 * The keyset + ciphertext live in an app-private `SharedPreferences` file (uninstall-wiped, excluded
 * from cloud backup by the app manifest); the Tink keyset itself is encrypted by the AndroidKeystore
 * master key so the on-disk keyset is useless off this device.
 */
public class TinkCertStore(
    context: Context,
    private val keysetName: String = DEFAULT_KEYSET_NAME,
    private val prefFileName: String = DEFAULT_PREF_FILE,
    private val masterKeyUri: String = DEFAULT_MASTER_KEY_URI,
) : CertStore {
    private val appContext: Context = context.applicationContext

    // Built on first use: registers AEAD, loads-or-creates the AndroidKeystore-wrapped keyset.
    private val aead: Aead by lazy { buildAead() }

    /**
     * Encrypt and persist [metadata], replacing any previously stored blob. Uses synchronous
     * [SharedPreferences.Editor.commit] (NOT `apply()`): the pointer flip is the atomic durable
     * commit of a rotation, and `install()` deletes the superseded key immediately after this
     * returns — so the write MUST be on disk (and MUST surface a failure) before that delete. A
     * non-durable/silently-failed write could leave the on-disk pointer naming an already-deleted
     * key after a crash, losing BOTH identities.
     */
    override fun save(metadata: StoredIdentityMetadata) {
        val plaintext = IdentityMetadataCodec.encode(metadata)
        val ciphertext = aead.encrypt(plaintext, ASSOCIATED_DATA)
        val committed = prefs().edit()
            .putString(BLOB_KEY, Base64.encodeToString(ciphertext, Base64.NO_WRAP))
            .commit()
        if (!committed) {
            throw java.io.IOException("Failed to durably persist the device-identity live pointer")
        }
    }

    /**
     * Load + decrypt the stored blob, or null if none is stored. Throws
     * [CorruptStoredIdentityException] if a blob is present but fails to decrypt/decode (tamper or
     * version skew) — callers treat that as "no usable identity" (see [IdentityRepository]).
     */
    override fun load(): StoredIdentityMetadata? {
        val encoded = prefs().getString(BLOB_KEY, null) ?: return null
        val ciphertext = try {
            Base64.decode(encoded, Base64.NO_WRAP)
        } catch (e: IllegalArgumentException) {
            throw CorruptStoredIdentityException("Stored identity blob was not valid base64", e)
        }
        val plaintext = try {
            aead.decrypt(ciphertext, ASSOCIATED_DATA)
        } catch (e: java.security.GeneralSecurityException) {
            throw CorruptStoredIdentityException("Stored identity blob failed AEAD decryption", e)
        }
        return IdentityMetadataCodec.decode(plaintext)
    }

    /**
     * Remove the stored blob. Idempotent. Uses synchronous [SharedPreferences.Editor.commit] and
     * throws on failure: `remove()` clears this pointer BEFORE deleting the AndroidKeyStore keys, so
     * a silently-failed clear (pointer left naming a soon-to-be-deleted key) would diverge the two
     * stores — surfacing the failure lets `remove()` abort with the prior identity still live. The
     * Tink keyset (master key) is intentionally left intact.
     */
    override fun clear() {
        val committed = prefs().edit().remove(BLOB_KEY).commit()
        if (!committed) {
            throw java.io.IOException("Failed to durably clear the device-identity live pointer")
        }
    }

    private fun buildAead(): Aead {
        AeadConfig.register()
        val keysetHandle = AndroidKeysetManager.Builder()
            .withSharedPref(appContext, keysetName, prefFileName)
            .withKeyTemplate(KeyTemplates.get(AEAD_KEY_TEMPLATE))
            .withMasterKeyUri(masterKeyUri)
            .build()
            .keysetHandle
        return keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead::class.java)
    }

    private fun prefs(): SharedPreferences =
        appContext.getSharedPreferences(prefFileName, Context.MODE_PRIVATE)

    public companion object {
        private const val DEFAULT_KEYSET_NAME = "webterm_cert_keyset"
        private const val DEFAULT_PREF_FILE = "webterm_client_tls_prefs"
        private const val DEFAULT_MASTER_KEY_URI = "android-keystore://webterm_cert_master_key"
        private const val AEAD_KEY_TEMPLATE = "AES256_GCM"
        private const val BLOB_KEY = "cert_chain_blob"

        /**
         * AEAD associated data — binds the ciphertext to this app's identity-blob context so a blob
         * lifted into another Tink ciphertext slot fails to decrypt. Not secret.
         */
        private val ASSOCIATED_DATA: ByteArray = "webterm.client-tls.identity".toByteArray()
    }
}
