"""API response shapes for the hub pages (airports now; brands and categories follow).

Kept apart from `schemas.py` so the hub work never edits the catalogue's shapes
while another stream owns them. The frontend's TypeScript client is generated
from these like every other response model.
"""

from datetime import datetime

from pydantic import BaseModel

from app.models.editorial import ArticleOut
from app.models.schemas import CategoryCount, ProductSummary


class AirportShop(BaseModel):
    """One retailer's storefront at the airport (an airport can hold several)."""

    code: str
    retailer_name: str
    product_variants: int = 0
    last_collected_at: datetime | None = None


class AirportTerminal(BaseModel):
    """One terminal's duty free, written: where the main store is, and the
    specialty boutiques around it. `specialty` is absent where a terminal has
    none worth naming, not where nobody has looked yet."""

    name: str
    airlines: str | None = None
    duty_free: str
    specialty: str | None = None


class AirportService(BaseModel):
    """Something the shops there offer that changes how you shop: reserve and
    collect, a pre-order desk, collection on arrival."""

    title: str
    body: str


class HoursProvenance(BaseModel):
    """Where an airport's hours came from (`services/hours/store.py`): collected from the
    operator's site on a date, or entered by a named account on a date. The public page prints
    the date beside the hours and nothing more; the owner's views print the whole line."""

    kind: str  # collected | hand
    observed_at: datetime
    entered_by_username: str | None = None
    source_url: str | None = None
    #: The host the hours were read from ("www.heathrow.com"), for the owner's line.
    source_host: str | None = None


class AirportHoursOut(BaseModel):
    """`GET /api/airports/{iata}/hours`: the current line and its provenance, or kind "none"
    and nothing else where neither a collected nor a hand row exists."""

    kind: str  # collected | hand | none
    text: str | None = None
    observed_at: datetime | None = None
    entered_by_username: str | None = None
    source_url: str | None = None


class AirportGuide(BaseModel):
    """The written guide to an airport's duty free (`services.airport_guides`):
    who operates it, where the shops are by terminal, and what else is worth
    knowing before the gate. Every field is optional but the terminals, because
    a half-written guide is still worth showing; what is missing shows in the
    demo's placeholder view."""

    operator: str | None = None
    overview: str | None = None
    #: One line on where the shops sit relative to security.
    access: str | None = None
    terminals: list[AirportTerminal] = []
    #: Specialty shops for an airport whose guide does not split by terminal.
    specialty: str | None = None
    services: list[AirportService] = []
    #: The airport's own terminal map, where it marks the shops.
    map_url: str | None = None
    map_label: str | None = None
    #: The one line under "Opening hours": collected from the airport operator's site where
    #: its robots allow, entered by hand where they do not (`services/hours/`); never typed here.
    hours: str | None = None
    #: Which of the two it was, and when; absent exactly when `hours` is.
    hours_provenance: HoursProvenance | None = None
    #: When a person last checked these shops, shown with the guide, because
    #: retailers move between gates.
    checked: str | None = None


class FeaturedFamily(BaseModel):
    """One row of "Featured at this airport": a family and the best-value comparables in it
    here (`services/airport_featured.py`). A family with nothing comparable has no row."""

    key: str
    label: str
    items: list[ProductSummary] = []


class CategoryPageLink(BaseModel):
    """One category-at-airport page that exists: the pair is at or over the coverage bar."""

    category: str
    slug: str
    path: str
    count: int = 0


class AirportSummary(BaseModel):
    """An airport with a page: the identity, the words in its URL, and its size."""

    iata: str
    path: str
    name: str
    city: str | None = None
    country: str | None = None
    currency: str
    product_variants: int = 0
    last_collected_at: datetime | None = None


