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

import androidx.annotation.StringRes
import com.runicgateway.app.R
import com.runicgateway.app.core.auth.Session
import com.runicgateway.app.data.repository.ShardFeature
import com.runicgateway.app.data.repository.ShardFeatures
import com.runicgateway.app.data.repository.canSee

/**
 * One shared, declarative, access-level navigation definition (PLAN.md §5): a
 * single list where each entry declares the minimum access it needs, filtered by
 * the current session — not a pile of `if role ==` checks. The server stays the
 * source of truth; a hidden item is a UX convenience and every gated call still
 * enforces on the backend.
 */
enum class MenuAccess {
    /** Visible to everyone, signed in or not. */
    PUBLIC,

    /** Visible to any signed-in account (§5, "My Account"). */
    SIGNED_IN,

    /**
     * The linked game-data groups (§6.3). Visible to any player **or** staff:
     * staff are a superset of players (all player abilities plus their staff
     * tools), and the backend's player self-service surface is role-agnostic, so
     * a signed-in admin/editor/moderator sees + uses their own characters too.
     */
    PLAYER,

    /** Visible to any staff role (admin/editor/moderator) — the M10 staff surface (§1). */
    STAFF,

    /** Visible to admin/moderator — moderation actions + the support queue (§1, M10). */
    MODERATOR,
}

data class MenuEntry(
    val route: String,
    @param:StringRes val labelRes: Int,
    val access: MenuAccess = MenuAccess.PUBLIC,
    /**
     * For a shard-derived surface, the visibility feature it belongs to (M11).
     *
     * Session role is not the only gate on these: an admin can switch a feature off
     * or raise its audience above the caller's rung, so the entry is filtered by
     * `GET /public/shard/features` as well as by [access]. `null` means the entry
     * isn't shard-derived and only [access] applies.
     */
    val feature: String? = null,
    /**
     * An admin's own label for this row, from the shard's `nav_public` override
     * (THEMING_AND_NAV.md §6). Null — always, as coded — means [labelRes] stands.
     *
     * A label set this way is **not localized**: it is one string for every locale,
     * which is what an admin typing a label means, and it matches the website. It
     * only ever arrives via [applyNavOverrides]; nothing in [APP_MENU] sets it.
     */
    val label: String? = null,
)

/**
 * The full menu, in display order. Public content first, then the signed-in
 * surfaces, then the player-only game-data groups (revealed once the session's
 * role is `player`, §6.3).
 */
val APP_MENU: List<MenuEntry> = listOf(
    MenuEntry(Routes.HOME, R.string.menu_home),
    MenuEntry(Routes.NEWS, R.string.menu_news),
    MenuEntry(Routes.WIKI, R.string.menu_wiki),
    MenuEntry(Routes.SHARD, R.string.menu_shard, feature = ShardFeature.STATUS),
    // Protocol 3.0 shard content (M11). Each hides when the shard doesn't publish it,
    // which for a brand-new install is every one of them until the plugin has swept.
    MenuEntry(Routes.SHARD_RULES, R.string.menu_rules, feature = ShardFeature.RULESET),
    MenuEntry(Routes.ATLAS, R.string.menu_atlas, feature = ShardFeature.ATLAS),
    MenuEntry(Routes.SHARD_LEADERBOARDS, R.string.menu_leaderboards, feature = ShardFeature.LEADERBOARDS),
    MenuEntry(Routes.SHARD_MARKET, R.string.menu_market, feature = ShardFeature.MARKET),
    MenuEntry(Routes.page("about"), R.string.menu_about),
    MenuEntry(Routes.CONTACT, R.string.menu_contact),
    MenuEntry(Routes.ACCOUNT, R.string.menu_account, MenuAccess.SIGNED_IN),
    MenuEntry(Routes.NOTIFICATIONS, R.string.menu_notifications, MenuAccess.SIGNED_IN),
    MenuEntry(Routes.PLAYER_CHARACTERS, R.string.menu_my_characters, MenuAccess.PLAYER),
    MenuEntry(Routes.PLAYER_VENDORS, R.string.menu_my_vendors, MenuAccess.PLAYER),
    MenuEntry(Routes.PLAYER_HOUSES, R.string.menu_my_houses, MenuAccess.PLAYER),
    // Staff operations (§1, M10) — revealed for staff roles; the backend re-checks every call.
    MenuEntry(Routes.ADMIN_DASHBOARD, R.string.menu_admin_dashboard, MenuAccess.STAFF),
    MenuEntry(Routes.ADMIN_CONTENT, R.string.menu_admin_content, MenuAccess.STAFF),
    MenuEntry(Routes.ADMIN_MODERATION, R.string.menu_admin_moderation, MenuAccess.MODERATOR),
    MenuEntry(Routes.ADMIN_SUPPORT, R.string.menu_admin_support, MenuAccess.MODERATOR),
)

/**
 * The entries the given [session] may see, given the shard [features] it may reach.
 * Pure + side-effect-free so the gating is unit-tested without Compose.
 *
 * Two independent filters, and both must pass:
 *
 *  - [MenuEntry.access] against the session — who the caller is.
 *  - [MenuEntry.feature] against the shard's live visibility config — what this shard
 *    publishes at all (M11). `null` [features] means the answer isn't known yet and
 *    every shard entry shows; see [canSee] for why that direction is deliberate.
 */
fun visibleEntries(
    entries: List<MenuEntry>,
    session: Session,
    features: ShardFeatures? = null,
): List<MenuEntry> = entries.filter { isEntryVisible(it, session, features) }

/**
 * [visibleEntries] for a single entry — the same two filters, and the same
 * boundary. Split out because the drawer is a tree once an admin groups rows into
 * sections (§6.3): [pruneNav] applies this predicate inside a section as well, and
 * both callers must ask exactly one question or a sectioned row could be gated by
 * a rule its top-level twin is not.
 */
fun isEntryVisible(
    entry: MenuEntry,
    session: Session,
    features: ShardFeatures? = null,
): Boolean {
    val allowedByRole = when (entry.access) {
        MenuAccess.PUBLIC -> true
        MenuAccess.SIGNED_IN -> session is Session.SignedIn
        MenuAccess.PLAYER -> session is Session.SignedIn && (session.user.isPlayer || session.user.isStaff)
        MenuAccess.STAFF -> session is Session.SignedIn && session.user.isStaff
        MenuAccess.MODERATOR -> session is Session.SignedIn && session.user.isModerator
    }
    return allowedByRole && (entry.feature == null || canSee(features, entry.feature))
}
