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

import com.runicgateway.app.data.repository.ContentRepository.PostCategory

/**
 * The website path → app route table (THEMING_AND_NAV.md §6.2).
 *
 * The public nav an admin edits is keyed by **website** paths, so honoring it in
 * the app needs a translation. This is the one new piece of cross-repo coupling
 * the milestone introduces, which is why it lives in a single file with the
 * website's own array quoted right beside it — the coupling is visible and
 * reviewable in one place rather than spread across the drawer's call sites.
 *
 * Verbatim from `website/client/src/components/SiteHeader.jsx`, which is the
 * exported owner of the list (`export const NAV`, and Admin → Navigation edits
 * exactly it):
 *
 * ```js
 * export const NAV = [
 *   { label: 'Home', to: '/', end: true },
 *   { label: 'News', to: '/site/news' },
 *   { label: 'Screenshots', to: '/site/screenshots' },
 *   { label: 'Five on Friday', to: '/site/five-on-friday' },
 *   { label: 'Newsletter', to: '/site/newsletter' },
 *   { label: 'Wiki', to: '/wiki' },
 *   { label: 'Shard', to: '/site/shard', feature: 'status' },
 *   { label: 'Champions', to: '/site/champs', feature: 'champs' },
 *   { label: 'Guilds', to: '/site/guilds', feature: 'guilds' },
 *   { label: 'Governors', to: '/site/governors', feature: 'governors' },
 *   { label: 'Houses', to: '/site/houses', feature: 'houses' },
 *   { label: 'Rules', to: '/site/rules', feature: 'ruleset' },
 *   { label: 'Atlas', to: '/site/atlas', feature: 'atlas' },
 *   { label: 'Leaderboards', to: '/site/leaderboards', feature: 'leaderboards' },
 *   { label: 'Market', to: '/site/market', feature: 'market' },
 *   { label: 'About', to: '/site/about' },
 * ]
 * ```
 *
 * The `feature` values are **not** mirrored here on purpose. [APP_MENU] is the
 * app's own source of truth for gating, and a second copy of a security-relevant
 * value that drifts silently is worth more than it costs. This table carries the
 * mapping and nothing else.
 *
 * Not every row maps to something the app shows in its drawer, and that is the
 * design rather than an omission — see [WEB_PATH_TO_ROUTE].
 */

/** One row of the website's public nav: its path, and the app route it opens. */
data class WebNavPath(val path: String, val route: String)

/**
 * The website's public nav in **its** order, mapped to app routes.
 *
 * The order is load-bearing, not decorative: a stored `order` is an index into
 * *this* list (the admin's editor writes the position a row holds on the web), so
 * a row the admin never moved has to take its key from the same number line or
 * explicit and implicit keys would be incomparable. See `NavOverrides.kt`.
 */
val WEBSITE_PUBLIC_NAV: List<WebNavPath> = listOf(
    WebNavPath("/", Routes.HOME),
    WebNavPath("/site/news", Routes.NEWS),
    // The app's News screen carries all four categories as tabs, so these three
    // have a route but no drawer row of their own — see the note below.
    WebNavPath("/site/screenshots", Routes.news(PostCategory.SCREENSHOTS)),
    WebNavPath("/site/five-on-friday", Routes.news(PostCategory.FIVE_ON_FRIDAY)),
    WebNavPath("/site/newsletter", Routes.news(PostCategory.NEWSLETTER)),
    WebNavPath("/wiki", Routes.WIKI),
    WebNavPath("/site/shard", Routes.SHARD),
    // Behind the Shard hub in the app, deliberately — no drawer row either.
    WebNavPath("/site/champs", Routes.SHARD_CHAMPS),
    WebNavPath("/site/guilds", Routes.SHARD_GUILDS),
    WebNavPath("/site/governors", Routes.SHARD_GOVERNORS),
    WebNavPath("/site/houses", Routes.SHARD_HOUSES),
    WebNavPath("/site/rules", Routes.SHARD_RULES),
    WebNavPath("/site/atlas", Routes.ATLAS),
    WebNavPath("/site/leaderboards", Routes.SHARD_LEADERBOARDS),
    WebNavPath("/site/market", Routes.SHARD_MARKET),
    WebNavPath("/site/about", Routes.page("about")),
)

/**
 * The same table as a lookup.
 *
 * **A mapped route is not the same thing as a drawer row.** Seven of these paths
 * resolve to a screen the app reaches some other way: the three news categories
 * are tabs on one News screen, and champs / guilds / governors / houses sit behind
 * the Shard hub because that is the better shape on a phone. An override for one
 * of them is **ignored** — §6.1's rule is that a nav override may never introduce
 * navigation, and the hub is a design decision, not an accident to correct. The
 * merge enforces that by intersecting with [APP_MENU]; nothing here needs to know
 * which rows those are.
 *
 * The mapping still exists for all sixteen because phase 6's added links resolve
 * an admin-authored path against the same table, and *there* a category tab or a
 * hub board is a perfectly good destination — the admin asked for it by path.
 */
val WEB_PATH_TO_ROUTE: Map<String, String> =
    WEBSITE_PUBLIC_NAV.associate { it.path to it.route }

/**
 * Each app route's index in the website's own nav order — the sort key a row the
 * admin never moved takes, so it lands on the same number line as a stored
 * `order`. All sixteen routes are distinct, so this loses nothing.
 */
internal val WEB_ROUTE_ORDER: Map<String, Int> =
    WEBSITE_PUBLIC_NAV.withIndex().associate { (index, row) -> row.route to index }

