import Foundation
import LocalAuthentication
import Security

// MARK: - BiometricError

enum BiometricError: LocalizedError {
    case notAvailable
    case authenticationFailed
    case saveFailed(OSStatus)
    case loadFailed
    case cancelled

    var errorDescription: String? {
        switch self {
        case .notAvailable:
            return "Biometric authentication is not available on this device."
        case .authenticationFailed:
            return "Biometric authentication failed."
        case .saveFailed(let status):
            return "Failed to save biometric data (OSStatus \(status))."
        case .loadFailed:
            return "Failed to load biometric data."
        case .cancelled:
            return "Biometric authentication was cancelled."
        }
    }
}

// MARK: - BiometricType

enum BiometricType {
    case faceID
    case touchID
    case none
}

// MARK: - BiometricAuthManager

/// Manages biometric authentication (Face ID / Touch ID) for account unlock.
///
/// **Security model (iOS canonical, Secure Enclave):**
/// - Password is stored in Keychain with `SecAccessControl(.biometryCurrentSet)`.
/// - The Secure Enclave hardware protects the item — it is physically inaccessible
///   without successful biometric authentication, even on jailbroken devices.
/// - `.biometryCurrentSet` invalidates the item if biometric enrollment changes
///   (new face/fingerprint added or all removed).
/// - `.accessibleWhenPasscodeSetThisDeviceOnly` ensures the item is deleted
///   if the user removes their device passcode.
/// - Reading the item via `SecItemCopyMatching` with an `LAContext` automatically
///   triggers the system Face ID / Touch ID prompt — no separate `evaluatePolicy` needed.
///
/// **Flow:**
/// 1. User enables biometric in Settings → enters password → password saved to
///    biometric-protected Keychain via `savePassword()`
/// 2. On next unlock → `loadPassword()` triggers Face ID via Secure Enclave →
///    system shows biometric prompt → on success, password is returned
/// 3. Password is passed to `SessionManager.startSession(password:)`
final class BiometricAuthManager: @unchecked Sendable {

    static let shared = BiometricAuthManager()
    private init() {}

    private let keychainService = "com.rosetta.messenger.biometric"
    private let enabledKeyPrefix = "biometric_enabled_"

    // MARK: - Availability

