import Foundation
import Security

/// Explicit error taxonomy for keychain-backed persistence (plan T-iOS-7
/// "错误码显式映射"). `errSecDuplicateItem` and `errSecItemNotFound` are
/// BRANCHES (add→update fallback / empty list / idempotent delete), never
/// surfaced as errors.
public enum KeychainHostStoreError: Error, Equatable {
    /// -34018 `errSecMissingEntitlement`: the data-protection keychain is
    /// unavailable to unsigned binaries (e.g. `swift test`) — a signal to
    /// inject a fake `SecItemShim`, not a user-facing failure.
    case missingEntitlement
    /// Stored payload is not a decodable `[Host]` — treated as an explicit
    /// error, never a crash (defensive at the storage boundary).
    case corruptedData
    /// `[Host]` failed to JSON-encode (practically unreachable; kept for a
    /// complete, explicit taxonomy on the write path).
    case encodingFailed
    case unexpectedStatus(OSStatus)

    init(status: OSStatus) {
        self = status == errSecMissingEntitlement ? .missingEntitlement : .unexpectedStatus(status)
    }
}

/// Keychain-backed `HostStore`: the whole host list is ONE generic-password
/// item (JSON `[Host]`) under `service`/`account`. An actor so the
/// read-modify-write in `upsert`/`remove` serializes.
///
/// All SecItem dictionaries come from `KeychainItemSpec` — the §5.3 single
/// source; building an attribute dict anywhere else is a review CRITICAL.
public actor KeychainHostStore: HostStore {
    public static let defaultService = "com.yaojia.webterm.host-registry"
    public static let defaultAccount = "hosts"

    private let shim: any SecItemShim
    private let spec: KeychainItemSpec

    public init(
        shim: any SecItemShim = LiveSecItemShim(),
        service: String = KeychainHostStore.defaultService,
        account: String = KeychainHostStore.defaultAccount
    ) {
        self.shim = shim
        self.spec = KeychainItemSpec(service: service, account: account)
    }

    public func loadAll() async throws -> [Host] {
        try readHosts()
    }

    public func upsert(_ host: Host) async throws -> [Host] {
        let updated = try readHosts().upserting(host)
        try persist(updated)
        return updated
    }

    public func remove(id: UUID) async throws -> [Host] {
        let current = try readHosts()
        let updated = current.removing(id: id)
        guard updated.count != current.count else {
            return current // explicit no-op: unknown id, nothing persisted
        }
        try persist(updated)
        return updated
    }

    // MARK: - Keychain I/O (dictionaries built solely by KeychainItemSpec)

    private func readHosts() throws -> [Host] {
        let (status, data) = shim.copyMatching(spec.copyQuery())
        if status == errSecItemNotFound { return [] }
        guard status == errSecSuccess else { throw KeychainHostStoreError(status: status) }
        guard let data, let hosts = try? JSONDecoder().decode([Host].self, from: data) else {
            throw KeychainHostStoreError.corruptedData
        }
        return hosts
    }

    private func persist(_ hosts: [Host]) throws {
        if hosts.isEmpty {
            try deleteItem()
            return
        }
        let data = try encodeHosts(hosts)
        let addStatus = shim.add(spec.addAttributes(data: data))
        if addStatus == errSecSuccess { return }
        guard addStatus == errSecDuplicateItem else {
            throw KeychainHostStoreError(status: addStatus)
        }
        let updateStatus = shim.update(
            query: spec.baseQuery(),
            attributesToUpdate: spec.updateAttributes(data: data)
        )
        guard updateStatus == errSecSuccess else {
            throw KeychainHostStoreError(status: updateStatus)
        }
    }

    private func deleteItem() throws {
        let status = shim.delete(spec.baseQuery())
        guard status == errSecSuccess || status == errSecItemNotFound else {
            throw KeychainHostStoreError(status: status)
        }
    }

    private func encodeHosts(_ hosts: [Host]) throws -> Data {
        do {
            return try JSONEncoder().encode(hosts)
        } catch {
            throw KeychainHostStoreError.encodingFailed
        }
    }
}
