"""Where a finished replay LIVES, and what travels with it.

A rendered race is not just an MP4. Six months later the file has to answer which race it is, who
sailed it, who won, when it was sailed and against which event — and a filename cannot carry that,
so a film separated from its context is a film nobody can use. Two things therefore happen to every
replay this module publishes:

* a **sidecar** `<name>.json` beside it, carrying the race's own identity as the scorer resolved it
  (event, race name, teams with their full names, winner, the official gun and stop, the venue) plus
  how it was rendered (window, resolution, fps, quality, language, engine);
* the same facts **inside the container** (`ffmpeg -metadata`), because a sidecar is a file somebody
  will eventually move, mail or drop, and the one thing that survives every such move is the MP4.

**Where** is a deployment fact, never a product constant — one user's library is a shared drive, the
next one's is a NAS or a local folder, and the difference must not reach the code. `$VR_REPLAY_DIR`
names it (`G:/Shared drives/…`, `/Volumes/…`, anything `Path` accepts, `~` expanded). Unset, replays
collect under `_cap/replays/` so the behaviour exists out of the box and only its LOCATION is
configured.

Nothing here decides what a race is called or who won it. Those are the race plane's facts, already
carried on the exported feed's `meta`/`result`; this only copies them somewhere they survive.
"""
from __future__ import annotations

import datetime as _dt
import json
import os
import re
from pathlib import Path

#: Where replays collect when the deployment has not said. Beside the other render artefacts, so a
#: developer who never sets the variable still gets one directory rather than a scatter.
DEFAULT_SUBDIR = "replays"

#: A SIMULATED race and a real one are filed apart, because the two answer different questions and
#: get mixed up exactly when it matters — a rehearsal render and a championship render of the same
#: course look alike in a thumbnail, and only one of them is evidence of anything. The split is a
#: directory rather than a filename convention so it survives sorting, searching and copying.
SIMULATED_DIR = "Simulated"
REAL_DIR = "Real"


def library_dir(root: Path, explicit: str | None = None) -> Path:
    """The replay library, created if missing.

    `explicit` (a CLI flag) wins over `$VR_REPLAY_DIR`, which wins over `<root>/_cap/replays`. The
    variable is expanded for `~` and for other variables, because the value people actually type is
    a shell path (`~/Movies/replays`, `%ONEDRIVE%/…`)."""
    raw = explicit or os.environ.get("VR_REPLAY_DIR")
    if raw:
        return Path(os.path.expandvars(str(raw))).expanduser()
    return root / "_cap" / DEFAULT_SUBDIR


def is_simulated(feed: dict) -> bool:
    """Whether this race came from the simulator.

    TWO FIELDS, because they were introduced at different times and either alone gets it wrong.
    `meta.simulated` is the modern flag the simulator publishes with the race; `source == "sim"` is
    what the exporter has always stamped. A feed rendered before the flag existed carries only the
    latter — reading `meta.simulated` alone files every one of them as REAL, which is the error that
    matters, since it is the direction that turns a rehearsal into apparent evidence."""
    meta = feed.get("meta") or {}
    return bool(meta.get("simulated")) or str(feed.get("source") or "").lower() == "sim"


def replay_dir_for(base: Path, feed: dict) -> Path:
    """The library subdirectory this race belongs in — see `SIMULATED_DIR`."""
    return base / (SIMULATED_DIR if is_simulated(feed) else REAL_DIR)


def _venue_local(ms: int | None, tz_name: str | None) -> _dt.datetime | None:
    """A race's instant in the VENUE's zone — an event fact, never the render machine's zone.

    The cloud gateway runs UTC, so "machine local" looks right in dev and is wrong on air; the same
    trap applies to a filename, which is how a day's races get filed under the wrong date."""
    if not ms:
        return None
    tz: _dt.tzinfo = _dt.UTC
    if tz_name:
        try:
            from zoneinfo import ZoneInfo
            tz = ZoneInfo(str(tz_name))
        except Exception:                       # noqa: BLE001 — an unknown zone is not worth a crash
            tz = _dt.UTC
    return _dt.datetime.fromtimestamp(int(ms) / 1000, tz)


