"""The end-of-day metadata bundle: what we broadcast, in the form a partner takes away.

Run it after a transmission. It reads the two append-only logs the show wrote beside itself — the
as-run (`asrun_log`, what went to air) and the anchor (`recording_anchor`, where UTC zero is in the
recording and in the stream) — re-scores each race from the database, and writes the four documents
of `docs/broadcast-metadata.md`:

    <out>/channel.json
    <out>/asrun.json
    <out>/races/<raceId>.json
    <out>/races/<raceId>/chapters.vtt

**Everything here happens AFTER the broadcast.** The one thing that had to be captured live is a
single line per hand-over, because only the producer knows what it actually showed; the rest is
derived from data already stored. That is deliberate: World Sailing's two uses — cataloguing the
programme and indexing the recording — are both post-hoc, so end-of-day delivery costs them nothing
and nothing new runs during a race day.

    python -m v3.broadcast.live.distribution_package \\
        --event trwc-2026 --asrun D:/trwc/asrun.jsonl --anchor D:/trwc/recording_anchor.jsonl \\
        --out D:/trwc/handover/2026-08-12

The store reads are injected (`feed_for`, `facts_for`), so the whole bundle builds in a test with no
database and no server.
"""
from __future__ import annotations

import argparse
import json
import os
import sys
from collections.abc import Callable
from pathlib import Path

_HERE = Path(__file__).resolve().parent
for _p in (_HERE, _HERE.parent.parent / "ingest", _HERE.parent.parent / "scoring"):
    if str(_p) not in sys.path:
        sys.path.insert(0, str(_p))

import asrun_log  # noqa: E402
import broadcast_metadata as BM  # noqa: E402
import recording_anchor  # noqa: E402
from race_queue import PRESENTATION_LAG_S  # noqa: E402  — one delay constant, not a second copy


def race_facts_from_feed(feed: dict, *, race_number: int | None = None) -> dict:
    """The as-run's per-race block, derived from the scored feed.

    Kept beside the packager rather than in the builder because it is a *reading of the store*, not
    part of the published contract: a caller with a richer race plane (stage, flight, an official
    result) supplies its own and this is the fallback."""
    meta, result = feed.get("meta") or {}, feed.get("result") or {}
    t0 = int(meta.get("trackStartUtcMs") or 0)
    finish_sec = result.get("maxFinishSec")
    facts: dict = {
        "raceName": meta.get("raceName") or meta.get("title"),
        "raceNumber": race_number,
        "state": "completed" if result.get("winner") else "racing",
        "finishUtcMs": None if finish_sec is None else int(t0 + float(finish_sec) * 1000),
        "teams": BM.team_blocks(feed.get("teams") or []),
    }
    if result.get("winner"):
        # Provisional and attributed: tracking does not confirm its own result. A caller that has
        # read the Race Officer's confirmation off the race plane overrides this.
        names = {t.get("teamId"): (t.get("fullName") or t.get("name")) for t in feed.get("teams") or []}
        facts["result"] = {"status": "provisional", "winner": result["winner"].upper(),
                           "winnerName": names.get(result["winner"]),
                           "marginM": result.get("marginM"),
                           "attributedTo": "tracking", "confirmedUtc": None}
    return facts


