"""The boat-cam media map — ONE builder, for every producer that has to make one.

There are two ways a producer learns where a boat's clips are. With a database it reads
`video_segments` and signs each `gcs_path`; without one it asks the read plane for `clips.json`.
Those are genuinely different jobs. What comes out the other end is not: `{boatRef: [{tSec, file,
video}, …]}`, the exact shape the renderer's `mediaAt` consumes, sorted, with unusable rows dropped
and a boat that has nothing left out entirely.

That shape used to be spelled out twice, once per producer, and the two drifted in the way this
codebase keeps being bitten by — not by disagreeing, but by one of them *not existing*. The REST
producer had no media path at all for months: the mode whose whole selling point is that it needs no
database credentials, and therefore the mode a render PC, a partner and a guest stream all run,
published an empty map while the phones filmed normally and the read plane served the footage. Every
tile drew its honest "(clip unavailable for this window)" and nothing anywhere said why.

So the mapping lives here, both producers call it, and `test_media_map.py` runs the two against the
same clips and asserts byte-identical output. A third producer gets the shape for free; a fourth
cannot invent its own.
"""
from __future__ import annotations

from collections.abc import Iterable

#: How far BEFORE a race's track start a clip still belongs to it. A device buffers while it has no
#: signal and uploads on reconnect, and the boats are filming through the pre-start, so the first
#: clips of a heat are routinely stamped before its track began. Asking from the track start exactly
#: would drop them — and they are the ones covering the approach and the gun.
#:
#: It lives here, with the map, because it is the WINDOW the map covers and both producers need it:
#: the REST one to bound its `clips.json` query, the store one to bound its `video_segments` scan.
#: `rest_source` re-exports it, so the name it was written under still resolves.
PRE_RACE_CLIP_MARGIN_MS = 10 * 60_000


def media_rows(clips: Iterable[tuple[int | float | None, str | None]],
               track_start_ms: int, *,
               from_ms: int | None = None, until_ms: int | None = None) -> list[dict]:
    """`(start_ms, url)` pairs → the renderer's rows for one boat, in clip order.

    `from_ms`/`until_ms` (absolute UTC ms, inclusive) drop clips filmed OUTSIDE this race. They exist
    for the two LINE cameras, and the asymmetry with a hull is the whole point: a hull is entered in
    one race and films for it, while a start vessel and a finish vessel are bound to the COURSE and
    keep filming across every race run on it. Without a bound, the generous `PRE_RACE_CLIP_MARGIN_MS`
    below reaches back into the PREVIOUS race — and because `tSec` is clamped at zero, that footage
    does not merely appear, it stacks at the start of this race's timeline and plays at the gun. A
    viewer sees the line camera showing a start that is not the one on screen.

    This is a PRESENTATION bound and nothing else: the vessels go on filming and every clip is still
    stored, so a replay of the neighbouring race shows it in its own place.

    A row whose start or URL is unusable is SKIPPED, never defaulted: `tSec: 0` would stack every
    clip whose timing could not be read onto the start of the race, which is worse than the clip
    being absent — the tile would play the wrong footage confidently at the gun.

    `tSec` is clamped at zero rather than dropped when a clip predates the track: a device buffers
    while it has no signal and uploads on reconnect, so a clip stamped just before the race began is
    ordinary and belongs at the start of the timeline.

    IT KEEPS ITS MILLISECONDS. The renderer seeks into a clip by `cursor − tSec`, so flooring the
    position to a whole second put the picture up to a second away from the instant its own furniture
    claimed — and by a DIFFERENT amount per boat, since each camera's clips start on their own
    fraction of a second. Two tiles side by side then disagreed by up to a second at the same
    instant, permanently, and no amount of correctness upstream could close it: the filename carries
    the millisecond (`SERVER_INTERFACE.md` §5) and the device burns it into the picture, so this was
    the one place the precision was thrown away."""
    rows: list[dict] = []
    for start_ms, url in clips:
        if not url or not isinstance(start_ms, int | float) or isinstance(start_ms, bool):
            continue
        if from_ms is not None and start_ms < from_ms:
            continue
        if until_ms is not None and start_ms > until_ms:
            continue
        rows.append({"tSec": round(max(0.0, (start_ms - track_start_ms) / 1000), 3),
                     "file": url, "video": True})
    rows.sort(key=lambda r: r["tSec"])
    return rows


def media_map(per_boat: dict[str, Iterable[tuple[int | float | None, str | None]]],
              track_start_ms: int, *,
              from_ms: int | None = None, until_ms: int | None = None) -> dict[str, list[dict]]:
    """`{boatRef: (start_ms, url) pairs}` → the media map the feed publishes.

    A boat with no usable clip is OMITTED rather than given an empty list, because the two mean
    different things to the tile: absent is "this producer knows of no footage for this hull", an
    empty list would read as "there is footage and none of it covers now". The tile tells the viewer
    a different thing in each case, so the map must not blur them.

    `from_ms`/`until_ms` bound which clips BELONG to this race — see `media_rows`."""
    out: dict[str, list[dict]] = {}
    for boat, clips in per_boat.items():
        rows = media_rows(clips, track_start_ms, from_ms=from_ms, until_ms=until_ms)
        if rows:
            out[boat] = rows
    return out
