// ── Auth Client ──────────────────────────────────────────────
// Generic auth client matching @bytelyst/auth-client TypeScript interface.
// Login, register, refresh, forgot/reset/change password, verify email, delete account.
// SmartAuth v2: social login, MFA (TOTP), passkeys, device trust.
// Token storage via BLKeychain. Product apps configure via BLPlatformConfig.

import Foundation

// MARK: - Public Types

public struct BLAuthUser: Codable, Sendable {
    public let id: String
    public let email: String
    public let displayName: String
    public let plan: String
    public let role: String

    enum CodingKeys: String, CodingKey {
        case id, email, displayName, plan, role
    }

    public init(id: String, email: String, displayName: String, plan: String = "free", role: String = "user") {
        self.id = id
        self.email = email
        self.displayName = displayName
        self.plan = plan
        self.role = role
    }

    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        id = try c.decode(String.self, forKey: .id)
        email = try c.decode(String.self, forKey: .email)
        displayName = try c.decode(String.self, forKey: .displayName)
        plan = try c.decodeIfPresent(String.self, forKey: .plan) ?? "free"
        role = try c.decodeIfPresent(String.self, forKey: .role) ?? "user"
    }
}

public enum BLAuthState: Sendable {
    case loading
    case loggedOut
    case loggedIn(BLAuthUser)
    case mfaRequired(BLMfaChallenge)
    case error(String)
}

// MARK: - SmartAuth Types

/// MFA challenge returned when login requires multi-factor verification.
public struct BLMfaChallenge: Codable, Sendable {
    public let mfaRequired: Bool
    public let mfaChallenge: String
    public let methods: [String]

    public init(mfaRequired: Bool, mfaChallenge: String, methods: [String]) {
        self.mfaRequired = mfaRequired
        self.mfaChallenge = mfaChallenge
        self.methods = methods
    }
}

/// TOTP setup response with secret URI and recovery codes.
public struct BLTotpSetup: Codable, Sendable {
    public let otpauthUri: String
    public let qrCode: String
    public let recoveryCodes: [String]
}

/// MFA status for the current user.
public struct BLMfaStatus: Codable, Sendable {
    public let mfaEnabled: Bool
    public let methods: [String]
    public let recoveryCodesRemaining: Int
}

/// Linked OAuth provider.
public struct BLAuthProvider: Codable, Sendable {
    public let provider: String
    public let email: String
    public let linkedAt: String
    public let lastUsedAt: String?
}

/// Passkey metadata.
public struct BLPasskey: Codable, Sendable {
    public let id: String
    public let friendlyName: String
    public let deviceType: String
    public let lastUsedAt: String?
    public let createdAt: String
}

/// Trusted/remembered device.
public struct BLDevice: Codable, Sendable {
    public let fingerprint: String
    public let trustLevel: String
    public let deviceInfo: DeviceInfo?
    public let lastIp: String?
    public let lastLocation: String?
    public let trustExpiresAt: String?
    public let createdAt: String
    public let lastSeenAt: String
    public let isTrusted: Bool

    public struct DeviceInfo: Codable, Sendable {
        public let userAgent: String?
        public let platform: String?
        public let model: String?
        public let os: String?
    }

    /// Convenience: display name derived from device info.
    public var name: String {
        deviceInfo?.model ?? deviceInfo?.platform ?? fingerprint.prefix(8).description
    }

    /// Convenience: platform string.
    public var platform: String {
        deviceInfo?.platform ?? "unknown"
    }

    /// Stable identifier for SwiftUI ForEach.
    public var id: String { fingerprint }
}

/// Login event for security log.
public struct BLLoginEvent: Codable, Sendable {
    public let id: String
    public let eventType: String
    public let method: String
    public let ip: String
    public let geo: BLGeo?
    public let riskScore: Int
    public let createdAt: String
}

/// Geo location.
public struct BLGeo: Codable, Sendable {
    public let country: String
    public let city: String
}

// MARK: - Phase 5C–5E Types

/// Decrypted TOTP secret for local code generation in the auth app.
public struct BLTotpSecret: Codable, Sendable {
    public let secret: String
    public let issuer: String
    public let accountName: String
    public let digits: Int
    public let period: Int
    public let algorithm: String
}

/// Pending push MFA approval.
public struct BLPushApproval: Codable, Sendable {
    public let id: String
    public let requestProductId: String
    public let requestPlatform: String
    public let requestIp: String
    public let requestGeo: BLGeo?
    public let createdAt: String
    public let expiresAt: String
}

