import Foundation
import os
import Security

/// Persists the device identity so it survives relaunch. The raw `.p12` bytes
/// **and** its passphrase are stored together (the passphrase is required to
/// re-import via `SecPKCS12Import` at every launch), then re-imported on load.
public protocol ClientIdentityStore: Sendable {
    /// Validate (`SecPKCS12Import`) then persist the `.p12` + passphrase.
    /// Throws `PKCS12ImportError` on a bad passphrase / corrupt file (nothing is
    /// persisted in that case) and `ClientIdentityStoreError` on a storage fault.
    func save(p12Data: Data, passphrase: String) throws
    /// Re-import and return the stored identity; `nil` if none is installed.
    func loadIdentity() throws -> ClientIdentity?
    /// Display summary of the stored certificate; `nil` if none is installed.
    func loadSummary() throws -> ClientCertificateSummary?
    /// Delete the stored identity (rotation / removal). Idempotent.
    func remove() throws
    /// Cheap existence check for gating (does NOT re-import).
    func hasInstalledIdentity() -> Bool
}

public enum ClientIdentityStoreError: Error, Equatable, Sendable {
    /// A Keychain `SecItem*` call failed with this `OSStatus`.
    case keychain(OSStatus)
    /// The stored blob was present but could not be decoded.
    case corruptStoredBlob
}

public extension ClientIdentityStore {
    /// Convenience for composition roots: load the identity, logging and
    /// swallowing errors into `nil`. A missing cert is the normal pre-install
    /// state; a genuine fault must not crash launch, but is logged (never
    /// silently dropped).
    func loadedIdentityOrNil() -> ClientIdentity? {
        do {
            return try loadIdentity()
        } catch {
            ClientTLSLog.identity.error(
                "loadIdentity failed: \(String(describing: error), privacy: .public)"
            )
            return nil
        }
    }
}

/// The stored payload — `.p12` bytes plus the passphrase needed to re-import.
private struct StoredP12Blob: Codable {
    let p12: Data
    let passphrase: String
}

/// Keychain-backed store: one `kSecClassGenericPassword` item holding the
/// JSON-encoded `StoredP12Blob` in `kSecValueData`, protected with
/// `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` (available after the first
/// unlock post-boot, never migrates off this device).
public struct KeychainClientIdentityStore: ClientIdentityStore {
    public static let defaultService = "com.yaojia.webterm.clienttls"
    public static let defaultAccount = "device-identity"

    private let service: String
    private let account: String

    public init(
        service: String = defaultService, account: String = defaultAccount
    ) {
        self.service = service
        self.account = account
    }

    public func save(p12Data: Data, passphrase: String) throws {
        // Validate BEFORE persisting — a wrong passphrase / corrupt file must
        // surface to the install UI and leave any prior identity untouched.
        _ = try PKCS12Importer.importIdentity(data: p12Data, passphrase: passphrase)
        let blob = try encode(StoredP12Blob(p12: p12Data, passphrase: passphrase))
        try writeItem(blob)
    }

    public func loadIdentity() throws -> ClientIdentity? {
        guard let blob = try readBlob() else { return nil }
        return try PKCS12Importer.importIdentity(
            data: blob.p12, passphrase: blob.passphrase
        )
    }

    public func loadSummary() throws -> ClientCertificateSummary? {
        try loadIdentity()?.summary()
    }

    public func remove() throws {
        let status = SecItemDelete(baseQuery() as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else {
            throw ClientIdentityStoreError.keychain(status)
        }
    }

    public func hasInstalledIdentity() -> Bool {
        var query = baseQuery()
        query[kSecReturnData as String] = false
        query[kSecMatchLimit as String] = kSecMatchLimitOne
        return SecItemCopyMatching(query as CFDictionary, nil) == errSecSuccess
    }

    // MARK: - Keychain plumbing

    private func baseQuery() -> [String: Any] {
        [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
        ]
    }

    private func writeItem(_ data: Data) throws {
        // Delete-then-add keeps the item's protection class deterministic
        // (SecItemUpdate can't change kSecAttrAccessible in place).
        let deleteStatus = SecItemDelete(baseQuery() as CFDictionary)
        guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else {
            throw ClientIdentityStoreError.keychain(deleteStatus)
        }
        var attributes = baseQuery()
        attributes[kSecValueData as String] = data
        attributes[kSecAttrAccessible as String] =
            kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
        let addStatus = SecItemAdd(attributes as CFDictionary, nil)
        guard addStatus == errSecSuccess else {
            throw ClientIdentityStoreError.keychain(addStatus)
        }
    }

    private func readBlob() throws -> StoredP12Blob? {
        var query = baseQuery()
        query[kSecReturnData as String] = true
        query[kSecMatchLimit as String] = kSecMatchLimitOne
        var result: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        if status == errSecItemNotFound { return nil }
        guard status == errSecSuccess, let data = result as? Data else {
            throw ClientIdentityStoreError.keychain(status)
        }
        do {
            return try JSONDecoder().decode(StoredP12Blob.self, from: data)
        } catch {
            throw ClientIdentityStoreError.corruptStoredBlob
        }
    }

    private func encode(_ blob: StoredP12Blob) throws -> Data {
        do {
            return try JSONEncoder().encode(blob)
        } catch {
            throw ClientIdentityStoreError.corruptStoredBlob
        }
    }
}

/// In-memory store for previews and unit tests: same import/summary code path as
/// the keychain store (so the roundtrip is exercised) without any keychain
/// entitlement. `@unchecked Sendable` — mutable blob guarded by a lock.
public final class InMemoryClientIdentityStore: ClientIdentityStore, @unchecked Sendable {
    private let lock = NSLock()
    private var blob: StoredP12BlobBox?

    /// Boxed so the private `StoredP12Blob` type stays file-private above; this
    /// mirror keeps the two bytes+passphrase without exposing the Codable type.
    private struct StoredP12BlobBox {
        let p12: Data
        let passphrase: String
    }

    public init() {}

    public func save(p12Data: Data, passphrase: String) throws {
        _ = try PKCS12Importer.importIdentity(data: p12Data, passphrase: passphrase)
        lock.withLock { blob = StoredP12BlobBox(p12: p12Data, passphrase: passphrase) }
    }

    public func loadIdentity() throws -> ClientIdentity? {
        guard let stored = lock.withLock({ blob }) else { return nil }
        return try PKCS12Importer.importIdentity(
            data: stored.p12, passphrase: stored.passphrase
        )
    }

    public func loadSummary() throws -> ClientCertificateSummary? {
        try loadIdentity()?.summary()
    }

    public func remove() throws {
        lock.withLock { blob = nil }
    }

    public func hasInstalledIdentity() -> Bool {
        lock.withLock { blob != nil }
    }
}

enum ClientTLSLog {
    static let identity = Logger(subsystem: "com.yaojia.webterm", category: "client-tls")
}