def build_bundle(*, event_id: str, event_name: str, binding: dict, venue: dict,
                 segments: list[dict], feed_for: Callable[[dict], dict | None],
                 facts_for: Callable[[dict, dict], dict] | None = None,
                 session_id: str | None = None, as_of_utc_ms: int,
                 stream_start_utc_ms: int | None = None,
                 session_from_utc_ms: int | None = None,
                 session_to_utc_ms: int | None = None,
                 presentation_lag_s: float = PRESENTATION_LAG_S,
                 organizer: str | None = None,
                 on_log: Callable[[str], None] = print) -> dict[str, str]:
    """`{relative path: file body}` for the whole bundle. Pure, given the two injected readers.

    A race whose feed cannot be rebuilt is **kept in the as-run and skipped for markers**, with a
    line logged. It was on air; dropping it because its telemetry is unreadable would make the
    timeline claim the broadcast showed nothing, which is worse than an entry with no chapters."""
    tz = venue.get("timeZone") or "UTC"
    files: dict[str, str] = {}
    facts: dict[str, dict] = {}

    for seg in segments:
        rid = seg.get("instanceId")
        if not rid:
            continue
        feed = None
        try:
            feed = feed_for(seg)
        except Exception as e:                       # noqa: BLE001 — one bad race must not lose the day
            on_log(f"[package] {rid}: could not rebuild the feed ({type(e).__name__}: {e})")
        if not feed:
            on_log(f"[package] {rid}: on air, but no scored feed — listed with no markers")
            continue
        facts[rid] = (facts_for(seg, feed) if facts_for else race_facts_from_feed(feed))
        base = (stream_start_utc_ms if stream_start_utc_ms is not None else None)
        offset = (None if base is None else (seg["fromUtcMs"] - base) / 1000.0)
        t0 = int((feed.get("meta") or {}).get("trackStartUtcMs") or 0)
        # Coverage began at `fromUtcMs` in PICTURE time; the feed's clock is water time, so the race
        # second the coverage started at is the difference less the delay the picture already carries.
        from_sec = max(0.0, (seg["fromUtcMs"] - t0) / 1000.0 - presentation_lag_s)
        markers = BM.markers_from_feed(feed, race_id=rid, tz=tz,
                                       presentation_lag_s=presentation_lag_s,
                                       from_sec=from_sec, stream_offset_base_s=offset)
        files[f"races/{rid}.json"] = json.dumps(
            BM.race_document(feed, race_id=rid, tz=tz, presentation_lag_s=presentation_lag_s,
                             from_utc_ms=seg["fromUtcMs"], to_utc_ms=seg.get("toUtcMs"),
                             markers=markers, race_number=facts[rid].get("raceNumber")),
            ensure_ascii=False, indent=2) + "\n"
        files[f"races/{rid}/chapters.vtt"] = BM.chapters_vtt(markers)

    cfg = (binding or {}).get("config") or {}
    files["asrun.json"] = json.dumps(BM.asrun_document(
        segments, feed_id=cfg.get("feedId") or event_id,
        session_id=session_id or event_id, tz=tz, presentation_lag_s=presentation_lag_s,
        as_of_utc_ms=as_of_utc_ms, race_facts=facts,
        session_from_utc_ms=session_from_utc_ms, session_to_utc_ms=session_to_utc_ms,
        stream_start_utc_ms=stream_start_utc_ms), ensure_ascii=False, indent=2) + "\n"

    files["channel.json"] = json.dumps(BM.channel_document(
        event_id=event_id, event_name=event_name, binding=binding, venue=venue,
        presentation_lag_s=presentation_lag_s, generated_utc_ms=as_of_utc_ms,
        organizer=organizer), ensure_ascii=False, indent=2) + "\n"
    return files


def write_bundle(files: dict[str, str], out: Path) -> list[Path]:
    written = []
    for rel, body in sorted(files.items()):
        p = out / rel
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(body, encoding="utf-8")
        written.append(p)
    return written


# --- the store-backed readers (the CLI's default wiring) ----------------------------------------

def _store_feed_for(store, *, course_id: str | None) -> Callable[[dict], dict | None]:  # noqa: ANN001
    """Rebuild one race's scored feed the same way the offline exporter does — `build_feed` then
    `enrich`, so the metadata cannot describe a race differently from the picture that aired."""
    from broadcast_feed import build_feed
    from score_race import enrich
    from serve_v3_live import meta_course_from_store

    def feed_for(seg: dict) -> dict | None:
        rid, gun = seg["instanceId"], seg.get("gunUtcMs")
        meta, course, boats = meta_course_from_store(
            store, instance_id=rid, course_id=course_id, title=seg.get("raceName") or rid,
            user="", gun_ms=gun)
        if not boats:
            return None
        feed = build_feed(store, rid, boats, meta=meta, course=course,
                          team_config=None, course_id=course_id)
        return enrich(feed)

    return feed_for