class AirportDetail(AirportSummary):
    """Everything the airport page shows. Every number here is counted in the
    database; the page never types one."""

    # Plain `= []` defaults on purpose: they reach the OpenAPI schema as
    # defaults, so the generated TypeScript types are required, not optional.
    shops: list[AirportShop] = []
    #: Products here that are also priced at another airport we track.
    comparable: int = 0
    #: Comparable products for which this airport is the cheapest place we know.
    cheapest_here: int = 0
    exclusives: int = 0
    categories: list[CategoryCount] = []
    #: The biggest savings for a shopper standing at this airport, best first.
    savings: list[ProductSummary] = []
    #: A sample of the travel-retail exclusives sold here.
    exclusive_items: list[ProductSummary] = []
    #: The best-value comparables here, one row per family that has any; never padded.
    featured: list[FeaturedFamily] = []
    #: The category-at-airport pages this airport has (the pairs over the coverage bar).
    category_pages: list[CategoryPageLink] = []
    # The product list, one page of it, with the filters that shaped it.
    category: str | None = None
    #: The storefront's shopping-feature filters on the full list (`/api/products` names).
    multi_only: bool = False
    awarded_only: bool = False
    exclusives_only: bool = False
    sort: str = "featured"
    total: int = 0
    limit: int = 24
    offset: int = 0
    items: list[ProductSummary] = []
    #: The Professor's published write-up for this airport, when there is one
    #: (`editorial.airport_writeup`); seeded with the page so the SPA draws it
    #: without a second request and the server body shows the same text.
    writeup: ArticleOut | None = None
    #: The written guide to the airport's duty free, where one exists
    #: (`services.airport_guides`); seeded with the page like the write-up.
    guide: AirportGuide | None = None


class AirportCategoryDetail(AirportSummary):
    """Everything a category-at-airport page shows (`/airports/<airport>/<category>`): the
    airport's identity, then the category's figures at it, every one counted in the database.
    `product_variants` is the count in the category here; `total` the list's under its filters."""

    shops: list[AirportShop] = []
    category: str
    category_slug: str
    #: The airport's own page, for the crumb and the way back.
    airport_path: str
    #: Products of this category here that are also priced at another airport we track.
    comparable: int = 0
    #: Of those, the ones this airport is the cheapest place we know for.
    cheapest_here: int = 0
    exclusives: int = 0
    exclusive_items: list[ProductSummary] = []
    #: The other category pages this airport has, biggest first.
    siblings: list[CategoryPageLink] = []
    multi_only: bool = False
    awarded_only: bool = False
    exclusives_only: bool = False
    sort: str = "featured"
    total: int = 0
    limit: int = 24
    offset: int = 0
    items: list[ProductSummary] = []


class BrandAirport(BaseModel):
    """One airport that stocks the brand: how much of it, and how often it is
    the cheapest of our airports for a bottle sold at two or more."""

    iata: str
    path: str
    name: str
    product_variants: int = 0
    cheapest_for: int = 0
    last_collected_at: datetime | None = None


class BrandSummary(BaseModel):
    """A brand with a page: its slug (the brands table's, never a folded
    string), the spelling shown, and its size."""

    slug: str
    path: str
    name: str
    product_variants: int = 0
    last_collected_at: datetime | None = None
    #: The family most of its products sit in (taxonomy.majority_family: liquor,
    #: beauty, ...), so the header's department panels list their own brands.
    family: str | None = None


class BrandDetail(BrandSummary):
    """Everything the brand page shows; every number counted in the database."""

    airports: list[BrandAirport] = []
    #: Products of the brand priced at two or more of our airports.
    comparable: int = 0
    exclusives: int = 0
    categories: list[CategoryCount] = []
    # The product list, one page of it, with the filters that shaped it.
    category: str | None = None
    sort: str = "featured"
    total: int = 0
    limit: int = 24
    offset: int = 0
    items: list[ProductSummary] = []


class DatasetFacts(BaseModel):
    """What the public data page and the Dataset markup state, every figure
    counted over visible airports at request time."""

    observations: int = 0
    product_variants: int = 0
    airports: int = 0
    retailers: int = 0
    currencies: int = 0
    with_barcode: int = 0
    awards: int = 0
    first_observed_at: datetime | None = None
    last_observed_at: datetime | None = None
    airport_list: list[AirportSummary] = []
    categories: list[CategoryCount] = []
