// ── Auth Client ──────────────────────────────────────────────
// Generic auth client matching @bytelyst/auth-client TypeScript interface.
// Login, register, refresh, forgot/reset/change password, verify email, delete account.
// 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 error(String)
}

// MARK: - Auth Client

/// Generic auth client for all ByteLyst iOS apps.
/// Handles login, register, token refresh, password operations, and account management.
/// 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.
    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)
        let result = try JSONDecoder().decode(TokenResponse.self, from: data)
        saveTokens(access: result.accessToken, refresh: result.refreshToken)
        startRefreshTimer()
        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()
        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)
    }

    /// 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 45 minutes (tokens typically expire in 1 hour)
        refreshTimer = Timer.scheduledTimer(withTimeInterval: 45 * 60, repeats: true) { [weak self] _ in
            guard let self else { return }
            Task { await self.refreshAccessToken() }
        }
    }

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