package wang.yaojia.webterm.tlsandroid

import android.util.Log
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient
import wang.yaojia.webterm.api.enroll.CertificateSigningRequest
import wang.yaojia.webterm.api.enroll.DeviceEnrollmentClient
import wang.yaojia.webterm.api.enroll.EnrollmentResult
import wang.yaojia.webterm.clienttls.CertificateSummary
import wang.yaojia.webterm.clienttls.CertificateSummaryReader

/**
 * B4 · The Android device-enroll orchestrator — the `.p12`-free path that mirrors iOS
 * `KeychainClientIdentityStore.enroll/renew`. It composes the five B4 pieces:
 *
 *  1. generate a NON-EXPORTABLE hardware key ([HardwareKeyStore]: StrongBox → TEE),
 *  2. self-sign a P-256 PKCS#10 CSR with it ([CertificateSigningRequest], `:api-client`),
 *  3. run the login → `POST /device/enroll` flow ([DeviceEnrollmentClient], `:api-client`),
 *  4. store the returned leaf + issuer chain into the SAME [CertStore] + AndroidKeyStore slot the
 *     existing [AndroidIdentityRepository] resolves from — so it is presented on the EXISTING
 *     re-reading `X509KeyManager` mTLS path with no change to that module, and
 *  5. expose a silent [renew] against `/device/:id/renew` using the SAME hardware key.
 *
 * The mutating methods are serialized by a [Mutex] so an enroll and a rotation can never interleave
 * the two-store commit (cert live-pointer + enrollment record).
 *
 * ### The commit
 * The cert-store save is THE durable live-pointer flip (identical to the import/rotation path). It is
 * written LAST, after the enrollment record, so a successful cert-store save always means the mTLS
 * identity is fully live; the pool is then evicted so the next handshake presents the new leaf.
 */
public class DeviceEnroller(
    private val client: DeviceEnrollmentClient,
    private val certStore: CertStore,
    private val recordStore: EnrollmentRecordStore,
    private val sharedClient: OkHttpClient,
    private val keyAlias: String = AndroidKeyStoreImporter.DEFAULT_ALIAS,
    private val keyProvider: DeviceKeyProvider = HardwareDeviceKeyProvider,
    // FIX 3 (cache freshness): the in-memory identity cache (AndroidIdentityRepository) is refreshed
    // AFTER each commit so a mid-session enroll/renew is presented on the NEXT handshake with no
    // process restart. Optional so the JVM orchestration tests can construct the enroller without it.
    private val cacheRefresher: IdentityCacheRefresher? = null,
) {
    private val commitMutex = Mutex()

    /** Raised when a state-changing enroll/renew precondition is not met. Never leaks a secret. */
    public class EnrollmentStateException(message: String) : Exception(message)

    /**
     * One-time enrollment: login (operator password → short-lived `device:enroll` bearer) → generate
     * a non-exportable hardware key → CSR → `POST /device/enroll` → store the leaf + present it.
     * Returns the installed leaf's display summary. The bearer is held only for this call, never
     * persisted or logged.
     */
    public suspend fun enroll(
        password: String,
        subdomain: String,
        deviceName: String,
    ): CertificateSummary = commitMutex.withLock {
        val login = client.login(password)
        // Generate the hardware key ONLY after a successful login, so a rejected credential never
        // burns a fresh key slot; overwrites any stale key at the alias.
        val key = keyProvider.generate(keyAlias)
        try {
            val csr = CertificateSigningRequest.der(deviceName, key)
            val result = client.enroll(login.enrollToken, csr, subdomain, deviceName)
            commitIdentity(result, deviceName, key.alias)
            summaryOf(result)
        } catch (e: Exception) {
            // The enroll failed AFTER keygen: drop the orphan key so a retry starts clean and no
            // unreferenced key lingers in secure hardware.
            runCatching { keyProvider.delete(key.alias) }
            throw e
        }
    }

    /**
     * Silent rotation: re-CSR from the SAME hardware key and replace the leaf via
     * `POST /device/:id/renew`. The renew endpoint authenticates by the CURRENT device certificate over
     * mTLS (the presented client cert), so [bearerToken] is OPTIONAL and defaults to absent — the
     * production caller passes none (mirrors iOS, which renews with `bearerToken: nil`). The seam still
     * accepts a bearer for a hypothetical bearer-authenticated renew, but bakes in no credential policy;
     * it only re-signs and re-commits. Throws [EnrollmentStateException] if there is nothing enrolled to
     * renew or the key is gone.
     */
    public suspend fun renew(bearerToken: String? = null): CertificateSummary = commitMutex.withLock {
        val record = recordStore.load()
            ?: throw EnrollmentStateException("no enrollment record — nothing to renew")
        val key = keyProvider.load(record.keyStoreAlias)
            ?: throw EnrollmentStateException("device key missing — a fresh enroll is required")
        val csr = CertificateSigningRequest.der(record.deviceName, key)
        val result = client.renew(record.deviceId, csr, bearerToken)
        commitIdentity(result, record.deviceName, record.keyStoreAlias)
        summaryOf(result)
    }

    /** Remove the enrolled identity: cert pointer, enrollment record, and the hardware key. */
    public suspend fun remove(): Unit = commitMutex.withLock {
        certStore.clear()
        recordStore.clear()
        keyProvider.delete(keyAlias)
        sharedClient.connectionPool.evictAll()
    }

    /**
     * Persist the enrollment record (deviceId → renew), THEN commit the cert live-pointer (the mTLS
     * flip), THEN evict pooled/resumed connections so the next handshake presents the new leaf via
     * the existing re-reading `X509KeyManager`. The key already lives in AndroidKeyStore at [alias];
     * the private key never enters storage.
     */
    private fun commitIdentity(result: EnrollmentResult, deviceName: String, alias: String) {
        val leaf = parseCertificate(result.certificate)
        val issuers = result.caChain.map { parseCertificate(it) }

        recordStore.save(
            EnrollmentRecord(
                deviceId = result.deviceId,
                deviceName = deviceName,
                keyStoreAlias = alias,
                renewAfterEpochSeconds = result.renewAfter?.epochSecond ?: 0L,
            ),
        )
        certStore.save(
            StoredIdentityMetadata(
                alias = alias,
                keyAlgorithm = KEY_ALGORITHM_EC,
                keyStoreAlias = alias,
                certificateChain = listOf(leaf) + issuers,
            ),
        )
        sharedClient.connectionPool.evictAll()
        // FIX 3: re-read the just-committed live-pointer into the in-memory identity cache so the
        // newly enrolled/renewed leaf is presented on the NEXT mTLS handshake without a restart. Done
        // AFTER the durable commit + pool eviction so the cache can never publish an un-committed leaf.
        cacheRefresher?.refreshFromStore()
        Log.i(TAG, "Device identity enrolled/renewed and committed for alias '$alias'")
    }

    private fun summaryOf(result: EnrollmentResult): CertificateSummary =
        CertificateSummaryReader.summarize(parseCertificate(result.certificate))

    private fun parseCertificate(der: ByteArray): X509Certificate =
        CertificateFactory.getInstance(X509).generateCertificate(der.inputStream()) as X509Certificate

    private companion object {
        const val TAG = "DeviceEnroller"
        const val X509 = "X.509"

        /** AndroidKeyStore EC keys report algorithm "EC" — matched by `ClientKeyManagerLogic`. */
        const val KEY_ALGORITHM_EC = "EC"
    }
}
