"""Every route, mount and SPA path the app declares, walked once, for whoever asks.

Sources of truth: `app/main.py` (the mounted app), `web/src/App.tsx` (the SPA route
table), `app/services/access.py` (the classes a walk is read against),
`tests/test_route_inventory.py` (the walk read as a gate), `scripts/route-sweep.py`
(the walk read over HTTP).

The walk lived inside `test_route_inventory.py` until the unauthenticated sweep needed
it too, and a script that imports a test file is a second home waiting to drift. One
home: the test and the script both import from here, so a route the walker stops seeing
fails the inventory rather than quietly dropping out of the sweep as well.

Nothing here classifies or decides; `access.classify` and `access.decide` do that. This
module only enumerates, and it touches no database and no network.
"""

from __future__ import annotations

import pathlib
import re

from fastapi.routing import APIRoute
from starlette.routing import Mount

MAIN = pathlib.Path(__file__).resolve().parents[2]

#: A path parameter stands in for a real one so a route can be fetched: a slug is `x`, an
#: airport is the one IATA code every environment holds, an id is `1`. The sweep only ever
#: reads, so a sample that matches nothing answers as a miss, which is still an answer the
#: gate has to give before the handler sees it.
IATA_SAMPLE = "LHR"
SLUG_SAMPLE = "x"
ID_SAMPLE = "1"

_BRACED = re.compile(r"\{([A-Za-z_][A-Za-z0-9_]*)(?::[^}]+)?\}")
_COLON = re.compile(r":([A-Za-z_][A-Za-z0-9_]*)")


def walk(routes):
    """Every `APIRoute` and `Mount` under a router tree, including included routers."""
    for route in routes:
        if isinstance(route, APIRoute | Mount):
            yield route
        elif hasattr(route, "routes"):
            yield from walk(route.routes)
        elif hasattr(route, "original_router"):
            yield from walk(route.original_router.routes)


def every_key(app) -> set[str]:
    """The route keys `access.classify` takes: `"<METHOD> <path>"`, or `"MOUNT <path>"`."""
    keys = set()
    for route in walk(app.routes):
        if isinstance(route, Mount):
            keys.add(f"MOUNT {route.path}")
        else:
            for method in route.methods - {"HEAD", "OPTIONS"}:
                keys.add(f"{method} {route.path}")
    return keys


def spa_paths() -> set[str]:
    """Every `<Route path>` in `App.tsx`, the catch-all excepted."""
    src = (MAIN / "web" / "src" / "App.tsx").read_text()
    found = set(re.findall(r'<Route\s+path="([^"]+)"', src)) | set(re.findall(r'path="([^"]+)"\s*\n', src))
    return {p for p in found if p != "*"}


def sample_value(name: str) -> str:
    """The stand-in for one path parameter, by its name."""
    lowered = name.lower()
    if lowered == "iata":
        return IATA_SAMPLE
    if lowered == "id" or lowered.endswith("_id") or lowered.endswith("id"):
        return ID_SAMPLE
    return SLUG_SAMPLE


def sample_path(template: str) -> str:
    """A fetchable path from a route template (`{slug}`) or an SPA template (`:slug`)."""
    filled = _BRACED.sub(lambda m: sample_value(m.group(1)), template)
    return _COLON.sub(lambda m: sample_value(m.group(1)), filled)
