"""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)


KINDS: dict[str, PlaceKind] = {
    "airport": PlaceKind("airport", "Airport", address_prefix="/airports/"),
    "mall": PlaceKind("mall", "Mall"),
    "port": PlaceKind("port", "Port"),
    "ship": PlaceKind("ship", "Ship", moves=True),
    "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 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