    /// Returns the type of biometric authentication available on the device.
    var availableBiometricType: BiometricType {
        let context = LAContext()
        var error: NSError?
        guard context.canEvaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            error: &error
        ) else {
            return .none
        }
        switch context.biometryType {
        case .faceID: return .faceID
        case .touchID: return .touchID
        default: return .none
        }
    }

    /// Whether any biometric authentication is available on the device.
    var isBiometricAvailable: Bool {
        availableBiometricType != .none
    }

    /// Localized name for the available biometric type ("Face ID" / "Touch ID").
    var biometricName: String {
        switch availableBiometricType {
        case .faceID: return "Face ID"
        case .touchID: return "Touch ID"
        case .none: return "Biometric"
        }
    }

    /// SF Symbol name for the available biometric type.
    var biometricIconName: String {
        switch availableBiometricType {
        case .faceID: return "faceid"
        case .touchID: return "touchid"
        case .none: return "lock.open.fill"
        }
    }

    // MARK: - Enabled State (UserDefaults)

    /// Whether biometric unlock is enabled for the given account.
    func isBiometricEnabled(forAccount publicKey: String) -> Bool {
        UserDefaults.standard.bool(forKey: enabledKeyPrefix + publicKey)
    }

    /// Sets the biometric enabled flag for the given account.
    func setBiometricEnabled(_ enabled: Bool, forAccount publicKey: String) {
        UserDefaults.standard.set(enabled, forKey: enabledKeyPrefix + publicKey)
    }

    // MARK: - Password Storage (Keychain + Secure Enclave)

    /// Saves the account password to Keychain with biometric protection.
    ///
    /// The item is protected by `SecAccessControl` with `.biometryCurrentSet`,
    /// meaning it can only be read after successful Face ID / Touch ID authentication,
    /// and is invalidated if biometric enrollment changes.
    func savePassword(_ password: String, forAccount publicKey: String) throws {
        guard let data = password.data(using: .utf8) else { return }

        let key = passwordKey(for: publicKey)

        // Delete existing entry first
        let deleteQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: keychainService,
            kSecAttrAccount as String: key,
        ]
        SecItemDelete(deleteQuery as CFDictionary)

        // Create biometric-protected access control (Secure Enclave)
        var accessError: Unmanaged<CFError>?
        guard let accessControl = SecAccessControlCreateWithFlags(
            nil,
            kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
            .biometryCurrentSet,
            &accessError
        ) else {
            throw BiometricError.saveFailed(-1)
        }

        // Save with biometric protection — no LAContext needed for write,
        // biometric is only required on read.
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: keychainService,
            kSecAttrAccount as String: key,
            kSecValueData as String: data,
            kSecAttrAccessControl as String: accessControl,
        ]

        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            throw BiometricError.saveFailed(status)
        }
    }

    /// Loads the stored password from Keychain. Triggers Face ID / Touch ID automatically
    /// via Secure Enclave — the system biometric prompt is shown as part of the Keychain read.
    ///
    /// Must be called via `async` — runs on background thread because `SecItemCopyMatching`
    /// blocks while the biometric UI is displayed.
    func loadPassword(forAccount publicKey: String) async throws -> String {
        let key = passwordKey(for: publicKey)
        let serviceName = keychainService

        return try await Task.detached(priority: .userInitiated) {
            let context = LAContext()
            context.localizedReason = "Unlock Rosetta"

            let query: [String: Any] = [
                kSecClass as String: kSecClassGenericPassword,
                kSecAttrService as String: serviceName,
                kSecAttrAccount as String: key,
                kSecReturnData as String: true,
                kSecMatchLimit as String: kSecMatchLimitOne,
                kSecUseAuthenticationContext as String: context,
            ]

            var result: AnyObject?
            let status = SecItemCopyMatching(query as CFDictionary, &result)

            switch status {
            case errSecSuccess:
                guard let data = result as? Data,
                      let password = String(data: data, encoding: .utf8) else {
                    throw BiometricError.loadFailed
                }
                return password
            case errSecUserCanceled:
                throw BiometricError.cancelled
            case errSecAuthFailed:
                throw BiometricError.authenticationFailed
            case errSecItemNotFound:
                throw BiometricError.loadFailed
            default:
                throw BiometricError.loadFailed
            }
        }.value
    }

    /// Deletes the stored password for the given account.
    func deletePassword(forAccount publicKey: String) {
        let key = passwordKey(for: publicKey)
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: keychainService,
            kSecAttrAccount as String: key,
        ]
        SecItemDelete(query as CFDictionary)
    }

    /// Whether a biometric-protected password exists for the given account.
    /// Uses an `LAContext` with `interactionNotAllowed = true` to check existence
    /// without triggering biometric UI.
    func hasStoredPassword(forAccount publicKey: String) -> Bool {
        let key = passwordKey(for: publicKey)
        let context = LAContext()
        context.interactionNotAllowed = true
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: keychainService,
            kSecAttrAccount as String: key,
            kSecUseAuthenticationContext as String: context,
        ]
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        // errSecInteractionNotAllowed = item exists but requires biometric auth
        return status == errSecInteractionNotAllowed || status == errSecSuccess
    }

    // MARK: - Combined Unlock

    /// Loads stored password with biometric authentication (triggers Face ID / Touch ID).
    /// The Secure Enclave handles the entire biometric flow — no separate `evaluatePolicy` needed.
    /// Returns the password string for use with `SessionManager.startSession(password:)`.
    func unlockWithBiometric(forAccount publicKey: String) async throws -> String {
        try await loadPassword(forAccount: publicKey)
    }

    /// Whether biometric unlock can be used right now for the given account.
    func canUseBiometric(forAccount publicKey: String) -> Bool {
        isBiometricAvailable
            && isBiometricEnabled(forAccount: publicKey)
            && hasStoredPassword(forAccount: publicKey)
    }

    // MARK: - Standalone Authentication

    /// Shows the system biometric prompt (Face ID / Touch ID) for identity confirmation.
    /// Use this when you need biometric verification without reading a Keychain item.
    func authenticate(reason: String = "Unlock Rosetta") async throws {
        let context = LAContext()
        var error: NSError?

        guard context.canEvaluatePolicy(
            .deviceOwnerAuthenticationWithBiometrics,
            error: &error
        ) else {
            throw BiometricError.notAvailable
        }

        do {
            let success = try await context.evaluatePolicy(
                .deviceOwnerAuthenticationWithBiometrics,
                localizedReason: reason
            )
            guard success else { throw BiometricError.authenticationFailed }
        } catch let laError as LAError {
            switch laError.code {
            case .userCancel, .appCancel, .systemCancel:
                throw BiometricError.cancelled
            case .biometryNotAvailable, .biometryNotEnrolled:
                throw BiometricError.notAvailable
            default:
                throw BiometricError.authenticationFailed
            }
        }
    }

    // MARK: - Cleanup

    /// Removes all biometric data for the given account.
    func clearAll(forAccount publicKey: String) {
        deletePassword(forAccount: publicKey)
        setBiometricEnabled(false, forAccount: publicKey)
    }

    // MARK: - Private

    private func passwordKey(for publicKey: String) -> String {
        "biometric_password_\(publicKey)"
    }
}