def _slug(text: str, *, limit: int = 48) -> str:
    """A filename-safe fragment. Conservative on purpose: this name is typed, pasted into shells and
    synced by tools that are not all Unicode-clean.

    Accented letters are TRANSLITERATED rather than dropped, because dropping them mangles the word
    instead of simplifying it — Swedish "före" became "f-re", which is not a shorter spelling of
    anything. Venues, race names and team names in this product are routinely Nordic, so this is the
    normal case, not an edge one."""
    import unicodedata
    folded = unicodedata.normalize("NFKD", str(text or ""))
    ascii_only = folded.encode("ascii", "ignore").decode("ascii")
    s = re.sub(r"[^A-Za-z0-9]+", "-", ascii_only).strip("-")
    return s[:limit].strip("-") or "race"


def replay_name(feed: dict, race_id: str) -> str:
    """The stem a replay is filed under: `<venue-local date>_<race>_<matchup>`.

    Date first so a library sorts chronologically, which is how a regatta is remembered. The matchup
    is the team CODES — the spelled-out names live in the sidecar, where there is room."""
    meta = feed.get("meta") or {}
    when = _venue_local(meta.get("gunUtcMs"), ((meta.get("venue") or {}).get("timeZone")))
    day = when.strftime("%Y-%m-%d") if when else "undated"
    race = _slug(meta.get("raceName") or meta.get("title") or race_id, limit=32)
    codes = [str(t.get("name") or "") for t in (feed.get("teams") or []) if t.get("name")]
    matchup = _slug("-v-".join(codes), limit=24) if codes else ""
    return f"{day}_{race}_{matchup}".rstrip("_-")


