r"""
Real-race export for the V3 broadcast demo (read-only).

Targets a specific raceInstance by ID (default: GSYS "Race 1" / "Team race" on the
'digital 8' course), pulls the boat tracks for the race window, and writes
data/demo_race.json in the SAME schema synth_race.py emits — so the scoring engine
and the render page consume it unchanged. Reads only; writes nothing to Firestore.

  ..\.venv-demo\Scripts\python.exe export\export_race_demo.py [INSTANCE_ID] [GROUP_ID]
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

from google.cloud import firestore
from google.cloud.firestore_v1.base_query import FieldFilter as FF

PROJECT = "apps-viewregatta"

# GSYS "Race 1" (instance), group "Team race", course 'digital 8'
INSTANCE_ID = sys.argv[1] if len(sys.argv) > 1 else "FZoxFxpW9C7Nv8ykrEhy"
GROUP_ID = sys.argv[2] if len(sys.argv) > 2 else None   # resolve within the instance if not given

# race window around the real start gun
PRE_GUN_S = 180
POST_GUN_S = 1500       # generous; finish detection ends the race earlier

TEAM_PALETTE = ["#1565C0", "#F9A825", "#2E7D32", "#C62828", "#6A1B9A", "#00838F", "#EF6C00", "#3949AB"]


def latlon(geo):
    if not geo:
        return None
    return {"lat": geo.get("lat"), "lon": geo.get("lon")}


def resolve_mark(w, device_first_pt):
    if w.get("lat") is not None and w.get("lon") is not None:
        return {"lat": w["lat"], "lon": w["lon"]}
    dev = w.get("deviceId") or w.get("portDeviceId") or w.get("stbdDeviceId")
    if dev and dev in device_first_pt:
        p = device_first_pt[dev]
        return {"lat": p["lat"], "lon": p["lon"]}
    return None


def device_for_boat(db, user_id, boat_id, gun_ms):
    if not boat_id:
        return None
    for m in db.collection("deviceMounts").where(filter=FF("boatId", "==", boat_id)).stream():
        md = m.to_dict() or {}
        frm = md.get("fromUtcMs") or 0
        to = md.get("toUtcMs")
        if frm <= gun_ms and (to is None or gun_ms <= to):
            return md.get("deviceId")
    for d in db.collection("users").document(user_id).collection("devices").stream():
        if (d.to_dict() or {}).get("deployedBoatId") == boat_id:
            return d.id
    return None


def boat_color(db, user_id, boat_id):
    if not boat_id:
        return None
    bd = db.collection("users").document(user_id).collection("boats").document(boat_id).get()
    if bd.exists:
        c = (bd.to_dict() or {}).get("mainSailColor") or (bd.to_dict() or {}).get("jiboColor")
        if c and c.lower() not in ("#ffffff", "#fff", "white"):
            return c
    return None


def main():
    db = firestore.Client(project=PROJECT)
    print(f"Connected: {PROJECT}")

    idoc = db.collection("raceInstances").document(INSTANCE_ID).get()
    if not idoc.exists:
        sys.exit(f"instance {INSTANCE_ID} not found")
    inst = idoc.to_dict() or {}
    user_id = inst.get("ownerUserId") or inst.get("userId")
    print(f"instance {INSTANCE_ID} name={inst.get('name')!r} owner={user_id}")

    groups = list(db.collection("raceInstances").document(INSTANCE_ID).collection("startGroups").stream())
    grp_doc = None
    if GROUP_ID:
        grp_doc = next((g for g in groups if g.id == GROUP_ID), None)
    elif len(groups) == 1:
        grp_doc = groups[0]
    else:
        grp_doc = groups[0] if groups else None
    if not grp_doc:
        sys.exit("no start group found")
    grp_id, grp = grp_doc.id, (grp_doc.to_dict() or {})
    print(f"group {grp_id} name={grp.get('name')!r}")

    gun_ms = grp.get("officialStartUtcMs") or grp.get("startDateUtcMs") or inst.get("loggingStartUtcMs")
    if not gun_ms:
        sys.exit("no gun time on group/instance")
    track_start = gun_ms - PRE_GUN_S * 1000
    track_end = gun_ms + POST_GUN_S * 1000
    print(f"gun={gun_ms} window=[{track_start},{track_end}]")

    # teams / boats / devices
    teams, device_ids = [], []
    for ti, t in enumerate(grp_doc.reference.collection("teamEntries").stream()):
        td = t.to_dict() or {}
        color = TEAM_PALETTE[ti % len(TEAM_PALETTE)]
        boats = []
        for b in t.reference.collection("boatEntries").stream():
            bd = b.to_dict() or {}
            dev = bd.get("deviceId") or device_for_boat(db, user_id, bd.get("boatId"), gun_ms)
            if not dev:
                print(f"  WARN no device for boat {bd.get('sailId')!r} (boatId={bd.get('boatId')})")
                continue
            device_ids.append(dev)
            boats.append({
                "deviceId": dev,
                "sailId": bd.get("sailId") or bd.get("visualId") or dev[:6],
                "visualId": bd.get("visualId") or "",
                "boatId": bd.get("boatId"),
                "color": boat_color(db, user_id, bd.get("boatId")) or color,
                "sternM": bd.get("distanceToSternM") or 0,
            })
        teams.append({"teamId": t.id, "name": td.get("teamName") or f"Team {ti + 1}", "boats": boats})
        print(f"  team {td.get('teamName')!r}: {len(boats)} boats")

    if not device_ids:
        sys.exit("no devices resolved — cannot build frames")

    # frames: bucket each device's points to integer seconds from track_start
    frames, device_first_pt = {}, {}
    for dev in sorted(set(device_ids)):
        n = 0
        for tr in db.collection("tracks").where(filter=FF("deviceId", "==", dev)).stream():
            tdoc = tr.to_dict() or {}
            if tdoc.get("deletedAt") is not None:
                continue
            te = tdoc.get("endUtcMs") or tdoc.get("startUtcMs") or 0
            if te < track_start or (tdoc.get("startUtcMs") or 0) > track_end:
                continue
            for bdoc in (db.collection("tracks").document(tr.id).collection("batches")
                         .order_by("fromUtcMs").stream()):
                b = bdoc.to_dict() or {}
                if (b.get("toUtcMs") or 0) < track_start:
                    continue
                if (b.get("fromUtcMs") or 0) > track_end:
                    break
                for p in b.get("points", []):
                    t = p.get("t") or 0
                    if t < track_start or t > track_end or p.get("lat") is None:
                        continue
                    sec = (t - track_start) // 1000
                    rec = {"lat": round(p["lat"], 7), "lon": round(p.get("lon"), 7),
                           "speed": round(p.get("speed", 0) or 0, 2),
                           "course": round(p.get("course", 0) or 0, 1)}
                    frames.setdefault(str(sec), {})[dev] = rec
                    device_first_pt.setdefault(dev, rec)
                    n += 1
        print(f"  {dev[:10]}…  {n} points")

    # drop boats with no points in the window, and any team left empty
    present = {d for fr in frames.values() for d in fr}
    for tm in teams:
        tm["boats"] = [b for b in tm["boats"] if b["deviceId"] in present]
    teams = [tm for tm in teams if tm["boats"]]
    print(f"  after pruning empty boats: teams={len(teams)} "
          f"boats={sum(len(t['boats']) for t in teams)}")

    # course geometry
    cid = inst.get("courseId")
    cdoc = db.collection("courses").document(cid).get() if cid else None
    cdata = (cdoc.to_dict() if (cdoc and cdoc.exists) else None) or {}
    sl = cdata.get("startLine") or {}
    fl = cdata.get("finishLine") or sl
    wps = []
    for w in cdata.get("waypoints") or []:
        ll = resolve_mark(w, device_first_pt)
        if ll:
            wps.append({"name": w.get("name") or "", "type": w.get("type"),
                        "rounding": w.get("rounding"), "lat": ll["lat"], "lon": ll["lon"]})
    pts = [latlon(sl.get("port")), latlon(sl.get("starboard")),
           latlon(fl.get("port")), latlon(fl.get("starboard"))] + [{"lat": w["lat"], "lon": w["lon"]} for w in wps]
    pts = [p for p in pts if p and p.get("lat") is not None]
    center = ({"lat": sum(p["lat"] for p in pts) / len(pts), "lon": sum(p["lon"] for p in pts) / len(pts)}
              if pts else None)
    course = {
        "name": cdata.get("name") or "digital 8",
        "center": center,
        "startLine": {"port": latlon(sl.get("port")), "starboard": latlon(sl.get("starboard"))},
        "finishLine": {"port": latlon(fl.get("port")), "starboard": latlon(fl.get("starboard"))},
        "waypoints": wps,
    }

    feed = {
        "schema": "vr-broadcast-demo/1",
        "source": "real",
        "meta": {
            "title": f"{inst.get('name')} — {grp.get('name')}  ·  {course['name']}",
            "user": "GSYS", "instanceId": INSTANCE_ID, "groupId": grp_id,
            "openEnded": False,
            "raceType": inst.get("raceType") or grp.get("raceType") or "team_race_2",
            "trackStartUtcMs": track_start, "gunUtcMs": gun_ms, "trackEndUtcMs": track_end,
            "frameStepMs": 1000,
            "windFromDeg": grp.get("windDeg") if grp.get("windDeg") is not None else 0,
        },
        "course": course,
        "teams": teams,
        "frames": frames,
        "video": [],
    }

    out = Path(__file__).resolve().parent.parent / "data" / "demo_race.json"
    with open(out, "w", encoding="utf-8") as f:
        json.dump(feed, f, ensure_ascii=False, separators=(",", ":"))
    secs = sorted(int(s) for s in frames)
    span = (secs[-1] - secs[0]) if secs else 0
    print(f"\nWrote {out}")
    print(f"  teams={len(teams)} devices={len(set(device_ids))} frames={len(frames)} "
          f"span={span}s waypoints={len(wps)} center={center}")
    if not frames:
        print("  WARNING: no track points in window.")


if __name__ == "__main__":
    main()
