import Foundation

/// Local custody of per-project Carbon master keys (docs/CARBON_SHARDING.md §4.1, §4.4):
/// generate at enrollment, persist beside the shard store (0600 — the repo those keys protect
/// sits in plaintext on the same disk; the crypto exists for mesh transport and untrusted
/// sinks, not local at-rest), and escrow to the user's iCloud Keychain so total-device-loss
/// recovery works (D7).
///
/// Escrow/recover are **injected** so this type never touches the real Keychain in tests (a
/// headless Keychain read can hang on an invisible ACL prompt — the MESH_TRANSFER lesson).
/// `AppStore` binds them to `CarbonKeyEscrow` on Darwin; Linux runners leave them nil (their
/// keys arrive via the credential mesh, phase 5).
public final class CarbonKeyCustody: @unchecked Sendable {
    private let lock = NSLock()
    private let keysDir: URL
    private var cache: [UUID: CarbonCrypto.ProjectKey] = [:]
    /// Projects whose Keychain escrow write failed — the chip's "key not escrowed" input
    /// (spec §13); retried on next `ensureKey`.
    private var escrowFailed: Set<UUID> = []

    private let escrow: (@Sendable (CarbonCrypto.ProjectKey, UUID) throws -> Void)?
    private let recover: (@Sendable (UUID) throws -> CarbonCrypto.ProjectKey?)?

    public init(
        root: URL,
        escrow: (@Sendable (CarbonCrypto.ProjectKey, UUID) throws -> Void)? = nil,
        recover: (@Sendable (UUID) throws -> CarbonCrypto.ProjectKey?)? = nil
    ) {
        self.keysDir = root.appendingPathComponent("keys", isDirectory: true)
        self.escrow = escrow
        self.recover = recover
    }

    /// The project's key if this member holds (or can recover) it — the store/coordinator
    /// `keyProvider`. Never generates: a mirror must not mint a key the owner already has.
    public func key(for uuid: UUID) -> CarbonCrypto.ProjectKey? {
        lock.lock()
        if let cached = cache[uuid] {
            lock.unlock()
            return cached
        }
        lock.unlock()
        if let fromDisk = readKeyFile(uuid) {
            lock.lock()
            cache[uuid] = fromDisk
            lock.unlock()
            return fromDisk
        }
        // Total-device-loss path: the escrowed copy in iCloud Keychain (spec §4.4). Cache it
        // back to disk so later reads never re-hit the Keychain.
        if let recovered = try? recover?(uuid) {
            persist(recovered, for: uuid)
            return recovered
        }
        return nil
    }

    /// The key for a project this member *owns/enrolls* — generates on first use, persists,
    /// and best-effort escrows (an escrow failure never blocks enrollment; it surfaces).
    public func ensureKey(for uuid: UUID) -> CarbonCrypto.ProjectKey {
        if let existing = key(for: uuid) {
            retryEscrowIfNeeded(existing, for: uuid)
            return existing
        }
        let fresh = CarbonCrypto.ProjectKey.random()
        persist(fresh, for: uuid)
        attemptEscrow(fresh, for: uuid)
        return fresh
    }

    /// Whether the last escrow attempt for `uuid` failed (chip: "key not escrowed").
    public func escrowPending(for uuid: UUID) -> Bool {
        lock.lock()
        defer { lock.unlock() }
        return escrowFailed.contains(uuid)
    }

    // MARK: - Plumbing

    private func keyURL(_ uuid: UUID) -> URL {
        keysDir.appendingPathComponent("\(uuid.uuidString).key")
    }

    private func readKeyFile(_ uuid: UUID) -> CarbonCrypto.ProjectKey? {
        guard let data = try? Data(contentsOf: keyURL(uuid)) else { return nil }
        return try? CarbonCrypto.ProjectKey(bytes: data)
    }

    private func persist(_ key: CarbonCrypto.ProjectKey, for uuid: UUID) {
        lock.lock()
        cache[uuid] = key
        lock.unlock()
        try? SecretFile.createDirectory(at: keysDir)
        try? SecretFile.write(key.bytes, to: keyURL(uuid))
    }

    private func attemptEscrow(_ key: CarbonCrypto.ProjectKey, for uuid: UUID) {
        guard let escrow else { return }
        lock.lock()
        defer { lock.unlock() }
        do {
            try escrow(key, uuid)
            escrowFailed.remove(uuid)
        } catch {
            escrowFailed.insert(uuid)  // enroll anyway; chip shows "key not escrowed" (§13)
        }
    }

    private func retryEscrowIfNeeded(_ key: CarbonCrypto.ProjectKey, for uuid: UUID) {
        lock.lock()
        let needsRetry = escrowFailed.contains(uuid)
        lock.unlock()
        if needsRetry { attemptEscrow(key, for: uuid) }
    }
}