def replay_metadata(feed: dict, *, race_id: str, instance_id: str | None = None,
                    res: str | None = None, fps: int | None = None, crf: int | None = None,
                    lang: str | None = None, engine: str | None = None,
                    render_started_utc_ms: int | None = None,
                    film_start_race_sec: float | None = None,
                    film_duration_s: float | None = None) -> dict:
    """Everything a replay should be able to answer about itself, from the feed it was rendered from.

    Read straight off `meta`/`result`/`teams` — the scorer already resolved them and this must not
    form a second opinion. A field the feed does not carry is OMITTED rather than defaulted: an
    absent winner is a race with no result (a sea trial has none), and `null` says so where `""`
    would read as a team whose name is missing."""
    meta = feed.get("meta") or {}
    res_obj = feed.get("result") or {}
    venue = meta.get("venue") or {}
    when = _venue_local(meta.get("gunUtcMs"), venue.get("timeZone"))
    out: dict = {
        "raceId": race_id,
        "instanceId": instance_id or meta.get("instanceId"),
        "raceName": meta.get("raceName") or meta.get("title"),
        "event": meta.get("eventName"),
        "venue": venue.get("name"),
        "venueTimeZone": venue.get("timeZone"),
        # ISO 8601 with the offset, per the product's storage rule — never a bare `Z`, and never the
        # render machine's zone.
        "gunUtc": when.isoformat() if when else None,
        "gunUtcMs": meta.get("gunUtcMs"),
        "trackStartUtcMs": meta.get("trackStartUtcMs"),
        "trackEndUtcMs": meta.get("trackEndUtcMs"),
        "teams": [{"code": t.get("name"), "name": t.get("fullName"),
                   "boats": [b.get("sailId") or b.get("deviceId")
                             for b in (t.get("boats") or [])]}
                  for t in (feed.get("teams") or [])],
        "winner": res_obj.get("winnerName"),
        "simulated": is_simulated(feed),
        "source": feed.get("source"),
        "render": {"resolution": res, "fps": fps, "crf": crf, "language": lang, "engine": engine},
    }
    if meta.get("gunUtcMs") and meta.get("trackStartUtcMs"):
        # THE FEED's pre-start, which is not necessarily the FILM's — the name says which. It sat here
        # alone reading `preStartSeconds: 180` while the film opened at T−2:00, so the sidecar described
        # a window the artefact beside it did not have.
        out["feedPreStartSeconds"] = round(
            (int(meta["gunUtcMs"]) - int(meta["trackStartUtcMs"])) / 1000)
        out["preStartSeconds"] = out["feedPreStartSeconds"]      # retained: older sidecars carry it
    # …and THE FILM's own window.
    #
    # NAMED FOR THEIR ORIGIN, because this codebase has two and mixing them is the bug this whole
    # change is repairing. `feedSec` counts from `trackStartUtcMs`, which is what `events[].tSec` and
    # `result.maxFinishSec` use; `gunOffsetS` counts from the gun, which is what the renderer's
    # `?startRace=` takes and what a person means by "T−3". A field called `…RaceSec` would be either.
    if film_start_race_sec is not None and meta.get("gunUtcMs") and meta.get("trackStartUtcMs"):
        gun_sec = (int(meta["gunUtcMs"]) - int(meta["trackStartUtcMs"])) / 1000
        out["filmStartsAtGunOffsetS"] = round(film_start_race_sec, 1)      # −180.0 ⇒ opens at T−3:00
        out["filmStartsAtFeedSec"] = round(gun_sec + film_start_race_sec, 1)
        out["preStartSeconds"] = round(-film_start_race_sec) if film_start_race_sec < 0 else 0
        if film_duration_s is not None:
            out["durationS"] = round(film_duration_s, 1)
            out["filmEndsAtFeedSec"] = round(gun_sec + film_start_race_sec + film_duration_s, 1)
            # Whether the last boat crossing is inside the film — the question the old window got
            # wrong by 14 seconds, in the direction nobody checks. Compared in FEED seconds, the
            # origin `maxFinishSec` is already in.
            finish = (feed.get("result") or {}).get("maxFinishSec")
            out["coversFinish"] = bool(finish is None or finish <= out["filmEndsAtFeedSec"])
    if render_started_utc_ms:
        # When the render BEGAN, and named so. It is computed before an hour and a half of capture,
        # and "rendered" would read as when the film was finished.
        out["renderStartedUtc"] = _dt.datetime.fromtimestamp(
            render_started_utc_ms / 1000, _dt.UTC).isoformat()
    return {k: v for k, v in out.items() if v is not None}


def container_tags(meta: dict) -> list[str]:
    """`ffmpeg -metadata` arguments carrying the same facts INSIDE the file.

    Only the fields a player or a file browser will actually surface — title, when, a one-line
    description and the full record as a comment. A sidecar can be lost; the container cannot."""
    teams = " v ".join(t.get("name") or t.get("code") or "" for t in (meta.get("teams") or []))
    title = " · ".join(x for x in (meta.get("raceName"), teams) if x)
    bits = [x for x in (meta.get("event"), meta.get("venue"), meta.get("gunUtc")) if x]
    if meta.get("winner"):
        bits.append(f"winner {meta['winner']}")
    if meta.get("simulated"):
        bits.append("simulated")
    args: list[str] = []
    for key, value in (("title", title), ("description", " — ".join(bits)),
                       ("comment", json.dumps(meta, ensure_ascii=False)),
                       ("date", (meta.get("gunUtc") or "")[:10]),
                       ("album", meta.get("event") or "")):
        if value:
            args += ["-metadata", f"{key}={value}"]
    return args


def write_sidecar(mp4: Path, meta: dict) -> Path:
    """Write `<mp4 stem>.json` beside the film. Indented — it exists to be read."""
    side = mp4.with_suffix(".json")
    side.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
    return side
