r"""Commentary review-and-correction loop — the foundation for the Level-4 AI author.

Two modes:

  EXPORT (default): write a markup-ready CSV of a race's commentary, one row per spoken line, WITH the
  race context a reviewer needs to judge it without watching the render (race-clock, who leads, the leg's
  point of sail, the boats the line is about). Three blank columns are left for the reviewer:
      verdict  — one of: ok | wrong | dull | irrelevant   (Martin's "right / wrong / irrelevant")
      better   — what a good caller would have said instead (free text; the gold line)
      tag      — optional finer reason: fact | repeat | timing | tone | names | other
  Excel-friendly (utf-8-sig). One file per language.

      ..\..\..\.venv-demo\Scripts\python.exe export\review_commentary.py --race gsys08 --lang en
      ..\..\..\.venv-demo\Scripts\python.exe export\review_commentary.py            # real.json, all langs

  INGEST: read a reviewer's filled-in CSV back, print the "report card" (how much is ok / wrong / dull /
  irrelevant — Martin's percentages, made real per race) and write a corrections dataset JSON that is
  BOTH the eval set (the lines an AI version must stop getting wrong) AND the few-shot gold examples
  (his `better` lines) for the Level-4 author.

      # Martin returns the .xlsx workbooks — ingest them all, then score the author against the gold:
      ..\..\..\.venv-demo\Scripts\python.exe export\review_commentary.py --ingest reviews\gsys0{1,2,6,7}_en.xlsx
      ..\..\..\.venv-demo\Scripts\python.exe export\score_author.py --judge

This tool is read-through over the baked feeds; it changes no race data.
"""
from __future__ import annotations

import argparse
import csv
import json
import sys
from pathlib import Path

_HERE = Path(__file__).resolve().parent
ROOT = _HERE.parent.parent                                   # v3/broadcast
sys.path.insert(0, str(ROOT.parent / "scoring"))             # for --author (enrich + AIAuthor)
RACES = ROOT / "demo" / "data" / "races"
REVIEWS = ROOT / "demo" / "data" / "reviews"

ROLE_BY_VOICE = {"m": "Caller", "f": "Analyst"}
VERDICTS = {"ok", "wrong", "dull", "irrelevant"}
TAGS = {"", "fact", "repeat", "timing", "tone", "names", "other"}
COLUMNS = ["#", "clock", "t_sec", "role", "type", "leader", "leg", "about", "text",
           "ai_suggestion", "verdict", "better", "tag"]


def _load_env() -> None:
    """Load v3/broadcast/.env so --author can read ANTHROPIC_API_KEY without it on the CLI/in chat."""
    import os
    envf = ROOT / ".env"
    if envf.exists():
        for raw in envf.read_text(encoding="utf-8").splitlines():
            line = raw.strip()
            if line and not line.startswith("#") and "=" in line:
                k, v = line.split("=", 1)
                os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))


def load_feed(race: str | None) -> dict:
    """real.json by default, else a manifest race by id (e.g. gsys08)."""
    if not race or race == "real":
        return json.loads((RACES / "real.json").read_text(encoding="utf-8"))
    doc = json.loads((RACES / "manifest.json").read_text(encoding="utf-8"))
    entry = next((r for r in doc["races"] if r["id"] == race), None)
    if not entry:
        raise SystemExit(f"race {race} not in manifest")
    return entry["data"]


def _context(feed: dict):
    """Per-second helpers: race-clock (mm:ss vs the gun), leader team name, leader's point of sail, and a
    deviceId -> short label map ("RH 4")."""
    meta = feed["meta"]
    gun_sec = (meta["gunUtcMs"] - meta["trackStartUtcMs"]) // 1000
    label, team_name = {}, {}
    for t in feed.get("teams", []):
        team_name[t["teamId"]] = t["name"]
        for b in t["boats"]:
            label[b["deviceId"]] = b.get("visualId") or f'{t["name"]} {b.get("sailId", "")}'.strip()
    st = feed.get("standings", {})

    def clock(t_sec):
        d = t_sec - gun_sec
        sign = "-" if d < 0 else ""
        return f"{sign}{abs(d) // 60}:{abs(d) % 60:02d}"

    def at(t_sec):
        s = st.get(str(t_sec)) or {}
        leader = team_name.get(s.get("leader"), "")
        leg = ""
        for b in s.get("boats", []):
            if leader and b.get("teamName") == leader and b.get("pointOfSail"):
                leg = b["pointOfSail"]
                break
        return leader, leg

    return clock, at, label


def rows_for(feed: dict, lang: str, ai: dict | None = None) -> list[dict]:
    clock, at, label = _context(feed)
    ai = ai or {}
    out = []
    for n, e in enumerate(sorted(feed["events"], key=lambda e: e["tSec"]), 1):
        text = (e.get("i18n") or {}).get(lang) or (e.get("text") if lang == "en" else "")
        if not text:
            continue
        leader, leg = at(e["tSec"])
        about = " & ".join(label.get(d, d) for d in (e.get("focus") or []))
        out.append({"#": n, "clock": clock(e["tSec"]), "t_sec": e["tSec"],
                    "role": ROLE_BY_VOICE.get(e.get("voice"), e.get("voice", "")),
                    "type": e.get("type", ""), "leader": leader, "leg": leg, "about": about,
                    "text": text, "ai_suggestion": ai.get(e["tSec"], ""),
                    "verdict": "", "better": "", "tag": ""})
    return out


def author_suggestions(feed: dict, lang: str) -> tuple[dict, dict]:
    """Run the Level-4 author over the race; return ({tSec: ai_line}, the re-enriched feed). The review
    then shows the deterministic engine commentary + the AI's validated suggestion per colour line."""
    from ai_author import AIAuthor
    from score_race import enrich
    scored = enrich({**feed, "captureAiBeats": True})
    authored = AIAuthor().write(scored, langs=(lang,))
    return {t: d.get(lang) for t, d in authored.items() if d.get(lang)}, scored


