package com.bytelyst.platform

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer

/**
 * Auth client for platform-service.
 *
 * Manages login, register, token refresh, password operations.
 * SmartAuth v2: social login, MFA (TOTP), device trust, step-up auth.
 * Tokens are stored in [BLSecureStore] (EncryptedSharedPreferences).
 * Auth state is exposed as a [StateFlow] for reactive UI binding.
 *
 * Mirrors the Swift BLAuthClient API.
 */
class BLAuthClient(
    private val config: BLPlatformConfig,
    private val secureStore: BLSecureStore,
) {
    // ── Data classes ──────────────────────────────────────────

    @Serializable
    data class AuthUser(
        val id: String = "",
        val email: String = "",
        @SerialName("displayName")
        val name: String = "",
        val plan: String = "free",
        val role: String = "user",
    )

    @Serializable
    data class TokenResponse(
        val accessToken: String,
        val refreshToken: String,
        val user: AuthUser,
    )

    @Serializable
    data class RefreshResponse(
        val accessToken: String,
        val refreshToken: String,
    )

    @Serializable
    data class MessageResponse(val message: String)

    // ── SmartAuth v2 types ────────────────────────────────────

    @Serializable
    data class MfaChallenge(
        val mfaRequired: Boolean = false,
        val mfaChallenge: String = "",
        val methods: List<String> = emptyList(),
    )

    @Serializable
    data class TotpSetup(
        val otpauthUri: String,
        val qrCode: String,
        val recoveryCodes: List<String>,
    )

    @Serializable
    data class MfaStatus(
        val mfaEnabled: Boolean,
        val methods: List<String>,
        val recoveryCodesRemaining: Int,
    )

    @Serializable
    data class AuthProvider(
        val provider: String,
        val email: String,
        val linkedAt: String,
        val lastUsedAt: String? = null,
    )

    @Serializable
    data class Device(
        val fingerprint: String,
        val trustLevel: String,
        val deviceInfo: DeviceInfo? = null,
        val lastIp: String? = null,
        val lastLocation: String? = null,
        val trustExpiresAt: String? = null,
        val createdAt: String,
        val lastSeenAt: String,
        val isTrusted: Boolean,
    ) {
        /** Convenience: display name from device info. */
        val name: String get() = deviceInfo?.model ?: deviceInfo?.platform ?: fingerprint.take(8)
        /** Convenience: platform string. */
        val platform: String get() = deviceInfo?.platform ?: "unknown"
    }

    @Serializable
    data class DeviceInfo(
        val userAgent: String? = null,
        val platform: String? = null,
        val model: String? = null,
        val os: String? = null,
    )

    @Serializable
    data class LoginEvent(
        val id: String,
        val eventType: String,
        val method: String,
        val ip: String,
        val geo: Geo? = null,
        val riskScore: Int,
        val createdAt: String,
    )

    @Serializable
    data class Geo(
        val country: String,
        val city: String,
    )

    @Serializable
    data class RecoveryCodesResponse(
        val recoveryCodes: List<String>,
    )

    @Serializable
    data class Passkey(
        val id: String,
        val friendlyName: String,
        val deviceType: String,
        val lastUsedAt: String? = null,
        val createdAt: String = "",
    )

    @Serializable
    data class StepUpResponse(
        val stepUpToken: String,
    )

    // ── Phase 5C–5E types ─────────────────────────────────────

    @Serializable
    data class TotpSecret(
        val secret: String,
        val issuer: String,
        val accountName: String,
        val digits: Int = 6,
        val period: Int = 30,
        val algorithm: String = "SHA1",
    )

    @Serializable
    data class PushApproval(
        val id: String,
        val requestProductId: String,
        val requestPlatform: String,
        val requestIp: String,
        val requestGeo: Geo? = null,
        val createdAt: String,
        val expiresAt: String,
    )

    @Serializable
    data class PushApprovalResponse(
        val id: String,
        val status: String,
        val respondedAt: String? = null,
    )

    @Serializable
    data class QrChallenge(
        val id: String,
        val challengeToken: String,
        val expiresAt: String,
    )

    @Serializable
    data class QrStatus(
        val status: String,
        val accessToken: String? = null,
        val refreshToken: String? = null,
        val user: AuthUser? = null,
    )

    /** Exception when MFA is required after login. */
    class MfaRequiredException(val challenge: MfaChallenge) : Exception("MFA required")

    sealed class AuthState {
        data object Loading : AuthState()
        data object LoggedOut : AuthState()
        data class LoggedIn(val user: AuthUser) : AuthState()
        data class MfaRequired(val challenge: MfaChallenge) : AuthState()
        data class Error(val message: String) : AuthState()
    }

    // ── State ────────────────────────────────────────────────

    private val _state = MutableStateFlow<AuthState>(AuthState.Loading)
    val state: StateFlow<AuthState> = _state.asStateFlow()

    val isLoggedIn: Boolean
        get() = _state.value is AuthState.LoggedIn

    val currentUser: AuthUser?
        get() = (_state.value as? AuthState.LoggedIn)?.user

    // ── Storage keys (bare — applicationId provides namespace) ──

    companion object {
        private const val KEY_ACCESS_TOKEN = "access_token"
        private const val KEY_REFRESH_TOKEN = "refresh_token"
        private const val KEY_USER_EMAIL = "user_email"
        private const val KEY_USER_NAME = "user_name"
        private const val KEY_USER_PLAN = "user_plan"
        private const val KEY_USER_ID = "user_id"
    }

    // ── Platform client ──────────────────────────────────────

    internal val client = BLPlatformClient(config) { getAccessToken() }

    // ── Token management ─────────────────────────────────────

    fun getAccessToken(): String? = secureStore.read(KEY_ACCESS_TOKEN)

    fun getRefreshToken(): String? = secureStore.read(KEY_REFRESH_TOKEN)

    private fun setTokens(accessToken: String, refreshToken: String) {
        secureStore.save(KEY_ACCESS_TOKEN, accessToken)
        secureStore.save(KEY_REFRESH_TOKEN, refreshToken)
    }

    private fun saveUser(user: AuthUser) {
        secureStore.save(KEY_USER_ID, user.id)
        secureStore.save(KEY_USER_EMAIL, user.email)
        secureStore.save(KEY_USER_NAME, user.name)
        secureStore.save(KEY_USER_PLAN, user.plan)
    }

    private fun clearAll() {
        secureStore.delete(KEY_ACCESS_TOKEN)
        secureStore.delete(KEY_REFRESH_TOKEN)
        secureStore.delete(KEY_USER_EMAIL)
        secureStore.delete(KEY_USER_NAME)
        secureStore.delete(KEY_USER_PLAN)
        secureStore.delete(KEY_USER_ID)
    }

    // ── Session check ────────────────────────────────────────

    /**
     * Check for an existing session from stored tokens.
     * Call once at app startup to restore auth state.
     */
    fun checkExistingSession() {
        val token = secureStore.read(KEY_ACCESS_TOKEN)
        val email = secureStore.read(KEY_USER_EMAIL)
        if (!token.isNullOrBlank() && !email.isNullOrBlank()) {
            val user = AuthUser(
                id = secureStore.read(KEY_USER_ID) ?: "",
                email = email,
                name = secureStore.read(KEY_USER_NAME) ?: "",
                plan = secureStore.read(KEY_USER_PLAN) ?: "free",
            )
            _state.value = AuthState.LoggedIn(user)
        } else {
            _state.value = AuthState.LoggedOut
        }
    }

    // ── Auth operations ──────────────────────────────────────

    suspend fun login(email: String, password: String) {
        _state.value = AuthState.Loading
        try {
            val body = encodeMap(mapOf("email" to email, "password" to password, "productId" to config.productId))
            val response = client.request("POST", "/api/auth/login", body, skipAuth = true)
            // Check for MFA challenge
            try {
                val challenge = client.json.decodeFromString<MfaChallenge>(response)
                if (challenge.mfaRequired) {
                    _state.value = AuthState.MfaRequired(challenge)
                    return
                }
            } catch (_: Exception) { /* Not an MFA response, continue */ }
            val result = client.json.decodeFromString<TokenResponse>(response)
            handleAuthResult(result)
        } catch (e: Exception) {
            _state.value = AuthState.Error(e.message ?: "Login failed")
        }
    }

    suspend fun register(name: String, email: String, password: String) {
        _state.value = AuthState.Loading
        try {
            val body = client.json.encodeToString(
                MapSerializer(String.serializer(), String.serializer()),
                mapOf(
                    "email" to email,
                    "displayName" to name,
                    "password" to password,
                    "productId" to config.productId,
                ),
            )
            val response = client.request("POST", "/api/auth/register", body, skipAuth = true)
            val result = client.json.decodeFromString<TokenResponse>(response)
            handleAuthResult(result)
        } catch (e: Exception) {
            _state.value = AuthState.Error(e.message ?: "Registration failed")
        }
    }

    fun logout() {
        clearAll()
        _state.value = AuthState.LoggedOut
    }

    // ── Token refresh (singleton guard) ──────────────────────

    private val refreshMutex = Mutex()

    suspend fun refreshAccessToken(): Boolean = refreshMutex.withLock {
        val rt = getRefreshToken() ?: return false
        return try {
            val body = client.json.encodeToString(
                MapSerializer(String.serializer(), String.serializer()),
                mapOf("refreshToken" to rt),
            )
            val response = client.request("POST", "/api/auth/refresh", body, skipAuth = true)
            val result = client.json.decodeFromString<RefreshResponse>(response)
            setTokens(result.accessToken, result.refreshToken)
            true
        } catch (e: BLApiException) {
            if (e.statusCode == 401) {
                withContext(Dispatchers.Main) { logout() }
            }
            false
        } catch (_: Exception) {
            false
        }
    }

    // ── Password management ──────────────────────────────────

    suspend fun forgotPassword(email: String): String {
        val body = client.json.encodeToString(
            MapSerializer(String.serializer(), String.serializer()),
            mapOf("email" to email, "productId" to config.productId),
        )
        val response = client.request("POST", "/api/auth/forgot-password", body, skipAuth = true)
        return client.json.decodeFromString<MessageResponse>(response).message
    }

    suspend fun resetPassword(token: String, newPassword: String): String {
        val body = client.json.encodeToString(
            MapSerializer(String.serializer(), String.serializer()),
            mapOf("token" to token, "newPassword" to newPassword),
        )
        val response = client.request("POST", "/api/auth/reset-password", body, skipAuth = true)
        return client.json.decodeFromString<MessageResponse>(response).message
    }

    suspend fun changePassword(currentPassword: String, newPassword: String): String {
        val body = client.json.encodeToString(
            MapSerializer(String.serializer(), String.serializer()),
            mapOf("currentPassword" to currentPassword, "newPassword" to newPassword),
        )
        val response = client.request("POST", "/api/auth/change-password", body)
        return client.json.decodeFromString<MessageResponse>(response).message
    }

    // ── Email verification ───────────────────────────────────

    suspend fun verifyEmail(token: String): String {
        val body = client.json.encodeToString(
            MapSerializer(String.serializer(), String.serializer()),
            mapOf("token" to token),
        )
        val response = client.request("POST", "/api/auth/verify-email", body)
        return client.json.decodeFromString<MessageResponse>(response).message
    }

    suspend fun resendVerification(email: String): String {
        val body = client.json.encodeToString(
            MapSerializer(String.serializer(), String.serializer()),
            mapOf("email" to email, "productId" to config.productId),
        )
        val response = client.request("POST", "/api/auth/resend-verification", body)
        return client.json.decodeFromString<MessageResponse>(response).message
    }

    // ── Account management ───────────────────────────────────

    suspend fun deleteAccount(password: String): String {
        val body = client.json.encodeToString(
            MapSerializer(String.serializer(), String.serializer()),
            mapOf("password" to password),
        )
        val response = client.request("DELETE", "/api/auth/account", body)
        clearAll()
        _state.value = AuthState.LoggedOut
        return client.json.decodeFromString<MessageResponse>(response).message
    }

    suspend fun getMe(): AuthUser {
        val response = client.request("GET", "/api/auth/me")
        return client.json.decodeFromString<AuthUser>(response)
    }

    // ── Social Login (SmartAuth v2) ─────────────────────────

    /** Login with Google id_token. */
    suspend fun loginWithGoogle(idToken: String): AuthUser = socialLogin("google", idToken)

    /** Login with Microsoft id_token. */
    suspend fun loginWithMicrosoft(idToken: String): AuthUser = socialLogin("microsoft", idToken)

    /** Login with Apple id_token. */
    suspend fun loginWithApple(idToken: String): AuthUser = socialLogin("apple", idToken)

    private suspend fun socialLogin(provider: String, idToken: String): AuthUser {
        _state.value = AuthState.Loading
        val body = encodeMap(mapOf("idToken" to idToken, "productId" to config.productId))
        val response = client.request("POST", "/api/auth/oauth/$provider", body, skipAuth = true)
        // Check for MFA challenge
        try {
            val challenge = client.json.decodeFromString<MfaChallenge>(response)
            if (challenge.mfaRequired) {
                _state.value = AuthState.MfaRequired(challenge)
                throw MfaRequiredException(challenge)
            }
        } catch (e: MfaRequiredException) {
            throw e
        } catch (_: Exception) { /* Not an MFA response */ }
        val result = client.json.decodeFromString<TokenResponse>(response)
        handleAuthResult(result)
        return result.user
    }

    // ── MFA (SmartAuth v2) ───────────────────────────────────

    /** Verify MFA challenge (TOTP code or recovery code). */
    suspend fun verifyMfa(challengeToken: String, code: String, method: String = "totp"): AuthUser {
        val body = encodeMap(mapOf(
            "challengeToken" to challengeToken,
            "code" to code,
            "method" to method,
        ))
        val response = client.request("POST", "/api/auth/mfa/verify", body, skipAuth = true)
        val result = client.json.decodeFromString<TokenResponse>(response)
        handleAuthResult(result)
        return result.user
    }

    /** Begin TOTP setup — returns otpauth URI, QR code, and recovery codes. */
    suspend fun setupTotp(): TotpSetup {
        val response = client.request("POST", "/api/auth/mfa/totp/setup")
        return client.json.decodeFromString<TotpSetup>(response)
    }

    /** Verify TOTP setup with a code from the authenticator app. */
    suspend fun verifyTotpSetup(code: String) {
        val body = encodeMap(mapOf("code" to code))
        client.request("POST", "/api/auth/mfa/totp/verify-setup", body)
    }

    /** Disable MFA (requires step-up token). */
    suspend fun disableMfa() {
        client.request("DELETE", "/api/auth/mfa/totp")
    }

    /** Get current MFA status. */
    suspend fun getMfaStatus(): MfaStatus {
        val response = client.request("GET", "/api/auth/mfa/status")
        return client.json.decodeFromString<MfaStatus>(response)
    }

    /** Regenerate recovery codes (requires step-up). */
    suspend fun regenerateRecoveryCodes(): List<String> {
        val response = client.request("POST", "/api/auth/mfa/recovery/regenerate")
        return client.json.decodeFromString<RecoveryCodesResponse>(response).recoveryCodes
    }

    // ── Providers (SmartAuth v2) ─────────────────────────────

    /** List linked OAuth providers. */
    suspend fun getProviders(): List<AuthProvider> {
        val response = client.request("GET", "/api/auth/providers")
        return client.json.decodeFromString<List<AuthProvider>>(response)
    }

    /** Link an OAuth provider to the current account. */
    suspend fun linkProvider(provider: String, idToken: String) {
        val body = encodeMap(mapOf("provider" to provider, "idToken" to idToken))
        client.request("POST", "/api/auth/providers/link", body)
    }

    /** Unlink an OAuth provider. */
    suspend fun unlinkProvider(provider: String) {
        client.request("DELETE", "/api/auth/providers/$provider")
    }

    // ── Devices (SmartAuth v2) ───────────────────────────────

    @Serializable
    private data class DevicesResponse(val devices: List<Device>)

    /** List devices for current user. */
    suspend fun listDevices(): List<Device> {
        val response = client.request("GET", "/api/auth/devices")
        return client.json.decodeFromString<DevicesResponse>(response).devices
    }

    /** Trust the current device (promotes to trusted, skips MFA for 90 days). */
    suspend fun trustDevice() {
        client.request("POST", "/api/auth/devices/trust")
    }

    /** Revoke trust on a specific device by fingerprint. */
    suspend fun revokeDevice(fingerprint: String) {
        client.request("DELETE", "/api/auth/devices/$fingerprint")
    }

    /** Revoke all device trust. */
    suspend fun revokeAllDevices() {
        client.request("POST", "/api/auth/devices/revoke-all")
    }

    // ── Step-Up Auth (SmartAuth v2) ──────────────────────────

    /** Perform step-up authentication. Returns a short-lived step-up token. */
    suspend fun stepUp(method: String, credential: String): String {
        val body = encodeMap(mapOf("method" to method, "credential" to credential))
        val response = client.request("POST", "/api/auth/step-up", body)
        return client.json.decodeFromString<StepUpResponse>(response).stepUpToken
    }

    // ── Login History (SmartAuth v2) ─────────────────────────

    @Serializable
    private data class EventsResponse(val events: List<LoginEvent>)

    /** Get login events for the current user. */
    suspend fun getLoginHistory(limit: Int = 20): List<LoginEvent> {
        val response = client.request("GET", "/api/auth/login-events?limit=$limit")
        return client.json.decodeFromString<EventsResponse>(response).events
    }

    // ── TOTP Secret Retrieval (Phase 5C) ─────────────────────

    /** Get the decrypted TOTP secret for local code generation (auth app). */
    suspend fun getTotpSecret(): TotpSecret {
        val response = client.request("GET", "/api/auth/mfa/totp/secret")
        return client.json.decodeFromString<TotpSecret>(response)
    }

    // ── Push Approvals (Phase 5D) ────────────────────────────

    /** List pending push MFA approvals for the current user. */
    suspend fun getPendingApprovals(): List<PushApproval> {
        val response = client.request("GET", "/api/auth/mfa/push/pending")
        return client.json.decodeFromString<List<PushApproval>>(response)
    }

    /** Respond to a push MFA approval (approve or deny). */
    suspend fun respondToApproval(approvalId: String, action: String): PushApprovalResponse {
        val body = encodeMap(mapOf("action" to action))
        val response = client.request("POST", "/api/auth/mfa/push/$approvalId/respond", body)
        return client.json.decodeFromString<PushApprovalResponse>(response)
    }

    // ── QR Auth (Phase 5E) ───────────────────────────────────

    /** Confirm a QR login challenge from the auth app. */
    suspend fun confirmQrLogin(challengeToken: String): MessageResponse {
        val body = encodeMap(mapOf("challengeToken" to challengeToken))
        val response = client.request("POST", "/api/auth/qr/confirm", body)
        return client.json.decodeFromString<MessageResponse>(response)
    }

    // ── Session restore ─────────────────────────────────────

    /**
     * Restore session from stored tokens. Call on app launch.
     * Attempts to fetch current user; falls back to token refresh.
     * Mirrors Swift BLAuthClient.restoreSession().
     */
    suspend fun restoreSession() {
        val token = getAccessToken()
        if (token.isNullOrBlank()) {
            _state.value = AuthState.LoggedOut
            return
        }
        _state.value = AuthState.Loading
        try {
            val user = getMe()
            _state.value = AuthState.LoggedIn(user)
        } catch (_: Exception) {
            // Access token may be expired — try refresh
            val refreshed = refreshAccessToken()
            if (refreshed) {
                try {
                    val user = getMe()
                    _state.value = AuthState.LoggedIn(user)
                } catch (_: Exception) {
                    _state.value = AuthState.LoggedOut
                }
            } else {
                _state.value = AuthState.LoggedOut
            }
        }
    }

    // ── Private ──────────────────────────────────────────────

    private fun handleAuthResult(result: TokenResponse) {
        setTokens(result.accessToken, result.refreshToken)
        saveUser(result.user)
        _state.value = AuthState.LoggedIn(result.user)
    }

    /** Called by [BLPasskeyManager] after successful passkey authentication. */
    internal fun handleLoginResult(result: TokenResponse) = handleAuthResult(result)

    /** Encode a Map<String, String> to JSON string. */
    private fun encodeMap(map: Map<String, String>): String =
        client.json.encodeToString(
            MapSerializer(String.serializer(), String.serializer()),
            map,
        )
}
