package wang.yaojia.webterm.clienttls

import java.io.ByteArrayInputStream
import java.io.IOException
import java.security.GeneralSecurityException
import java.security.KeyStore
import java.security.PrivateKey
import java.security.UnrecoverableKeyException
import java.security.cert.X509Certificate
import javax.crypto.BadPaddingException

/**
 * The structural result of parsing a `.p12` import file — the private key, its leaf certificate,
 * and the issuer chain that accompanies it in the TLS handshake. Android analogue of iOS
 * `ClientIdentity`, but holding only the *pure JVM* material.
 *
 * @property alias               the PKCS#12 key-entry alias the identity was found under.
 * @property privateKey          the leaf private key (device authentication key).
 * @property keyAlgorithm        the private key's algorithm (e.g. `RSA`, `EC`) — drives
 *   [ClientKeyManagerLogic] client-alias selection.
 * @property leafCertificate     the leaf certificate (chain[0]); the device's own cert.
 * @property issuerCertificates  the issuer chain (the CA chain, leaf excluded) — may be empty when
 *   the server already pins the trust anchor. Mirrors iOS `issuerChain` (do not repeat the leaf).
 */
public data class ParsedClientIdentity(
    val alias: String,
    val privateKey: PrivateKey,
    val keyAlgorithm: String,
    val leafCertificate: X509Certificate,
    val issuerCertificates: List<X509Certificate>,
) {
    /** The full chain with the leaf at index 0, as presented to the server. */
    public val fullChain: List<X509Certificate>
        get() = listOf(leafCertificate) + issuerCertificates

    /** Display summary of the leaf certificate for the install/rotation UI. */
    public fun summary(): CertificateSummary = CertificateSummaryReader.summarize(leafCertificate)
}

/**
 * Decoded, but carried no importable private-key entry (e.g. a certs-only / truststore `.p12`).
 * The iOS `PKCS12ImportError.noIdentity` analogue.
 */
public class NoClientIdentityException(message: String) : GeneralSecurityException(message)

/**
 * Not a decodable PKCS#12 blob (truncated / not a `.p12` / unsupported algorithms). The iOS
 * `PKCS12ImportError.corruptFile` / `.unsupported` analogue; retains the underlying cause.
 */
public class Pkcs12DecodeException(message: String, cause: Throwable? = null) :
    GeneralSecurityException(message, cause)

/**
 * Structural PKCS#12 parse via `KeyStore("PKCS12")` — JVM stdlib, needs NO Android SDK. This is the
 * pure half of the iOS `PKCS12Importer`: it validates the passphrase and extracts the identity, but
 * (unlike the framework half A11) never touches AndroidKeyStore/Tink and never persists anything.
 *
 * Error surface (matches the A10 verify contract):
 *  - wrong passphrase → [UnrecoverableKeyException] (the JVM idiom; the underlying cause is
 *    unwrapped so callers see the specific security exception, not a generic [IOException]).
 *  - corrupt / not-a-p12 / unsupported → [Pkcs12DecodeException].
 *  - decoded but no key entry → [NoClientIdentityException].
 */
public object Pkcs12Parse {
    private const val PKCS12 = "PKCS12"
    private const val MAX_CAUSE_DEPTH = 8

    /** Case-insensitive message fragments that a JCE provider uses for a rejected PKCS#12 passphrase. */
    private val WRONG_PASSPHRASE_HINTS = listOf("password", "mac", "integrity")

    /**
     * Parse [bytes] with [passphrase] and return the first key-entry identity found. The alias is
     * selected by preferring the first entry that is a *key* entry (a `.p12` may also carry
     * trusted-cert entries, which are skipped — they hold no private key).
     */
    public fun parse(bytes: ByteArray, passphrase: String): ParsedClientIdentity {
        val password = passphrase.toCharArray()
        val keyStore = loadKeyStore(bytes, password)
        val alias = firstKeyAlias(keyStore)
            ?: throw NoClientIdentityException("PKCS#12 contained no private-key entry")
        return buildIdentity(keyStore, alias, password)
    }

    private fun loadKeyStore(bytes: ByteArray, password: CharArray): KeyStore {
        val keyStore = KeyStore.getInstance(PKCS12)
        try {
            ByteArrayInputStream(bytes).use { keyStore.load(it, password) }
        } catch (e: IOException) {
            throw classifyLoadFailure(e)
        }
        return keyStore
    }

    /**
     * Map a `KeyStore.load` [IOException] to the A10 error surface. A wrong passphrase surfaces
     * DIFFERENTLY per JCE provider: SunJSSE throws an IOException caused by [UnrecoverableKeyException]
     * (MAC/integrity check), while Android's BouncyCastle throws one caused by a
     * [BadPaddingException] (or carries a "password"/"mac"/"integrity" message). Treat ALL of those
     * as the wrong-passphrase signal so a bad password is never misreported on-device as a corrupt
     * file; only a genuinely undecodable blob stays a [Pkcs12DecodeException]. Never echoes the
     * passphrase. `internal` so the classification is unit-testable without a provider-specific blob.
     */
    internal fun classifyLoadFailure(error: IOException): GeneralSecurityException =
        if (isWrongPassphrase(error)) {
            wrongPassphrase(error)
        } else {
            Pkcs12DecodeException("PKCS#12 blob could not be decoded", error)
        }

    private fun isWrongPassphrase(error: Throwable): Boolean =
        causeChain(error).any { cause ->
            cause is UnrecoverableKeyException ||
                cause is BadPaddingException ||
                messageHintsWrongPassphrase(cause.message)
        }

    private fun messageHintsWrongPassphrase(message: String?): Boolean {
        val lowered = message?.lowercase() ?: return false
        return WRONG_PASSPHRASE_HINTS.any { it in lowered }
    }

    /** Prefer a provider-native [UnrecoverableKeyException]; otherwise wrap (never echo the passphrase). */
    private fun wrongPassphrase(error: Throwable): UnrecoverableKeyException =
        causeChain(error).filterIsInstance<UnrecoverableKeyException>().firstOrNull()
            ?: UnrecoverableKeyException("PKCS#12 passphrase was rejected (MAC/integrity check failed)")

    /** Bounded cause-chain walk with an identity cycle-guard (never trust an error graph not to cycle). */
    private fun causeChain(error: Throwable): List<Throwable> {
        val chain = mutableListOf<Throwable>()
        var current: Throwable? = error
        while (current != null && chain.size < MAX_CAUSE_DEPTH) {
            if (chain.any { it === current }) break
            chain.add(current)
            current = current.cause
        }
        return chain
    }

    /** First alias that holds a private key; trusted-cert-only entries are skipped. */
    private fun firstKeyAlias(keyStore: KeyStore): String? =
        keyStore.aliases().toList().firstOrNull { keyStore.isKeyEntry(it) }

    private fun buildIdentity(
        keyStore: KeyStore,
        alias: String,
        password: CharArray,
    ): ParsedClientIdentity {
        // getKey with a wrong key-password throws UnrecoverableKeyException directly (propagated).
        val key = keyStore.getKey(alias, password) as? PrivateKey
            ?: throw NoClientIdentityException("Entry '$alias' held no private key")
        val chain = (keyStore.getCertificateChain(alias) ?: emptyArray())
            .filterIsInstance<X509Certificate>()
        val leaf = chain.firstOrNull()
            ?: throw NoClientIdentityException("Entry '$alias' had no certificate chain")
        return ParsedClientIdentity(
            alias = alias,
            privateKey = key,
            keyAlgorithm = key.algorithm,
            leafCertificate = leaf,
            issuerCertificates = chain.drop(1),
        )
    }
}