/// Push approval response after approve/deny.
public struct BLPushApprovalResponse: Codable, Sendable {
    public let id: String
    public let status: String
    public let respondedAt: String?
}

/// QR challenge for desktop/TV login.
public struct BLQrChallenge: Codable, Sendable {
    public let id: String
    public let challengeToken: String
    public let expiresAt: String
}

/// QR challenge poll status.
public struct BLQrStatus: Codable, Sendable {
    public let status: String
    public let accessToken: String?
    public let refreshToken: String?
    public let user: BLAuthUser?
}

// MARK: - Auth Errors

/// Auth-specific errors for SmartAuth flows.
public enum BLAuthError: LocalizedError {
    case mfaRequired(BLMfaChallenge)

    public var errorDescription: String? {
        switch self {
        case .mfaRequired: return "Multi-factor authentication required"
        }
    }
}

// MARK: - Auth Client

/// Generic auth client for all ByteLyst iOS apps.
/// Handles login, register, token refresh, password operations, and account management.
/// SmartAuth v2: social login, MFA, passkeys, device trust, step-up auth.
/// Stores tokens in Keychain. Notifies via `onAuthStateChanged` callback.
public final class BLAuthClient {

    private let config: BLPlatformConfig
    private let client: BLPlatformClient
    private let keychainService: String

    /// Called whenever auth state changes. Set by the product app's view model / observable.
    public var onAuthStateChanged: ((BLAuthState) -> Void)?

    /// Called when tokens are updated (for wiring into sync managers).
    public var onTokensUpdated: ((String?) -> Void)?

    private var refreshTimer: Timer?

    private struct TokenResponse: Codable {
        let accessToken: String
        let refreshToken: String
        let user: BLAuthUser
    }

    private struct RefreshResponse: Codable {
        let accessToken: String
        let refreshToken: String
    }

    private struct MessageResponse: Codable {
        let message: String
    }

    public init(config: BLPlatformConfig, client: BLPlatformClient) {
        self.config = config
        self.client = client
        self.keychainService = config.bundleId
    }

    // MARK: - Token Access

    public var accessToken: String? {
        BLKeychain.read(service: keychainService, key: "access_token")
    }

    public var refreshTokenValue: String? {
        BLKeychain.read(service: keychainService, key: "refresh_token")
    }

    public var isAuthenticated: Bool {
        accessToken != nil && !(accessToken?.isEmpty ?? true)
    }

    // MARK: - Auth Operations

    /// Login with email and password.
    /// If MFA is enabled, throws `BLAuthError.mfaRequired` with the challenge.
    public func login(email: String, password: String) async throws -> BLAuthUser {
        let body: [String: String] = [
            "email": email,
            "password": password,
            "productId": config.productId,
        ]
        let (data, _) = try await client.rawRequest(path: "/api/auth/login", method: "POST", body: body)
        // Check for MFA challenge response
        if let challenge = try? JSONDecoder().decode(BLMfaChallenge.self, from: data),
           challenge.mfaRequired {
            onAuthStateChanged?(.mfaRequired(challenge))
            throw BLAuthError.mfaRequired(challenge)
        }
        let result = try JSONDecoder().decode(TokenResponse.self, from: data)
        saveTokens(access: result.accessToken, refresh: result.refreshToken)
        startRefreshTimer()
        onAuthStateChanged?(.loggedIn(result.user))
        return result.user
    }

    /// Register a new account.
    public func register(displayName: String, email: String, password: String) async throws -> BLAuthUser {
        let body: [String: String] = [
            "displayName": displayName,
            "email": email,
            "password": password,
            "productId": config.productId,
        ]
        let (data, _) = try await client.rawRequest(path: "/api/auth/register", method: "POST", body: body)
        let result = try JSONDecoder().decode(TokenResponse.self, from: data)
        saveTokens(access: result.accessToken, refresh: result.refreshToken)
        startRefreshTimer()
        onAuthStateChanged?(.loggedIn(result.user))
        return result.user
    }

    /// Fetch current user profile.
    public func getMe() async throws -> BLAuthUser {
        return try await client.request(path: "/api/auth/me", responseType: BLAuthUser.self)
    }

