/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package com.runicgateway.app.core.auth

import com.runicgateway.app.data.api.dto.SafeUserDto
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton

/**
 * The single source of truth for the current session (PLAN.md §4.3). It holds the
 * in-memory token pair the network layer reads on every call, mirrors the
 * signed-in identity into an observable [state] the UI + menu react to, and keeps
 * the encrypted [TokenStore] in sync.
 *
 * Threading: [state] and the token holders are read from the UI thread and
 * written from both coroutines (login/logout) and the OkHttp
 * [com.runicgateway.app.core.net.TokenAuthenticator] dispatcher thread (silent
 * refresh), so tokens live in [AtomicReference]s and the mutators are
 * `@Synchronized` to keep the token pair and [state] consistent with each other.
 */
@Singleton
class SessionManager @Inject constructor(
    private val store: TokenStore,
) {
    private val accessRef = AtomicReference<String?>(null)
    private val refreshRef = AtomicReference<String?>(null)

    private val _state: MutableStateFlow<Session>
    val state: StateFlow<Session>

    init {
        val restored = store.load()
        if (restored != null) {
            accessRef.set(restored.accessToken)
            refreshRef.set(restored.refreshToken)
            _state = MutableStateFlow(Session.SignedIn(restored.toSessionUser()))
        } else {
            _state = MutableStateFlow(Session.SignedOut)
        }
        state = _state.asStateFlow()
    }

    /** The bearer for the current request, or null when signed out. */
    fun currentAccessToken(): String? = accessRef.get()

    /** The refresh token the authenticator rotates, or null when signed out. */
    fun currentRefreshToken(): String? = refreshRef.get()

    val isSignedIn: Boolean get() = _state.value is Session.SignedIn

    /** Establish a session from a successful login (§4.1). */
    @Synchronized
    fun onSignedIn(accessToken: String, refreshToken: String, user: SafeUserDto) {
        accessRef.set(accessToken)
        refreshRef.set(refreshToken)
        store.save(user.toStored(accessToken, refreshToken))
        _state.value = Session.SignedIn(user.toSessionUser())
    }

    /**
     * Store a rotated token pair after a silent refresh (§4.3). Keeps the current
     * user; if somehow signed out already, it is a no-op (the refresh raced a
     * logout and must not resurrect the session).
     */
    @Synchronized
    fun onRefreshed(accessToken: String, refreshToken: String, user: SafeUserDto) {
        if (_state.value !is Session.SignedIn) return
        accessRef.set(accessToken)
        refreshRef.set(refreshToken)
        store.save(user.toStored(accessToken, refreshToken))
        // Refresh may carry an updated role — reflect it so the menu stays honest.
        _state.value = Session.SignedIn(user.toSessionUser())
    }

    /** Refresh the cached identity from a `/auth/me` re-validation (§4.3). */
    @Synchronized
    fun onUserRefreshed(user: SafeUserDto) {
        val current = _state.value
        if (current !is Session.SignedIn) return
        val access = accessRef.get() ?: return
        val refresh = refreshRef.get() ?: return
        store.save(user.toStored(access, refresh))
        _state.value = Session.SignedIn(user.toSessionUser())
    }

    /** Tear the session down — user logout, dead refresh, or a server switch (§3). */
    @Synchronized
    fun onSignedOut() {
        accessRef.set(null)
        refreshRef.set(null)
        store.clear()
        _state.value = Session.SignedOut
    }
}
