// Darwin-only by nature: distribution builds use the data-protection Keychain. On Linux the
// callers branch to the 0600-file `LinuxSecretStore` (LinuxSupport.swift) instead.
#if canImport(Security)
import Foundation
import Security

// `SecKeychainGet/SetUserInteractionAllowed` are formally deprecated but remain the ONLY API that
// suppresses the legacy Keychain ACL/partition-list password panel (the data-protection
// `kSecUseAuthenticationUI*` flags govern only Touch ID / passcode presence, not that dialog). Bind
// the C symbols directly — the deprecation rides on their Swift imports, not the raw symbols — so
// the required calls in `withoutLegacyKeychainUI` compile without deprecation warnings.
@_silgen_name("SecKeychainGetUserInteractionAllowed")
private func nucleic_SecKeychainGetUserInteractionAllowed(_ state: UnsafeMutablePointer<DarwinBoolean>) -> OSStatus
@_silgen_name("SecKeychainSetUserInteractionAllowed")
private func nucleic_SecKeychainSetUserInteractionAllowed(_ state: DarwinBoolean) -> OSStatus

/// Shared persistence for Nucleic's own secrets that is structurally incapable of presenting a
/// Keychain authorization panel.
///
/// Developer-ID builds use the data-protection Keychain. Its access-group model follows the app's
/// signed identity across updates and channels, unlike the legacy login Keychain's per-binary ACL
/// and private partition list. Distribution packaging requires an embedded provisioning profile
/// authorizing this group, so a release can never silently regress to the prompting implementation.
///
/// Ad-hoc dev builds have no Team ID and cannot claim that group. They use a Nucleic-owned 0600 file
/// under a 0700 Application Support directory; this is stable across constant ad-hoc re-signing.
///
/// For pre-migration items, `copyLegacyDataWithoutUI` adopts an already-readable login-Keychain
/// value while `SecKeychainSetUserInteractionAllowed(false)` guarantees the read can never present
/// the legacy ACL/partition-list password panel: if macOS would need to ask, the lookup fails and
/// the old item is left untouched. No automatic Nucleic code path is allowed to raise authorization UI.
///
/// The Claude Code item is not an owned secret. `ClaudeLoginKeychain` may use the same no-UI lookup
/// to adopt an already-authorized value and to notice a newer value after an explicit
/// `claude /login`. Writes still use a distinct Nucleic-owned item; the external item is never
/// mutated.
enum KeychainOwnedAccess {
    private static let accessGroup = "L7UDTQ6F5W.xyz.blakeslee.nucleic"

    /// A harmless lookup determines whether this signature was provisioned for the shared group.
    private static let dataProtectionAvailable: Bool = {
        let status = SecItemCopyMatching([
            kSecClass as String: kSecClassGenericPassword,
            kSecUseDataProtectionKeychain as String: true,
            kSecAttrAccessGroup as String: accessGroup,
            kSecAttrAccount as String: "com.nucleic.__dp_probe__",
            kSecMatchLimit as String: kSecMatchLimitOne,
            kSecReturnData as String: true,
        ] as CFDictionary, nil)
        return status != errSecMissingEntitlement
    }()

    /// Reads from the owned store, silently adopting an accessible legacy value when necessary.
    static func copyData(_ identity: [String: Any]) -> Data? {
        if dataProtectionAvailable {
            if let data = rawRead(dataProtection(identity)) { return data }
            // A previous ad-hoc build may already have moved this item to the stable file store.
            if let data = FileStore.read(identity) {
                var add = dataProtection(identity)
                add[kSecValueData as String] = data
                add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
                if SecItemAdd(add as CFDictionary, nil) == errSecSuccess {
                    FileStore.remove(identity)
                }
                return data
            }
            guard !FileStore.legacyHandled(identity),
                  let legacy = copyLegacyDataWithoutUI(identity)
            else { return nil }
            var add = dataProtection(identity)
            add[kSecValueData as String] = legacy
            add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
            if SecItemAdd(add as CFDictionary, nil) == errSecSuccess {
                FileStore.markLegacyHandled(identity)
            }
            return legacy
        }

        if let data = FileStore.read(identity) { return data }
        guard !FileStore.legacyHandled(identity),
              let legacy = copyLegacyDataWithoutUI(identity)
        else { return nil }
        if FileStore.write(identity, data: legacy) {
            FileStore.markLegacyHandled(identity)
        }
        return legacy
    }

    /// Reads a legacy login-Keychain item ONLY when this app is already trusted for it — never
    /// showing a password panel.
    ///
    /// The legacy (file-based) login Keychain shows an ACL / partition-list authorization panel
    /// ("<app> wants to use … in your keychain") when an app that isn't already in an item's access
    /// list reads it — e.g. Nucleic reading Claude Code's `Claude Code-credentials`. The
    /// data-protection flags (`kSecUseAuthenticationUI*`) govern only that Keychain's user-*presence*
    /// (Touch ID / passcode) and do NOT suppress the legacy ACL panel. The one switch that does is
    /// `SecKeychainSetUserInteractionAllowed(false)`: a read that would need the panel returns an
    /// error instead of prompting. We flip it off around the read and restore it immediately, so
    /// Nucleic can NEVER raise a Keychain password prompt — an already-trusted item still reads
    /// silently; anything else fails silently and Nucleic falls back to its own mediated credential.
    static func copyLegacyDataWithoutUI(_ identity: [String: Any]) -> Data? {
        withoutLegacyKeychainUI { rawRead(identity) }
    }