def export(race: str | None, lang: str, out_dir: Path, author: bool = False) -> tuple[Path, int]:
    feed = load_feed(race)
    ai = {}
    if author:
        ai, feed = author_suggestions(feed, lang)        # review the re-enriched commentary + suggestions
    rows = rows_for(feed, lang, ai)
    out_dir.mkdir(parents=True, exist_ok=True)
    path = out_dir / f"{race or 'real'}_{lang}.csv"
    with open(path, "w", encoding="utf-8-sig", newline="") as f:
        w = csv.DictWriter(f, fieldnames=COLUMNS)
        w.writeheader()
        w.writerows(rows)
    return path, len(rows)


def score_review(rows: list[dict]) -> dict:
    """Tally a reviewer's filled-in rows into the report card + the corrections dataset (pure; tested)."""
    judged = [r for r in rows if (r.get("verdict") or "").strip().lower() in VERDICTS]
    counts = {v: 0 for v in VERDICTS}
    corrections = []
    for r in judged:
        v = r["verdict"].strip().lower()
        counts[v] += 1
        if v != "ok":
            corrections.append({"t_sec": int(r["t_sec"]), "role": r.get("role", ""),
                                "type": r.get("type", ""), "verdict": v,
                                "said": r.get("text", ""), "better": (r.get("better") or "").strip(),
                                "tag": (r.get("tag") or "").strip().lower()})
    n = len(judged)
    pct = {v: (round(100 * c / n) if n else 0) for v, c in counts.items()}
    return {"judged": n, "counts": counts, "pct": pct, "corrections": corrections}


def _read_rows(path: Path) -> list[dict]:
    """A reviewer's filled-in file as row dicts — whether it's the CSV or the friendlier .xlsx workbook
    Martin actually grades (its 'Grade the calls' sheet; hidden columns like t_sec are still read)."""
    if path.suffix.lower() == ".xlsx":
        from openpyxl import load_workbook
        wb = load_workbook(path, read_only=True, data_only=True)
        ws = wb["Grade the calls"] if "Grade the calls" in wb.sheetnames else wb[wb.sheetnames[-1]]
        rows = list(ws.iter_rows(values_only=True))
        if not rows:
            return []
        header = [str(h) if h is not None else "" for h in rows[0]]
        return [dict(zip(header, ["" if v is None else str(v) for v in r], strict=False)) for r in rows[1:]]
    return list(csv.DictReader(path.read_text(encoding="utf-8-sig").splitlines()))


def ingest(path: Path) -> dict:
    """Read a graded CSV/xlsx → report card + corrections dataset JSON. Returns the report (for a batch
    caller to aggregate). The dataset is written next to the input as <stem>.corrections.json."""
    rows = _read_rows(path)
    bad = {(r.get("verdict") or "").strip().lower() for r in rows} - VERDICTS - {""}
    if bad:
        print(f"  warning: unrecognised verdicts {sorted(bad)} (use {sorted(VERDICTS)}) — ignored")
    res = score_review(rows)
    n = res["judged"]
    print(f"reviewed {path.name}: {n} lines judged")
    if n:
        c, p = res["counts"], res["pct"]
        print(f"  ok {c['ok']} ({p['ok']}%) | wrong {c['wrong']} ({p['wrong']}%) | "
              f"dull {c['dull']} ({p['dull']}%) | irrelevant {c['irrelevant']} ({p['irrelevant']}%)")
        with_gold = sum(1 for x in res["corrections"] if x["better"])
        print(f"  {len(res['corrections'])} corrections ({with_gold} with a gold 'better' line)")
    # normalise the stem so a .xlsx and its .csv both land on <race>_<lang>.corrections.json
    out = path.with_name(path.name.rsplit(".", 1)[0] + ".corrections.json")
    out.write_text(json.dumps(res, ensure_ascii=False, indent=1), encoding="utf-8")
    print(f"  wrote {out.name}  (eval set + few-shot gold for the Level-4 author)")
    return res


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--race", help="race id (e.g. gsys08); default real.json")
    ap.add_argument("--lang", help="language for export (en/sv); default every language present")
    ap.add_argument("--author", action="store_true",
                    help="add an ai_suggestion column from the Level-4 author (needs ANTHROPIC_API_KEY; .env loaded)")
    ap.add_argument("--ingest", nargs="+",
                    help="read filled-in review file(s) — CSV or .xlsx — into the report card + dataset")
    ap.add_argument("--out", help=f"export dir (default {REVIEWS})")
    args = ap.parse_args()
    if args.ingest:
        reps = [ingest(Path(p)) for p in args.ingest]
        if len(reps) > 1:
            tot = sum(r["judged"] for r in reps)
            gold = sum(1 for r in reps for x in r["corrections"] if x["better"])
            by = {v: sum(r["counts"][v] for r in reps) for v in VERDICTS}
            print(f"\nALL {len(reps)} files: {tot} judged  "
                  f"(ok {by['ok']} · wrong {by['wrong']} · dull {by['dull']} · irrelevant {by['irrelevant']}), "
                  f"{gold} gold lines. Now: score_author.py --judge")
        return 0
    if args.author:
        _load_env()
    feed = load_feed(args.race)
    langs = [args.lang] if args.lang else sorted(
        set().union(*[set((e.get("i18n") or {}).keys()) for e in feed["events"]]) or {"en"})
    out_dir = Path(args.out) if args.out else REVIEWS
    for lang in langs:
        path, k = export(args.race, lang, out_dir, author=args.author)
        print(f"wrote {path}  ({k} lines to review)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
