package wang.yaojia.webterm.clienttls

/**
 * The pinned device identity as seen by the key-manager selection logic: just enough to *decide*
 * whether to present a client certificate. The key material (private key + chain) lives in the
 * framework half (A11, AndroidKeyStore-backed) — the pure logic only needs the alias and the key
 * algorithm to make the selection.
 *
 * @property alias        the single device-identity alias this app presents.
 * @property keyAlgorithm the leaf key's algorithm (`RSA` / `EC`), matched against the server's
 *   requested client key types.
 */
public data class KeyManagerIdentity(
    val alias: String,
    val keyAlgorithm: String,
)

/**
 * The pure, synchronous alias-selection heart of a client `X509KeyManager` (the Android analogue of
 * iOS `MutualTLSChallengeResponder`). Deliberately free of any `SSLEngine` / `Socket` / OkHttp
 * state so the full truth table is unit-testable without a live handshake; the framework half (A11)
 * is a thin `X509KeyManager` that delegates alias decisions here and reads the actual
 * `PrivateKey`/chain from AndroidKeyStore for the [KeyManagerIdentity.alias].
 *
 * Truth table (mirrors iOS C-iOS-1):
 *  a. identity present + server accepts our key type → present our [KeyManagerIdentity.alias].
 *  b. identity present + key-type mismatch           → present nothing (null) — a clean, classifiable
 *     handshake failure rather than a wrong-type cert.
 *  c. no identity installed                          → present nothing (null) — never a silent
 *     wrong-cert; the TLS handshake fails and the failure surfaces (iOS `.cancel`).
 *
 * @property identity the installed device identity, or null when none is installed.
 */
public class ClientKeyManagerLogic(
    private val identity: KeyManagerIdentity?,
) {
    /**
     * The alias to present for a client-certificate request whose acceptable public-key types are
     * [keyTypes] (the `keyType` argument of `X509KeyManager.chooseClientAlias`). Returns the pinned
     * alias iff an identity is installed AND its key algorithm is acceptable; null otherwise. A null
     * or empty [keyTypes] (server expressed no constraint) is treated as "accept" (fail-open — keep
     * the handshake working when there is exactly one pinned identity).
     */
    public fun chooseClientAlias(keyTypes: Array<String>?): String? {
        val id = identity ?: return null
        return if (accepts(keyTypes)) id.alias else null
    }

    /**
     * All aliases eligible for [keyType] (the `X509KeyManager.getClientAliases` shape): a
     * single-element array of the pinned alias when eligible, else null.
     */
    public fun clientAliases(keyType: String?): Array<String>? {
        val id = identity ?: return null
        return if (accepts(if (keyType == null) null else arrayOf(keyType))) arrayOf(id.alias) else null
    }

    /**
     * True iff [candidate] is the pinned alias of an installed identity. The framework
     * `getPrivateKey(alias)` / `getCertificateChain(alias)` MUST gate on this before returning key
     * material, so a foreign alias never leaks our identity.
     */
    public fun ownsAlias(candidate: String?): Boolean =
        identity != null && candidate != null && candidate == identity.alias

    private fun accepts(keyTypes: Array<String>?): Boolean {
        val id = identity ?: return false
        if (keyTypes == null || keyTypes.isEmpty()) return true
        return keyTypes.any { it.equals(id.keyAlgorithm, ignoreCase = true) }
    }
}
