import Foundation

/// Platform-neutral persistence for Nucleic-owned secrets, keyed by an optional service plus an
/// account string with `Data` values (docs/WINDOWS_PORT.md §4.2). One backend per platform:
///
///   • macOS — ``KeychainSecretStore``, a thin adapter over `KeychainOwnedAccess` (the
///     data-protection Keychain with the ad-hoc-build file fallback and silent legacy-item
///     adoption). The identity dictionaries it builds are byte-identical to what the call
///     sites built before this seam existed, so every already-stored item keeps resolving.
///   • Linux — ``FileSecretStore`` over `LinuxSecretStore` (0600 files keyed by account).
///   • Windows — the DPAPI `CredentialStore` (docs/WINDOWS_PORT.md §4.2) once it lands;
///     until then Windows shares the file backend.
///
/// `service` is optional and its **absence is load-bearing**: most existing items were stored
/// with no `kSecAttrService` at all (their reverse-DNS `account` string is the whole key), and
/// the Keychain + file-fallback naming both distinguish "no service" from any literal value.
/// Substituting a default service would orphan every existing item — callers that never set
/// one must keep passing `nil`. The Linux/file backend keys by account alone (as it always
/// has); callers there guarantee account uniqueness by convention.
///
/// Deliberately no `list` yet: no caller enumerates secrets today, and the Keychain backend
/// would need a separate query shape for it — add it with its first consumer.
public protocol SecretStore: Sendable {
    /// The stored value, or `nil` when absent (or unreadable without user interaction —
    /// no backend is ever allowed to raise authorization UI).
    func get(service: String?, account: String) -> Data?
    /// Create or replace the value. Returns `false` when the backend failed to persist it.
    @discardableResult
    func set(service: String?, account: String, data: Data) -> Bool
    /// Remove the Nucleic-owned value (legacy/foreign items are never touched).
    func remove(service: String?, account: String)
}

/// The process-wide store for the current platform.
public enum SecretStores {
    public static let `default`: any SecretStore = {
        #if canImport(Security)
        return KeychainSecretStore()
        #else
        return FileSecretStore()
        #endif
    }()
}

#if canImport(Security)
import Security

/// macOS backend: `KeychainOwnedAccess` behind the seam. Builds exactly the generic-password
/// identity the call sites used to build inline — class + account, plus the service only when
/// the caller has one — so pre-seam items keep resolving without migration.
struct KeychainSecretStore: SecretStore {
    private func identity(_ service: String?, _ account: String) -> [String: Any] {
        var identity: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
        ]
        if let service { identity[kSecAttrService as String] = service }
        return identity
    }

    func get(service: String?, account: String) -> Data? {
        KeychainOwnedAccess.copyData(identity(service, account))
    }

    @discardableResult
    func set(service: String?, account: String, data: Data) -> Bool {
        var add = identity(service, account)
        add[kSecValueData as String] = data
        return KeychainOwnedAccess.set(add) == errSecSuccess
    }

    func remove(service: String?, account: String) {
        KeychainOwnedAccess.remove(identity(service, account))
    }
}
#else
/// Non-Darwin backend: the 0600-file `LinuxSecretStore`, keyed by account alone (`service` is
/// ignored — the reverse-DNS account strings are unique by convention, and existing installs'
/// files are named by account). Windows shares this until the DPAPI `CredentialStore` of
/// docs/WINDOWS_PORT.md §4.2 replaces it.
struct FileSecretStore: SecretStore {
    func get(service: String?, account: String) -> Data? {
        LinuxSecretStore.read(account: account)
    }

    @discardableResult
    func set(service: String?, account: String, data: Data) -> Bool {
        LinuxSecretStore.write(account: account, data: data)
    }

    func remove(service: String?, account: String) {
        LinuxSecretStore.remove(account: account)
    }
}
#endif
