"""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.place_guide import PlaceGuide
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 AirportHoursOut(BaseModel):
    """An airport's current opening hours and where they came from (`services/hours/store.py`):
    collected from the operator's site on a date, or entered by a named account on a date.

    `GET /api/airports/{iata}/hours` answers kind "none" where neither exists; on the airport page
    the field is simply absent instead. Hours are never part of the guide document: a document is
    imported from a file, and a file import must not be able to overwrite what a person entered.
    The public page prints the date beside the hours and nothing more; the owner's views print the
    whole line."""

    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
    #: The host the hours were read from ("www.heathrow.com"), for the owner's line.
    source_host: 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: the document stored on the
    #: place (`services.place_guides`), seeded with the page like the write-up.
    guide: PlaceGuide | None = None
    #: The current opening hours and their provenance, absent where neither a collected nor a hand
    #: row exists. Beside the guide, never inside it.
    hours: AirportHoursOut | None = None
    #: What one part of this place is called, from its kind (`services/places.py`): a terminal here,
    #: a pier at a port, a floor in a mall. The page prints it; it never assumes "terminal".
    area_label: str = "Terminal"


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] = []