def main(argv=None) -> int:                                       # noqa: ANN001
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--event", required=True, help="event id, e.g. trwc-2026")
    ap.add_argument("--asrun", required=True, help="the as-run JSONL the producer appended")
    ap.add_argument("--anchor", help="the recording/stream anchor JSONL (for streamOffsetS)")
    ap.add_argument("--out", required=True, help="directory to write the bundle into")
    ap.add_argument("--course", help="course id, when the races do not name their own")
    ap.add_argument("--session", help="session id (default: the event id + the first race's date)")
    ap.add_argument("--event-name", dest="event_name", help="presented event name")
    ap.add_argument("--venue-name", dest="venue_name", default="")
    ap.add_argument("--tz", default="UTC", help="venue IANA time zone, e.g. Europe/Stockholm")
    ap.add_argument("--dry-run", action="store_true", help="build and report, write nothing")
    a = ap.parse_args(argv)

    rows = asrun_log.read_rows(Path(a.asrun).expanduser())
    segments = asrun_log.segments(rows)
    if not segments:
        print(f"[package] no races in {a.asrun} — nothing was on air, so there is nothing to package")
        return 1

    stream_start = None
    if a.anchor:
        samples = recording_anchor.read_samples(Path(a.anchor).expanduser())
        stream_start = recording_anchor.stream_anchor_for(samples)
        if stream_start is None:
            print("[package] no stream anchor in the log — offsets omitted, UTC is normative anyway")

    # The event's own facts: the distribution binding, the venue and its zone. Read from the store
    # when we have one; the flags are the fallback for a bundle rebuilt away from the database.
    binding: dict = {}
    venue = {"name": a.venue_name, "timeZone": a.tz}
    event_name = a.event_name or a.event
    # The same store the producer opens (`serve_v3_live`): `$VR_DATABASE_URL` → Postgres. Without it
    # the races are still listed — the as-run is what they were on air for — but nothing can be
    # re-scored, so they carry no markers. Said out loud and in capitals, because a bundle that is
    # quietly marker-less looks exactly like a race day on which nothing happened.
    store = None
    dsn = os.environ.get("VR_DATABASE_URL")
    if not dsn:
        print("[package] no $VR_DATABASE_URL — races will be listed WITHOUT markers")
    else:
        try:
            from postgres_store import PostgresStore, connect  # noqa: PLC0415 — optional dep

            store = PostgresStore(connect(dsn))
        except Exception as e:                                    # noqa: BLE001
            print(f"[package] could not open the store ({type(e).__name__}: {e}) — "
                  "races will be listed WITHOUT markers")
    if store is not None:
        from event_config import integration_for
        cfg = store.get_event_config(a.event) or {}
        binding = integration_for(cfg, "distribution") or {}
        venue = {**(cfg.get("venue") or {}), **{k: v for k, v in venue.items() if v}}
        venue.setdefault("timeZone", a.tz)
        event_name = a.event_name or cfg.get("name") or a.event

    files = build_bundle(
        event_id=a.event, event_name=event_name, binding=binding, venue=venue,
        segments=segments,
        feed_for=(_store_feed_for(store, course_id=a.course) if store is not None
                  else (lambda _s: None)),
        session_id=a.session or a.event,
        as_of_utc_ms=int(rows[-1]["tUtcMs"]) if rows else 0,
        stream_start_utc_ms=stream_start,
        session_from_utc_ms=segments[0]["fromUtcMs"],
        session_to_utc_ms=segments[-1].get("toUtcMs"))

    races = sum(1 for k in files if k.startswith("races/") and k.endswith(".json"))
    if a.dry_run:
        print(f"[package] would write {len(files)} files ({races} races) to {a.out}")
        return 0
    written = write_bundle(files, Path(a.out).expanduser())
    print(f"[package] {len(written)} files -> {a.out}  ({races} races, "
          f"{'stream anchored' if stream_start else 'UTC only'})")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
