import Foundation
import Security

/// Persists the session token issued by `POST /api/auth/login` /
/// `/api/auth/signup` in the iOS Keychain — never UserDefaults, which is
/// unencrypted on disk. This is the native counterpart to the web app's
/// httpOnly session cookie (see `src/lib/auth/session.ts`); the backend has
/// no idea which storage a given Bearer token came from.
final class KeychainTokenStore {
    static let shared = KeychainTokenStore()

    private let service = "app.scan-receipts.ios.session"
    private let account = "session-token"

    private init() {}

    var token: String? {
        get { read() }
        set {
            if let newValue {
                save(newValue)
            } else {
                delete()
            }
        }
    }

    private func query(extra: [String: Any] = [:]) -> [String: Any] {
        var q: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
        ]
        extra.forEach { q[$0] = $1 }
        return q
    }

    private func read() -> String? {
        var query = self.query()
        query[kSecReturnData as String] = true
        query[kSecMatchLimit as String] = kSecMatchLimitOne

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess, let data = result as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }

    private func save(_ token: String) {
        let data = Data(token.utf8)
        // Overwrite-or-insert: a plain SecItemAdd fails with errSecDuplicateItem
        // on every login after the first, since the (service, account) pair is
        // stable by design.
        let attributes: [String: Any] = [
            kSecValueData as String: data,
            // Available as soon as the device is unlocked once after boot —
            // matches what a background-launched app (e.g. from a push
            // notification) needs, without requiring the passcode be entered
            // in *this* unlock cycle the way `WhenUnlocked` would.
            kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
        ]
        let updateStatus = SecItemUpdate(query() as CFDictionary, attributes as CFDictionary)
        if updateStatus == errSecItemNotFound {
            var insert = query()
            attributes.forEach { insert[$0] = $1 }
            SecItemAdd(insert as CFDictionary, nil)
        }
    }

    private func delete() {
        SecItemDelete(query() as CFDictionary)
    }
}