    /// Run `body` with the legacy Keychain's interactive authorization panel disabled, restoring the
    /// prior setting afterward. `SecKeychain*` is formally deprecated but remains the ONLY API that
    /// suppresses that panel — its data-protection successors don't govern the legacy ACL dialog at
    /// all. The three symbols are bound directly (`nucleic_SecKeychain*`, top of file) so the
    /// required calls compile without deprecation warnings.
    private static func withoutLegacyKeychainUI<T>(_ body: () -> T) -> T {
        var previous = DarwinBoolean(true)
        _ = nucleic_SecKeychainGetUserInteractionAllowed(&previous)
        _ = nucleic_SecKeychainSetUserInteractionAllowed(false)
        defer { _ = nucleic_SecKeychainSetUserInteractionAllowed(previous) }
        return body()
    }

    /// Replaces the owned value. Neither backend is capable of presenting authorization UI.
    @discardableResult
    static func set(_ base: [String: Any], label _: String = "Nucleic") -> OSStatus {
        var identity = base
        identity.removeValue(forKey: kSecValueData as String)
        guard let data = base[kSecValueData as String] as? Data else { return errSecParam }

        let status: OSStatus
        if dataProtectionAvailable {
            let query = dataProtection(identity)
            let update = SecItemUpdate(
                query as CFDictionary,
                [kSecValueData as String: data] as CFDictionary)
            if update == errSecItemNotFound {
                var add = dataProtection(base)
                add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
                status = SecItemAdd(add as CFDictionary, nil)
            } else {
                status = update
            }
        } else {
            status = FileStore.write(identity, data: data) ? errSecSuccess : errSecIO
        }
        // Only suppress the legacy value once the replacement is safely stored.
        if status == errSecSuccess { FileStore.markLegacyHandled(identity) }
        return status
    }

    /// Removes only Nucleic-owned copies. A legacy item is never touched because doing so can ask
    /// for authorization, and it may belong to an older installation or another application.
    static func remove(_ identity: [String: Any]) {
        SecItemDelete(dataProtection(identity) as CFDictionary)
        FileStore.remove(identity)
        FileStore.markLegacyHandled(identity)
    }

    private static func dataProtection(_ query: [String: Any]) -> [String: Any] {
        var q = query
        q[kSecUseDataProtectionKeychain as String] = true
        q[kSecAttrAccessGroup as String] = accessGroup
        return q
    }

    private static func rawRead(
        _ query: [String: Any], authenticationUI: CFString? = nil
    ) -> Data? {
        var q = query
        q[kSecReturnData as String] = true
        q[kSecMatchLimit as String] = kSecMatchLimitOne
        if let authenticationUI {
            q[kSecUseAuthenticationUI as String] = authenticationUI
        }
        var item: CFTypeRef?
        guard SecItemCopyMatching(q as CFDictionary, &item) == errSecSuccess else { return nil }
        return item as? Data
    }

    private enum FileStore {
        private static var directory: URL {
            FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
                .appendingPathComponent("Nucleic/Secrets", isDirectory: true)
        }

        private static func name(_ identity: [String: Any]) -> String {
            let fields = [kSecClass, kSecAttrService, kSecAttrAccount].map { key in
                identity[key as String].map { String(describing: $0) } ?? ""
            }
            return Data(fields.joined(separator: "\u{0}").utf8).base64EncodedString()
                .replacingOccurrences(of: "/", with: "_")
                .replacingOccurrences(of: "+", with: "-")
                .replacingOccurrences(of: "=", with: "")
        }

        private static func url(_ identity: [String: Any]) -> URL {
            directory.appendingPathComponent(name(identity))
        }

        private static func markerURL(_ identity: [String: Any]) -> URL {
            directory.appendingPathComponent(".legacy-" + name(identity))
        }

        static func read(_ identity: [String: Any]) -> Data? {
            try? Data(contentsOf: url(identity))
        }

        static func write(_ identity: [String: Any], data: Data) -> Bool {
            do {
                try FileManager.default.createDirectory(
                    at: directory, withIntermediateDirectories: true,
                    attributes: [.posixPermissions: 0o700])
                let target = url(identity)
                try data.write(to: target, options: .atomic)
                try FileManager.default.setAttributes(
                    [.posixPermissions: 0o600], ofItemAtPath: target.path)
                return true
            } catch {
                return false
            }
        }

        static func remove(_ identity: [String: Any]) {
            try? FileManager.default.removeItem(at: url(identity))
        }

        static func legacyHandled(_ identity: [String: Any]) -> Bool {
            FileManager.default.fileExists(atPath: markerURL(identity).path)
        }

        static func markLegacyHandled(_ identity: [String: Any]) {
            try? FileManager.default.createDirectory(
                at: directory, withIntermediateDirectories: true,
                attributes: [.posixPermissions: 0o700])
            _ = FileManager.default.createFile(
                atPath: markerURL(identity).path,
                contents: Data(), attributes: [.posixPermissions: 0o600])
        }
    }
}
#endif