    /// Refresh the access token using the stored refresh token.
    @discardableResult
    public func refreshAccessToken() async -> Bool {
        guard let rt = refreshTokenValue, !rt.isEmpty else { return false }
        let body = ["refreshToken": rt]

        do {
            let (data, _) = try await client.rawRequest(path: "/api/auth/refresh", method: "POST", body: body)
            let result = try JSONDecoder().decode(RefreshResponse.self, from: data)
            saveTokens(access: result.accessToken, refresh: result.refreshToken)
            return true
        } catch {
            if let netErr = error as? BLNetworkError, netErr.statusCode == 401 {
                logout()
            }
            return false
        }
    }

    /// Request password reset email.
    public func forgotPassword(email: String) async throws {
        let body = ["email": email, "productId": config.productId]
        _ = try await client.rawRequest(path: "/api/auth/forgot-password", method: "POST", body: body)
    }

    /// Reset password with token.
    public func resetPassword(token: String, newPassword: String) async throws {
        let body = ["token": token, "newPassword": newPassword]
        _ = try await client.rawRequest(path: "/api/auth/reset-password", method: "POST", body: body)
    }

    /// Change password (authenticated).
    public func changePassword(currentPassword: String, newPassword: String) async throws {
        let body = ["currentPassword": currentPassword, "newPassword": newPassword]
        _ = try await client.rawRequest(path: "/api/auth/change-password", method: "POST", body: body)
    }

    /// Verify email with token.
    public func verifyEmail(token: String) async throws {
        let body = ["token": token]
        _ = try await client.rawRequest(path: "/api/auth/verify-email", method: "POST", body: body)
    }

    /// Resend verification email.
    public func resendVerification(email: String) async throws {
        let body = ["email": email, "productId": config.productId]
        _ = try await client.rawRequest(path: "/api/auth/resend-verification", method: "POST", body: body)
    }

    /// Delete account (requires password confirmation).
    public func deleteAccount(password: String) async throws {
        let body = ["password": password]
        _ = try await client.rawRequest(path: "/api/auth/account", method: "DELETE", body: body)
        logout()
    }

    /// Logout — clear tokens and notify.
    public func logout() {
        stopRefreshTimer()
        clearTokens()
        onAuthStateChanged?(.loggedOut)
    }

    // MARK: - Social Login (SmartAuth v2)

    /// Login with Google id_token.
    public func loginWithGoogle(idToken: String) async throws -> BLAuthUser {
        return try await socialLogin(provider: "google", idToken: idToken)
    }

    /// Login with Microsoft id_token.
    public func loginWithMicrosoft(idToken: String) async throws -> BLAuthUser {
        return try await socialLogin(provider: "microsoft", idToken: idToken)
    }

    /// Login with Apple id_token.
    public func loginWithApple(idToken: String) async throws -> BLAuthUser {
        return try await socialLogin(provider: "apple", idToken: idToken)
    }

    /// Generic social login — sends id_token to /auth/oauth/{provider}.
    private func socialLogin(provider: String, idToken: String) async throws -> BLAuthUser {
        let body: [String: String] = ["idToken": idToken, "productId": config.productId]
        let (data, _) = try await client.rawRequest(
            path: "/api/auth/oauth/\(provider)",
            method: "POST",
            body: body
        )
        // Server may return MFA challenge or tokens
        if let challenge = try? JSONDecoder().decode(BLMfaChallenge.self, from: data),
           challenge.mfaRequired {
            onAuthStateChanged?(.mfaRequired(challenge))
            throw BLAuthError.mfaRequired(challenge)
        }
        let result = try JSONDecoder().decode(TokenResponse.self, from: data)
        saveTokens(access: result.accessToken, refresh: result.refreshToken)
        startRefreshTimer()
        onAuthStateChanged?(.loggedIn(result.user))
        return result.user
    }

    // MARK: - MFA (SmartAuth v2)

    /// Verify MFA challenge (TOTP code or recovery code).
    public func verifyMfa(challengeToken: String, code: String, method: String = "totp") async throws -> BLAuthUser {
        let body: [String: String] = [
            "challengeToken": challengeToken,
            "code": code,
            "method": method,
        ]
        let (data, _) = try await client.rawRequest(path: "/api/auth/mfa/verify", method: "POST", body: body)
        let result = try JSONDecoder().decode(TokenResponse.self, from: data)
        saveTokens(access: result.accessToken, refresh: result.refreshToken)
        startRefreshTimer()
        return result.user
    }

    /// Begin TOTP setup — returns otpauth URI, QR code, and recovery codes.
    public func setupTotp() async throws -> BLTotpSetup {
        return try await client.request(path: "/api/auth/mfa/totp/setup", method: "POST", responseType: BLTotpSetup.self)
    }