/**
 * The app route a website nav path opens, or null when the app has no screen for
 * it. A trailing slash is tolerated (`/wiki/` is `/wiki`) since a hand-edited
 * settings row may carry one; the root path is left alone.
 */
fun appRouteForWebPath(path: String?): String? = WEB_PATH_TO_ROUTE[normalizeWebPath(path)]

/** `/wiki/` → `/wiki`, blank → null, and `/` left alone. */
private fun normalizeWebPath(path: String?): String? {
    val trimmed = path?.trim().orEmpty()
    if (trimmed.isEmpty()) return null
    val normalized = if (trimmed.length > 1) trimmed.trimEnd('/') else trimmed
    return normalized.ifEmpty { "/" }
}

/**
 * The website's top-level paths that are **not** CMS pages.
 *
 * The site serves its CMS pages from a top-level `/<slug>` (React Router ranks its
 * static routes above that dynamic one), which is what lets [resolveWebPath]'s
 * last rule open an admin-authored page natively. These are the segments that rule
 * must not swallow: the SPA's own sections, and the two server mounts. A link to
 * one of them hands off to the browser, which is where they actually live.
 */
private val RESERVED_TOP_LEVEL = setOf(
    "admin", "account", "player", "site", "wiki", "invite", "preview", "api", "uploads",
)

/**
 * The app route an **arbitrary** website path opens, or null when the app has no
 * screen for it and the link must hand off to a Custom Tab (§6.3).
 *
 * [appRouteForWebPath] answers for the sixteen paths the *nav* is built from; this
 * answers for a path an admin typed into an added link, which may name any page on
 * the site. It is the app's read of the site's own route table, and like the table
 * above it is cross-repo coupling kept in one file — quoted here for the same
 * reason, from `website/client/src/App.jsx`:
 *
 * ```jsx
 * <Route path="/" element={<Portal />} />
 * <Route path="/site/news" element={<News />} />
 * <Route path="/site/screenshots" element={<Screenshots />} />
 * <Route path="/site/five-on-friday" element={<FiveOnFriday />} />
 * <Route path="/site/newsletter" element={<Newsletter />} />
 * <Route path="/site/newsletter/:id" element={<NewsletterIssue />} />
 * <Route path="/site/about" element={<About />} />
 * <Route path="/site/status" element={<Status />} />
 * <Route path="/site/shard" element={<Shard />} />
 * <Route path="/site/shard/activity" element={<ShardActivity />} />
 * ... /site/champs, /guilds, /governors, /houses, /rules, /leaderboards, /market
 * <Route path="/site/atlas" element={<Atlas />} />
 * <Route path="/site/atlas/:slug" element={<AtlasCreature />} />
 * <Route path="/site/market/vendors/:serial" element={<MarketVendor />} />
 * <Route path="/wiki" element={<Wiki />} />
 * <Route path="/wiki/:slug" element={<WikiArticle />} />
 * // CMS pages: top-level /:slug, matched only after the named routes above
 * <Route path="/:slug" element={<CmsPage />} />
 * ```
 *
 * Note what is *not* in it: no `/site/news/<id>` (a news item renders on its
 * category page; the newsletter's is the site's one post-detail route), no
 * `/page/<slug>`, and no `/contact` — the app's contact form is app-only (§6.2).
 *
 * ```
 * /                               → HOME
 * /site/news                      → NEWS
 * /site/{screenshots,five-on-friday,newsletter}
 *                                 → NEWS, that category's tab
 * /site/newsletter/<id>           → POST         (the site's one post-detail route)
 * /wiki                           → WIKI
 * /wiki/<slug>                    → WIKI_PAGE
 * /site/<shard surface>           → the mapped shard route (§6.2)
 * /site/atlas/<slug>              → ATLAS_CREATURE
 * /site/market/vendors/<serial>   → SHARD_MARKET_VENDOR
 * /site/about                     → PAGE("about")
 * /<slug>                         → PAGE(slug), unless <slug> is reserved
 * anything else                   → null, i.e. the Custom Tab
 * ```
 *
 * **A path carrying a query or a fragment hands off**, whatever its route part
 * says. No app route takes either, so a native match would quietly drop what the
 * admin wrote; the browser honors it exactly.
 *
 * Resolving a path is not the same as being allowed to see the screen behind it.
 * A link to `/site/market` on a shard that does not publish the market lands on
 * the Market screen's honest "not published here" state, which is what typing the
 * URL on the web does too (§6.3).
 */
fun resolveWebPath(path: String?): String? {
    val normalized = normalizeWebPath(path) ?: return null
    if (normalized.any { it == '?' || it == '#' }) return null
    WEB_PATH_TO_ROUTE[normalized]?.let { return it }
    if (!normalized.startsWith("/")) return null

    // Blank segments ("/site//news") mean a malformed path, not a slug.
    val segments = normalized.removePrefix("/").split('/')
    if (segments.any { it.isBlank() }) return null

    return when {
        segments.size == 1 -> segments[0].takeIf { it !in RESERVED_TOP_LEVEL }?.let(Routes::page)
        segments[0] == "wiki" && segments.size == 2 -> Routes.wikiPage(segments[1])
        segments[0] != "site" -> null
        segments.size == 3 && segments[1] == "newsletter" ->
            Routes.post(PostCategory.NEWSLETTER.urlSlug, segments[2])
        segments.size == 3 && segments[1] == "atlas" -> Routes.atlasCreature(segments[2])
        segments.size == 4 && segments[1] == "market" && segments[2] == "vendors" ->
            Routes.marketVendor(segments[3])
        else -> null
    }
}
