"""The written guide to a place's duty free, as a document stored on the place (Stream G2).

One document per place, held in `places.attributes["guide"]` and imported from a file per place
in `import/places/<slug>.json`. It replaces the code constant that held one airport's guide, which
made every new airport a code deploy: the client was promised that populating a new place is data
and a command, never a release.

**The document is faithful; the page is free.** Adam writes the profiles, and he structures them
himself: one key of seven section kinds, used for the place as a whole and again inside each area.
This module keeps his kinds and his wording, so nothing he wrote is lost and where a line came from
stays plain. What the page shows, where it sits, and whether two of his kinds read as one thing to a
shopper is a rendering decision taken elsewhere, and it never edits the document.

**Kind-neutral by construction.** An area is an area, not a terminal: at Zurich they are an airside
centre and a pier reached by train, and a cruise port's piers or a mall's floors are the same field
with a different label (`services/places.py` `PlaceKind.area_label`). Adding a kind of place costs a
registry line, never a migration.

**What it never carries.** Opening hours: those are observations with a provenance, and they live in
`airport_hours` with the date and the account that entered them (`services/hours/`). A guide that
carried hours would let a document import overwrite a collected reading.

Unknown fields and unknown section kinds fail the import rather than being dropped silently or
rendered unlabelled, because a profile that has grown a new section is a thing to read, not to lose.

Sources of truth: this module, `services/place_guides.py`, `cli_places.py`, `import/places/*.json`,
`tests/test_place_guides.py`. The labels here are the British prose the site uses; the identifiers
stay American, as `tests/test_house_style.py` requires.
"""

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel, ConfigDict


class SectionKind(str, Enum):
    """Adam's seven section kinds, as his profiles use them. The value is the identifier in the
    document and in the CSS class the renderers derive from it (`guide-section--key-facts`)."""

    KEY_FACTS = "key_facts"
    KEY_FEATURES = "key_features"
    SPECIAL_OFFERINGS = "special_offerings"
    NOTABLE_SHOPS = "notable_shops"
    INTERESTING_TO_BUY = "interesting_to_buy"
    TRAVELER_TIPS = "traveler_tips"
    RESTRICTIONS = "restrictions"


#: The heading a reader sees, in the site's own spelling. One home: both renderers read this, the
#: SPA through the mirror in `web/src/lib/placeGuide.ts`, which a test pins against this map.
SECTION_LABEL: dict[SectionKind, str] = {
    SectionKind.KEY_FACTS: "Key facts",
    SectionKind.KEY_FEATURES: "Key features",
    SectionKind.SPECIAL_OFFERINGS: "Special offerings",
    SectionKind.NOTABLE_SHOPS: "Notable shops",
    SectionKind.INTERESTING_TO_BUY: "Interesting things to buy",
    SectionKind.TRAVELER_TIPS: "Traveller tips",
    SectionKind.RESTRICTIONS: "Restrictions and limits",
}


class GuideModel(BaseModel):
    """Every part of a guide refuses a field it does not know, so a profile that has grown a
    section reaches a person instead of being silently dropped on import."""

    model_config = ConfigDict(extra="forbid")


class GuideSection(GuideModel):
    """One block of Adam's key: its kind, and his lines under it, in his order."""

    kind: SectionKind
    items: list[str] = []


class GuideArea(GuideModel):
    """A part of the place a shopper is in, and cannot easily leave: a terminal, a pier, a floor.
    `code` is what the profile prints beside the name ("T2", "Airside", "Dock E") and is what a
    shop entry points at; it is unique within the guide."""

    code: str
    name: str
    strapline: str | None = None
    sections: list[GuideSection] = []


class GuideBlock(GuideModel):
    """A titled block after the areas, in the profile's own words: "Arriving and clearing customs",
    "Other things to know". The title varies by place, so it is carried rather than derived."""

    title: str
    strapline: str | None = None
    sections: list[GuideSection] = []


class GuideShop(GuideModel):
    """A shop the guide names, whether or not we read its prices. `retailer_slug` is set only where
    we do, which is what lets the page mark the ones behind its own numbers, and is the seed of the
    shop and retailer pages without building either yet."""

    name: str
    retailer_slug: str | None = None
    #: The `code` of each area it sits in; empty where the profile does not say.
    areas: list[str] = []
    #: main | boutique | specialty, as the profile describes it.
    kinds: list[str] = []


class GuideService(GuideModel):
    """Something the shops offer that changes how you shop: ordering ahead, collection at the gate."""

    title: str
    body: str


class GuideMap(GuideModel):
    """The place's own map, linked and never rehosted (its graphics are its own)."""

    url: str
    label: str | None = None


class GuideLink(GuideModel):
    """A link the profile makes, kept because the restructure of a profile tends to lose them."""

    title: str
    url: str


class PlaceGuide(GuideModel):
    """The whole document. Every field is optional: a half-written guide is worth showing, and what
    is absent draws a placeholder in the owner's view rather than filler in the shopper's."""

    version: int = 1
    #: Which documents this was built from, newest first ("profile-2026-09-16").
    sources: list[str] = []
    #: The profile's own footer date, shown as when a person last went through the shops.
    structured: str | None = None
    #: What the author says still needs checking, and any conflict between two sources. Owner-facing:
    #: it reaches the placeholder view and the running list, never the page.
    verify: list[str] = []
    #: The line under the title: "Terminals 2, 3, 4 and 5, duty free run by World Duty Free".
    strapline: str | None = None
    operator: str | None = None
    #: Prose, where the profile writes a paragraph rather than bullets.
    overview: str | None = None
    #: Where the shops sit relative to security, when the place says it once for everywhere.
    access: str | None = None
    #: The place as a whole, before any area.
    sections: list[GuideSection] = []
    areas: list[GuideArea] = []
    #: Titled blocks after the areas, in the profile's order.
    closing: list[GuideBlock] = []
    shops: list[GuideShop] = []
    services: list[GuideService] = []
    map: GuideMap | None = None
    links: list[GuideLink] = []

    def area_codes(self) -> list[str]:
        return [a.code for a in self.areas]
