"""The site's URL shapes, in one place.

Mirrored by `web/src/lib/urls.ts`: the two must agree or server canonicals
fight client links. Kept out of `seo.py` so the query layer can build a page's
path without importing the renderer (which imports the query layer).
"""

import re
import unicodedata
from urllib.parse import quote

_SLUG_RE = re.compile(r"[^a-z0-9]+")
_IATA_RE = re.compile(r"^[a-z]{3}$")


def _slug(text: str | None) -> str:
    cleaned = unicodedata.normalize("NFKD", text or "").encode("ascii", "ignore").decode()
    return _SLUG_RE.sub("-", cleaned.lower()).strip("-")


def slugify(text: str) -> str:
    """ASCII slug for URLs. Must stay trivial: the frontend mirrors it."""
    return _slug(text) or "product"


def product_path(product_id: int, name: str) -> str:
    return f"/products/{slugify(name)}-{product_id}"


def parse_product_slug(slug: str) -> int | None:
    """The trailing number is the identity; the words are for humans."""
    match = re.search(r"(\d+)$", slug)
    return int(match.group(1)) if match else None


#: The airport's common name for its address, keyed by IATA code. What people
#: search for leads ("heathrow" is overwhelmingly the query, not "lhr" or
#: "london airport"), then the code, then the city unless the name already
#: says it. JFK is the one airport known by its code. Declared data: the words
#: are for people and never come from a request string; the code stays the
#: identity. Mirrored EXACTLY by lib/urls.ts AIRPORT_SLUGS
#: (tests/test_seo_airport.py::TestAirportSlugTable reads that file). An airport
#: with no entry falls back to `<iata>-<city>` so nothing 404s.
AIRPORT_SLUGS: dict[str, str] = {
    "ATH": "athens-ath",
    "BCN": "barcelona-el-prat-bcn",
    "BOG": "bogota-el-dorado-bog",
    "CDG": "paris-cdg",
    "DUB": "dublin-dub",
    "DXB": "dubai-dxb",
    "EZE": "buenos-aires-ezeiza-eze",
    "HKG": "hong-kong-hkg",
    "ICN": "incheon-icn-seoul",
    "JFK": "jfk-new-york",
    "KEF": "keflavik-kef-reykjavik",
    "LHR": "heathrow-lhr-london",
    "MAD": "madrid-barajas-mad",
    "MEX": "mexico-city-mex",
    "PTY": "tocumen-pty-panama-city",
    "SAL": "san-salvador-sal",
    "SIN": "changi-sin-singapore",
    "SYD": "sydney-syd",
    "YUL": "montreal-trudeau-yul",
    "YYZ": "toronto-pearson-yyz",
    "ZRH": "zurich-zrh",
}
_IATA_OF_SLUG = {slug: iata for iata, slug in AIRPORT_SLUGS.items()}


def airport_path(iata: str, city: str | None, name: str | None = None) -> str:
    """`/airports/<common-name>-<iata>-<city>` from the declared table (the
    review's shape: the airport's name leads, JFK code-first), else
    `/airports/<iata>-<city>` for an airport nobody has named yet. The IATA
    code is the identity either way; the words come from the table or the
    `locations` row, never from a string a request sent."""
    code = iata.upper()
    declared = AIRPORT_SLUGS.get(code)
    if declared:
        return f"/airports/{declared}"
    words = _slug(city or name)
    return f"/airports/{code.lower()}-{words}" if words else f"/airports/{code.lower()}"


def parse_airport_slug(slug: str) -> str | None:
    """Which airport a slug means, so every shape ever linked still lands:
    (1) a declared slug, exactly; (2) a declared code leading the slug (the
    pre-review `lhr-london` shape, the bare `lhr`); (3) a declared code in any
    later segment (a stale new-shape slug, `heathrow-lhr-terminal-5`); (4) an
    undeclared airport in the old shape, its code leading. A three-letter word
    is only ever read as a code when it leads or is declared: `san-salvador-sal`
    resolves to SAL by (1), and a stale `san-salvador-sal-x` by (3), never to a
    San Diego that was never in the table. The caller 301s anything that is
    not the canonical path; an unknown airport is None."""
    lowered = slug.lower()
    declared = _IATA_OF_SLUG.get(lowered)
    if declared:
        return declared
    segments = lowered.split("-")
    head = segments[0]
    if head.upper() in AIRPORT_SLUGS:
        return head.upper()
    for segment in segments[1:]:
        if segment.upper() in AIRPORT_SLUGS:
            return segment.upper()
    return head.upper() if _IATA_RE.match(head) else None


AIRPORT_TABS = ("value", "exclusives", "all")


def airport_tab(tab: str | None, category: str | None, sort: str, page: int) -> str:
    """Which of the airport page's shelves a URL is asking for. A filter, a sort
    or a page can only mean the full list. Mirrors lib/urls.ts airportTab."""
    if tab in AIRPORT_TABS:
        return tab
    if category or (sort and sort != "featured") or page > 1:
        return "all"
    return "value"


def airport_query(category: str | None, sort: str, page: int, tab: str = "value") -> str:
    """The query string of an airport page's filtered view, defaults omitted so
    the clean page has no query at all. Mirrors lib/urls.ts airportQuery."""
    bits = []
    if tab != "value":
        bits.append("tab=" + tab)
    if category:
        bits.append("category=" + urlquote(category))
    if sort and sort != "featured":
        bits.append("sort=" + urlquote(sort))
    if page > 1:
        bits.append(f"page={page}")
    return "?" + "&".join(bits) if bits else ""


def brand_page_path(slug: str) -> str:
    """`/brands/<slug>`: the slug is the brands table's (`Brand.slug`), never a
    string folded from a name here. Mirrors lib/urls.ts brandPagePath."""
    return f"/brands/{slug}"


def brand_path(brand: str, slug: str | None = None) -> str:
    """Where a brand name links: its page when the API handed us the brand's
    slug, else a catalogue search. Mirrors lib/urls.ts brandPath."""
    if slug:
        return brand_page_path(slug)
    return "/products?q=" + urlquote(brand)


def category_path(category: str) -> str:
    """Mirrors lib/urls.ts categoryPath."""
    return "/products?category=" + urlquote(category)


def urlquote(value: str) -> str:
    # Same output as JavaScript's encodeURIComponent for the characters that
    # matter, so server links and client links are byte-identical.
    return quote(value, safe="-_.!~*'()")
