"""Places: where a shop is, of any kind, and the comparison unit defined once (Stream K2; plan
W19). A kind is a registry entry here, never a migration: an airport, a mall, a port, a ship
that moves (its port today is an attribute, not a parent), a border crossing, in-flight (a
kind with no pages), the online catalogue (no primary place). The unit every count uses is
"shops counted once per primary place": a shop with a primary place counts as that place, a
shop without one counts as its airport code, a shop with neither counts as itself, so the
counts read the same before `backfill places` has run (the test kit) and after it (every host).

Sources of truth: this module, `models/places.py`, `catalog_queries.py` (every count of shops
goes through `unit_count()` / `unit_join()`; `tests/test_places.py` greps for a count that does
not), `cli.backfill_places`. What it cost before: three spellings of the unit (`distinct
shop_id`, `distinct iata`, `distinct Shop.id`) that agreed only because every shop was one airport.
"""

from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass

from sqlalchemy import String, and_, cast, distinct, func, literal, select
from sqlalchemy.orm import Session

from app.models import Shop
from app.models.places import Place, ShopPlace


@dataclass(frozen=True)
class PlaceKind:
    name: str
    label: str
    comparable: bool = True      # a page compares within it and it counts as one unit across places
    pages: bool = True           # its pages exist (in-flight has none)
    address_prefix: str = "/places/"
    moves: bool = False          # a ship: its port today is an attribute, never a parent
    primary: bool = True         # a shop of this kind needs a primary place (the online catalogue does not)
    #: What one part of this place is called, where a shopper is and cannot easily leave. The guide
    #: document calls them areas so a port's piers and a mall's floors need no second field
    #: (`models/place_guide.py`); this is the word the page prints.
    area_label: str = "Area"


KINDS: dict[str, PlaceKind] = {
    "airport": PlaceKind("airport", "Airport", address_prefix="/airports/", area_label="Terminal"),
    "mall": PlaceKind("mall", "Mall", area_label="Floor"),
    "port": PlaceKind("port", "Port", area_label="Pier"),
    "ship": PlaceKind("ship", "Ship", moves=True, area_label="Deck"),
    "border": PlaceKind("border", "Border crossing"),
    "inflight": PlaceKind("inflight", "In-flight", pages=False, comparable=False),
    "online": PlaceKind("online", "Online catalogue", pages=False, comparable=False, primary=False),
}


def area_label_for(db: Session, iata: str | None) -> str:
    """What one part of the place with this airport code is called ("Terminal"). Falls back to the
    airport kind's word, so an airport with no place row yet still prints the right noun."""
    from app.services import place_guides

    place = place_guides.place_by_identifier(db, "iata", iata) if iata else None
    kind = KINDS.get(place.kind) if place else None
    return (kind or KINDS["airport"]).area_label


def primary_on():
    """The join condition of a shop to its primary place."""
    return and_(ShopPlace.shop_id == Shop.id, ShopPlace.role == "primary")


def unit():
    """The SQL comparison unit of the current `Shop` row: its primary place, else its airport
    code, else itself. Requires `unit_join` (the outer join to `shop_places`) on the query."""
    return func.coalesce(
        literal("place:") + cast(ShopPlace.place_id, String),
        literal("iata:") + Shop.iata,
        literal("shop:") + cast(Shop.id, String),
    )


def unit_count():
    """`COUNT(DISTINCT <unit>)`: the one spelling of "how many places carry it"."""
    return func.count(distinct(unit()))


def unit_join(query):
    """The outer join that makes `unit()` readable: at most one primary place per shop (the
    unique partial index), so it never multiplies rows."""
    return query.outerjoin(ShopPlace, primary_on())


def primary_of(db: Session) -> dict[int, int]:
    """`{shop_id: place_id}` for every shop with a primary place, one query."""
    return {s: p for s, p in db.execute(select(ShopPlace.shop_id, ShopPlace.place_id).where(ShopPlace.role == "primary"))}


def count_units(shop_ids: Iterable[int], primary: dict[int, int], iata_of: dict[int, str | None] | None = None) -> int:
    """The Python form of `unit_count()` over given shop ids, the same three-way fallback."""
    units = set()
    for shop_id in shop_ids:
        if shop_id in primary:
            units.add(("place", primary[shop_id]))
        elif iata_of and iata_of.get(shop_id):
            units.add(("iata", iata_of[shop_id]))
        else:
            units.add(("shop", shop_id))
    return len(units)


def place_of(db: Session, shop: Shop) -> Place | None:
    row = db.scalar(select(ShopPlace).where(ShopPlace.shop_id == shop.id, ShopPlace.role == "primary"))
    return db.get(Place, row.place_id) if row is not None else None
