"""Reading and writing a place's guide document (Stream G2).

The guide lives in `places.attributes["guide"]`, one document per place, imported from
`import/places/<slug>.json`. This module is the only code that reads or writes that key, so the
rules hold in one place: `guide` is a **hand value**, so only an import writes it and no collector
may; the write is idempotent, and a document that does not validate is refused whole rather than
half-applied.

Why a document and not a table: it is a few paragraphs per place, rewritten a few times a year and
reviewed like copy, and the shape is the author's rather than ours (`models/place_guide.py`). Why
data and not the code constant that came before: a constant made every new place a release, and
populating a place was promised as a file and a command.

Sources of truth: this module, `models/place_guide.py`, `cli_places.py`, `tests/test_place_guides.py`.
"""

from __future__ import annotations

import json
import pathlib

from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified

from app.models.place_guide import PlaceGuide
from app.models.places import Place

def _files() -> pathlib.Path:
    """Where the source files live, one per place, named for its slug.

    The workspace's `import/` is mounted read-only at `/srv/import` inside the container, and is
    the repository folder on a developer's machine; the same two candidates the plan and the
    running list use. Naming a path on the command line always wins over both."""
    local = pathlib.Path(__file__).resolve().parents[3] / "import" / "places"
    return local if local.exists() else pathlib.Path("/srv/import/places")


FILES = _files()

KEY = "guide"


def place_by_slug(db: Session, slug: str) -> Place | None:
    return db.execute(select(Place).where(Place.slug == slug)).scalar_one_or_none()


def place_by_identifier(db: Session, scheme: str, value: str) -> Place | None:
    """A place by an external identifier (`iata`, `locode`). The identifiers are an open list on
    the row, so this reads them in Python: there are tens of places, not millions, and a JSON
    containment query would tie the code to Postgres and break the SQLite test kit."""
    value = value.upper()
    for place in db.execute(select(Place)).scalars():
        for ident in place.identifiers or []:
            if ident.get("scheme") == scheme and str(ident.get("value", "")).upper() == value:
                return place
    return None


def for_place(place: Place | None) -> PlaceGuide | None:
    """The guide on a place row, or None. A stored document that no longer validates raises: it
    means the schema moved without its files, and rendering half of it would hide that."""
    if place is None:
        return None
    raw = (place.attributes or {}).get(KEY)
    return PlaceGuide.model_validate(raw) if raw else None


def for_airport(db: Session, iata: str | None) -> PlaceGuide | None:
    """The guide for an airport by its code, the lookup the airport page makes."""
    if not iata:
        return None
    return for_place(place_by_identifier(db, "iata", iata))


def has_guide(db: Session, iata: str | None) -> bool:
    """Cheap enough for the airports index, and it never parses the document."""
    if not iata:
        return False
    place = place_by_identifier(db, "iata", iata)
    return bool(place and (place.attributes or {}).get(KEY))


def load_file(path: pathlib.Path | str) -> PlaceGuide:
    """Read and validate one source file. An unknown section kind or an unknown field fails here,
    naming the file, rather than reaching the database."""
    path = pathlib.Path(path)
    return PlaceGuide.model_validate(json.loads(path.read_text()))


def as_stored(guide: PlaceGuide) -> dict:
    """The document as it is written to the row and to a file: defaults omitted, so a file stays
    readable and a diff shows only what someone wrote."""
    return guide.model_dump(exclude_defaults=True, mode="json")


def set_guide(db: Session, place: Place, guide: PlaceGuide) -> bool:
    """Write the document, returning whether anything changed. Unchanged means no write at all, so
    re-running an import is free and leaves the row's `updated_at` alone."""
    attributes = dict(place.attributes or {})
    new = as_stored(guide)
    if attributes.get(KEY) == new:
        return False
    attributes[KEY] = new
    place.attributes = attributes
    flag_modified(place, "attributes")
    db.flush()
    return True


def file_for(slug: str) -> pathlib.Path:
    return FILES / f"{slug}.json"


def write_file(slug: str, guide: PlaceGuide) -> pathlib.Path:
    """Export: the same JSON shape as an import file, so export after import is the file again."""
    path = file_for(slug)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(as_stored(guide), indent=2, ensure_ascii=False) + "\n")
    return path