    /// Verify TOTP setup with a code from the authenticator app.
    public func verifyTotpSetup(code: String) async throws {
        let body = ["code": code]
        _ = try await client.rawRequest(path: "/api/auth/mfa/totp/verify-setup", method: "POST", body: body)
    }

    /// Disable MFA (requires step-up token via X-Step-Up-Token header).
    public func disableMfa() async throws {
        _ = try await client.rawRequest(path: "/api/auth/mfa/totp", method: "DELETE")
    }

    /// Get current MFA status.
    public func getMfaStatus() async throws -> BLMfaStatus {
        return try await client.request(path: "/api/auth/mfa/status", responseType: BLMfaStatus.self)
    }

    /// Regenerate recovery codes (requires step-up).
    public func regenerateRecoveryCodes() async throws -> [String] {
        struct CodesResponse: Codable { let recoveryCodes: [String] }
        let result = try await client.request(
            path: "/api/auth/mfa/recovery/regenerate",
            method: "POST",
            responseType: CodesResponse.self
        )
        return result.recoveryCodes
    }

    // MARK: - Providers (SmartAuth v2)

    /// List linked OAuth providers.
    public func getProviders() async throws -> [BLAuthProvider] {
        return try await client.request(path: "/api/auth/providers", responseType: [BLAuthProvider].self)
    }

    /// Link an OAuth provider to the current account.
    public func linkProvider(provider: String, idToken: String) async throws {
        let body = ["provider": provider, "idToken": idToken]
        _ = try await client.rawRequest(path: "/api/auth/providers/link", method: "POST", body: body)
    }

    /// Unlink an OAuth provider.
    public func unlinkProvider(provider: String) async throws {
        _ = try await client.rawRequest(path: "/api/auth/providers/\(provider)", method: "DELETE")
    }

    // MARK: - Passkeys (SmartAuth v2)

    /// Get passkey registration options from server.
    public func getPasskeyRegistrationOptions() async throws -> Data {
        let (data, _) = try await client.rawRequest(
            path: "/api/auth/passkeys/register/options",
            method: "POST"
        )
        return data
    }

    /// Verify passkey registration with attestation response.
    public func verifyPasskeyRegistration(attestation: [String: Any], friendlyName: String) async throws {
        var payload = attestation
        payload["friendlyName"] = friendlyName
        let jsonData = try JSONSerialization.data(withJSONObject: payload)
        // Wrap raw JSON data in a Codable struct for BLPlatformClient
        _ = try await client.rawRequest(
            path: "/api/auth/passkeys/register/verify",
            method: "POST",
            rawBody: jsonData
        )
    }

    /// Get passkey authentication options from server.
    public func getPasskeyAuthenticationOptions() async throws -> Data {
        let (data, _) = try await client.rawRequest(
            path: "/api/auth/passkeys/authenticate/options",
            method: "POST"
        )
        return data
    }

    /// Verify passkey authentication with assertion response.
    public func verifyPasskeyAuthentication(assertion: [String: Any]) async throws -> BLAuthUser {
        let jsonData = try JSONSerialization.data(withJSONObject: assertion)
        let (responseData, _) = try await client.rawRequest(
            path: "/api/auth/passkeys/authenticate/verify",
            method: "POST",
            rawBody: jsonData
        )
        let result = try JSONDecoder().decode(TokenResponse.self, from: responseData)
        saveTokens(access: result.accessToken, refresh: result.refreshToken)
        startRefreshTimer()
        onAuthStateChanged?(.loggedIn(result.user))
        return result.user
    }

    /// List registered passkeys.
    public func listPasskeys() async throws -> [BLPasskey] {
        return try await client.request(path: "/api/auth/passkeys", responseType: [BLPasskey].self)
    }

    /// Delete a passkey (requires step-up).
    public func deletePasskey(passkeyId: String) async throws {
        _ = try await client.rawRequest(path: "/api/auth/passkeys/\(passkeyId)", method: "DELETE")
    }

    // MARK: - Devices (SmartAuth v2)

    /// List devices for current user.
    public func listDevices() async throws -> [BLDevice] {
        struct DevicesResponse: Codable { let devices: [BLDevice] }
        let result = try await client.request(path: "/api/auth/devices", responseType: DevicesResponse.self)
        return result.devices
    }

