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

import com.runicgateway.app.core.result.ApiResult
import com.runicgateway.app.core.result.safeApiCall
import com.runicgateway.app.data.api.PublicApi
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 javax.inject.Inject
import javax.inject.Singleton

/**
 * Which shard surfaces the current viewer may reach, from
 * `GET /public/shard/features` (PLAN.md §5, §9 M11).
 *
 * Every shard-derived feature is admin-configurable — it can be switched off, or its
 * audience raised above the caller's rung — so shard navigation can no longer be a
 * static list gated on the session role alone. [level] is the server's own answer on
 * the `anonymous → logged_in → player → staff → admin` ladder; the app does not
 * re-derive it.
 *
 * **This is presentation only.** The gate is server-side: a disabled feature `404`s
 * and an out-of-rung one `403`s whether or not the entry was rendered. That is why an
 * unknown answer deliberately **fails open** — see [ShardFeatures] and [canSee].
 */
@Singleton
class ShardFeaturesRepository @Inject constructor(
    private val api: PublicApi,
) {
    private val _features = MutableStateFlow<ShardFeatures?>(null)

    /** The current answer, or `null` while it is unknown (in flight, or the lookup failed). */
    val features: StateFlow<ShardFeatures?> = _features.asStateFlow()

    // Serializes concurrent refreshes: the shell refreshes on every session change,
    // and two overlapping loads would race to publish.
    private val mutex = Mutex()

    /**
     * Re-resolve the visible set. Called on every session change (sign-in, sign-out,
     * a role revalidation that actually changed the user), because the answer is
     * per-viewer.
     *
     * A failed lookup clears the cache rather than keeping a stale one: falling back
     * to "show everything" is the safe direction here, since the server still gates
     * every call.
     */
    suspend fun refresh() = mutex.withLock {
        _features.value = when (val result = safeApiCall { api.getShardFeatures() }) {
            is ApiResult.Ok -> ShardFeatures(
                level = result.data.level,
                visible = result.data.features.toSet(),
            )
            // Includes the 404 an older, pre-Protocol-3.0 website returns for this
            // route — that site has no visibility framework, so "unknown" is exactly
            // the right answer and the menu behaves as it did before M11.
            else -> null
        }
    }

    /**
     * Drop the cached answer. Called on a Settings → Server switch: the features
     * belong to the host that reported them, and a switch between two signed-out
     * hosts changes no session, so nothing else would invalidate them.
     */
    fun invalidate() {
        _features.value = null
    }
}

/**
 * The resolved visibility answer for one viewer: the rung the server placed them on
 * and the shard features they may reach.
 */
data class ShardFeatures(
    val level: String?,
    val visible: Set<String>,
)

/**
 * True when [feature] may be shown — **or when the answer isn't known yet**.
 *
 * The fail-open default is deliberate and matches the web client (`lib/useShardFeatures.js`):
 * the server gates every call regardless, so the cost of guessing wrong is a link that
 * briefly `403`s, while the cost of guessing the other way is a navigation drawer that
 * flickers its entries in on every cold start.
 */
fun canSee(features: ShardFeatures?, feature: String): Boolean =
    features == null || feature in features.visible

/** Feature names as the website's `shardVisibility.js` `FEATURES` map spells them. */
object ShardFeature {
    const val STATUS = "status"
    const val ACTIVITY = "activity"
    const val CHAMPS = "champs"
    const val GUILDS = "guilds"
    const val GOVERNORS = "governors"
    const val HOUSES = "houses"
    const val PRESENCE = "presence"

    // Added by Protocol 3.0.
    const val RULESET = "ruleset"
    const val ATLAS = "atlas"
    const val LEADERBOARDS = "leaderboards"
    const val MARKET = "market"
}
