package com.fabledsword.minstrel.auth

import com.fabledsword.minstrel.api.endpoints.AuthApi
import com.fabledsword.minstrel.api.endpoints.MeApi
import com.fabledsword.minstrel.di.ApplicationScope
import com.fabledsword.minstrel.models.UserRef
import com.fabledsword.minstrel.models.wire.LoginRequestBody
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Singleton facade over the auth state machine. Mirrors Flutter's
 * `AuthController` from `auth_provider.dart`.
 *
 * Cookie persistence is handled by [AuthCookieInterceptor] capturing
 * Set-Cookie on the login response; the user identity itself
 * persists via AuthStore.userJson so the username + isAdmin survive
 * cold restarts. On construction the controller subscribes to
 * AuthStore.userJson and reflects any persisted value into
 * `currentUser` — that's what makes the Settings screen show the
 * username after a fresh app launch without re-prompting.
 */
@Singleton
class AuthController @Inject constructor(
    private val authStore: AuthStore,
    private val json: Json,
    @ApplicationScope private val scope: CoroutineScope,
    retrofit: Retrofit,
) {
    private val api: AuthApi = retrofit.create()
    private val meApi: MeApi = retrofit.create()

    private val currentUserState = MutableStateFlow<UserRef?>(null)
    val currentUser: StateFlow<UserRef?> = currentUserState.asStateFlow()

    val sessionCookie: StateFlow<String?> get() = authStore.sessionCookie

    val isSignedIn: Boolean
        get() = !authStore.sessionCookie.value.isNullOrEmpty()

    init {
        // Rehydrate currentUser whenever the persisted userJson emits
        // (cold start with a saved cookie OR explicit sign-in/out
        // writing through AuthStore). Decode failures collapse to null
        // so a malformed row can't crash the app — worst case is a
        // blank username until the next sign-in writes a valid value.
        scope.launch {
            authStore.userJson.collect { raw ->
                currentUserState.value = raw?.let { decodeUser(it) }
            }
        }
        // Refresh from /api/me on startup so a remotely-changed account
        // flag (e.g. admin enabling debug mode, M9) reaches the device
        // without a re-login. Best-effort; failures keep the cached value.
        scope.launch { refreshProfile() }
    }

    /**
     * Submit credentials. On success the AuthCookieInterceptor will
     * have captured the Set-Cookie before this method returns; the
     * caller can safely navigate to the in-shell graph. Throws on
     * any non-2xx response (caller surfaces in UI).
     */
    suspend fun signIn(username: String, password: String): UserRef {
        val response = api.login(LoginRequestBody(username = username, password = password))
        val user = UserRef(
            id = response.user.id,
            username = response.user.username,
            isAdmin = response.user.isAdmin,
        )
        currentUserState.value = user
        authStore.setUserJson(json.encodeToString(UserRef.serializer(), user))
        // Pull the fuller /me shape (carries debug_mode_enabled) right
        // after login so the diagnostics gate is correct without waiting
        // for the next startup refresh.
        scope.launch { refreshProfile() }
        return user
    }

    /**
     * Re-fetch the caller's profile from /api/me and update currentUser
     * + persisted userJson. Carries account-level flags (debug mode)
     * that aren't in the login response. Best-effort: a network failure
     * leaves the cached identity untouched. No-op when signed out.
     */
    suspend fun refreshProfile() {
        if (!isSignedIn) return
        val p = runCatching { meApi.getProfile() }.getOrNull() ?: return
        val user = UserRef(
            id = p.id,
            username = p.username,
            isAdmin = p.isAdmin,
            debugModeEnabled = p.debugModeEnabled,
        )
        currentUserState.value = user
        authStore.setUserJson(json.encodeToString(UserRef.serializer(), user))
    }

    /**
     * Clears the local session immediately and best-effort hits
     * `/api/auth/logout` so the server can drop its session row.
     * Network errors are swallowed — the user is already locally
     * signed out and a stale server session times out on its own.
     */
    suspend fun signOut() {
        currentUserState.value = null
        authStore.setSessionCookie(null)
        authStore.setUserJson(null)
        runCatching { api.logout() }
    }

    private fun decodeUser(raw: String): UserRef? =
        runCatching { json.decodeFromString(UserRef.serializer(), raw) }.getOrNull()
}