    /// Trust the current device (promotes to trusted, skips MFA for 90 days).
    public func trustDevice() async throws {
        _ = try await client.rawRequest(path: "/api/auth/devices/trust", method: "POST")
    }

    /// Revoke trust on a specific device by fingerprint.
    public func revokeDevice(fingerprint: String) async throws {
        _ = try await client.rawRequest(path: "/api/auth/devices/\(fingerprint)", method: "DELETE")
    }

    /// Revoke all device trust.
    public func revokeAllDevices() async throws {
        _ = try await client.rawRequest(path: "/api/auth/devices/revoke-all", method: "POST")
    }

    // MARK: - Step-Up Auth (SmartAuth v2)

    /// Perform step-up authentication. Returns a short-lived step-up token.
    public func stepUp(method: String, credential: String) async throws -> String {
        let body = ["method": method, "credential": credential]
        struct StepUpResponse: Codable { let stepUpToken: String }
        let result = try await client.request(
            path: "/api/auth/step-up",
            method: "POST",
            body: body,
            responseType: StepUpResponse.self
        )
        return result.stepUpToken
    }

    // MARK: - Login History (SmartAuth v2)

    /// Get login events for the current user.
    public func getLoginHistory(limit: Int = 20) async throws -> [BLLoginEvent] {
        struct EventsResponse: Codable { let events: [BLLoginEvent] }
        let result = try await client.request(
            path: "/api/auth/login-events?limit=\(limit)",
            responseType: EventsResponse.self
        )
        return result.events
    }

    // MARK: - TOTP Secret Retrieval (Phase 5C)

    /// Get the decrypted TOTP secret for local code generation (auth app).
    public func getTotpSecret() async throws -> BLTotpSecret {
        return try await client.request(path: "/api/auth/mfa/totp/secret", responseType: BLTotpSecret.self)
    }

    // MARK: - Push Approvals (Phase 5D)

    /// List pending push MFA approvals for the current user.
    public func getPendingApprovals() async throws -> [BLPushApproval] {
        return try await client.request(path: "/api/auth/mfa/push/pending", responseType: [BLPushApproval].self)
    }

    /// Respond to a push MFA approval (approve or deny).
    public func respondToApproval(approvalId: String, action: String) async throws -> BLPushApprovalResponse {
        let body = ["action": action]
        return try await client.request(path: "/api/auth/mfa/push/\(approvalId)/respond", method: "POST", body: body, responseType: BLPushApprovalResponse.self)
    }

    // MARK: - QR Auth (Phase 5E)

    /// Confirm a QR login challenge from the auth app.
    public func confirmQrLogin(challengeToken: String) async throws {
        let body = ["challengeToken": challengeToken]
        _ = try await client.rawRequest(path: "/api/auth/qr/confirm", method: "POST", body: body)
    }

    /// Restore session from stored tokens. Call on app launch.
    public func restoreSession() async {
        guard isAuthenticated else {
            onAuthStateChanged?(.loggedOut)
            return
        }

        onAuthStateChanged?(.loading)

        do {
            let user = try await getMe()
            onAuthStateChanged?(.loggedIn(user))
            startRefreshTimer()
        } catch {
            // Try refresh
            let ok = await refreshAccessToken()
            if ok {
                do {
                    let user = try await getMe()
                    onAuthStateChanged?(.loggedIn(user))
                    startRefreshTimer()
                } catch {
                    onAuthStateChanged?(.loggedOut)
                }
            } else {
                onAuthStateChanged?(.loggedOut)
            }
        }
    }

    // MARK: - Private

    private func saveTokens(access: String, refresh: String) {
        BLKeychain.save(service: keychainService, key: "access_token", value: access)
        BLKeychain.save(service: keychainService, key: "refresh_token", value: refresh)
        client.authToken = access
        onTokensUpdated?(access)
    }

    private func clearTokens() {
        BLKeychain.delete(service: keychainService, key: "access_token")
        BLKeychain.delete(service: keychainService, key: "refresh_token")
        client.authToken = nil
        onTokensUpdated?(nil)
    }

    private func startRefreshTimer() {
        stopRefreshTimer()
        // Refresh every 12 minutes (access tokens expire in 15 minutes per PRD)
        refreshTimer = Timer.scheduledTimer(withTimeInterval: 12 * 60, repeats: true) { [weak self] _ in
            guard let self else { return }
            Task { await self.refreshAccessToken() }
        }
    }

    private func stopRefreshTimer() {
        refreshTimer?.invalidate()
        refreshTimer = nil
    }
}
