package dev.ulfrx.recipe.auth

import io.ktor.client.plugins.auth.providers.BearerTokens
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

interface OidcClientGateway {
    suspend fun login(browser: AuthBrowser): OidcResult

    suspend fun refresh(authStateJson: String): OidcResult

    suspend fun logout(
        authStateJson: String,
        browser: AuthBrowser,
    )
}

interface AuthStateStore {
    fun read(): String?

    fun write(authStateJson: String)

    fun clear()
}

sealed interface AuthLoginResult {
    data object Success : AuthLoginResult

    data object Cancelled : AuthLoginResult

    data object NetworkError : AuthLoginResult

    data class Failed(
        val message: String,
    ) : AuthLoginResult
}

/**
 * Owns *just* the authentication state machine: tokens, refresh, logout.
 * User profile fetch lives in [dev.ulfrx.recipe.user.UserRepository], which
 * observes [state] and reacts to transitions.
 */
class AuthSession(
    private val oidcClient: OidcClientGateway,
    private val store: AuthStateStore,
) {
    constructor(
        oidcClient: OidcClient,
        store: SecureAuthStateStore,
    ) : this(
        oidcClient =
            object : OidcClientGateway {
                override suspend fun login(browser: AuthBrowser): OidcResult = oidcClient.login(browser)

                override suspend fun refresh(authStateJson: String): OidcResult = oidcClient.refresh(authStateJson)

                override suspend fun logout(
                    authStateJson: String,
                    browser: AuthBrowser,
                ) {
                    oidcClient.logout(authStateJson, browser)
                }
            },
        store =
            object : AuthStateStore {
                override fun read(): String? = store.read()

                override fun write(authStateJson: String) {
                    store.write(authStateJson)
                }

                override fun clear() {
                    store.clear()
                }
            },
    )

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

    private var currentTokens: BearerTokens? = null

    suspend fun initialize() {
        _state.value = AuthState.Loading

        val storedJson = store.read()
        if (storedJson.isNullOrBlank()) {
            clearSession()
            return
        }

        when (val refreshResult = oidcClient.refresh(storedJson)) {
            is OidcResult.Success -> persistAndAuthenticate(refreshResult)

            OidcResult.Cancelled,
            OidcResult.NetworkError,
            is OidcResult.AuthError,
            -> clearSession()
        }
    }

    suspend fun login(browser: AuthBrowser): AuthLoginResult =
        when (val loginResult = oidcClient.login(browser)) {
            is OidcResult.Success -> {
                persistAndAuthenticate(loginResult)
                AuthLoginResult.Success
            }

            OidcResult.Cancelled -> {
                _state.value = AuthState.Unauthenticated
                AuthLoginResult.Cancelled
            }

            OidcResult.NetworkError -> {
                _state.value = AuthState.Unauthenticated
                AuthLoginResult.NetworkError
            }

            is OidcResult.AuthError -> {
                _state.value = AuthState.Unauthenticated
                AuthLoginResult.Failed(loginResult.message)
            }
        }

    suspend fun logout(browser: AuthBrowser) {
        val storedJson = store.read()
        if (!storedJson.isNullOrBlank()) {
            runCatching {
                oidcClient.logout(storedJson, browser)
            }
        }

        clearSession()
    }

    suspend fun getAccessToken(): String? = refreshBearerTokens()?.accessToken

    fun currentBearerTokens(): BearerTokens? = currentTokens

    suspend fun refreshBearerTokens(): BearerTokens? {
        val storedJson =
            store.read() ?: return null.also {
                clearSession()
            }

        return when (val refreshResult = oidcClient.refresh(storedJson)) {
            is OidcResult.Success -> {
                persistTokens(refreshResult)
                currentTokens
            }

            OidcResult.Cancelled,
            OidcResult.NetworkError,
            is OidcResult.AuthError,
            -> {
                null.also {
                    clearSession()
                }
            }
        }
    }

    private fun persistAndAuthenticate(result: OidcResult.Success) {
        persistTokens(result)
        _state.value = AuthState.Authenticated
    }

    private fun persistTokens(result: OidcResult.Success) {
        store.write(result.authStateJson)
        currentTokens =
            BearerTokens(
                accessToken = result.accessToken,
                refreshToken = result.authStateJson,
            )
    }

    private fun clearSession() {
        currentTokens = null
        store.clear()
        _state.value = AuthState.Unauthenticated
    }
}
