#!/usr/bin/env python3
"""Phase A1 metrics v2: AI vs station-scoped, de-duplicated GT.

Differences from compute_metrics.py, all of which were making the old gate
unreadable rather than merely pessimistic:

- GT is the station file (load operators only, cross-operator duplicates merged).
  The raw file counted the same physical load twice and included the unload
  operator, who works outside this camera's station.
- Occupancy is compared in PERSON-seconds on both sides. The old code compared
  AI scene-seconds (ceiling 3600) against GT person-seconds (5158), so work MAPE
  could not go below ~30% even with a perfect model.
- Cycle timing error is reported separately for GT rows that carry second
  precision, because the other operator's rows are minute-quantized and carry
  +-30 s of clerical error that is not the model's to answer for.
"""

from __future__ import annotations

import argparse
import json
from pathlib import Path


def match_points(gt_ts, pred_ts, window):
    used, hits, pairs = set(), 0, []
    for g in sorted(gt_ts):
        best_i, best_d = None, window + 1
        for i, p in enumerate(pred_ts):
            if i in used:
                continue
            d = abs(p - g)
            if d < best_d:
                best_d, best_i = d, i
        if best_i is not None and best_d <= window:
            used.add(best_i)
            hits += 1
            pairs.append({"gt": g, "pred": pred_ts[best_i], "dt": pred_ts[best_i] - g})
    return hits, len(pred_ts) - hits, len(gt_ts) - hits, pairs


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--run-dir", required=True)
    ap.add_argument("--gt-station", required=True)
    ap.add_argument("--cycle-window", type=float, default=90.0)
    ap.add_argument("--out", required=True)
    args = ap.parse_args()

    run = Path(args.run_dir)
    summary = json.loads((run / "summary_v2.json").read_text(encoding="utf-8"))
    cycles = json.loads((run / "load_cycles_v2.json").read_text(encoding="utf-8"))["cycles"]
    gt = json.loads(Path(args.gt_station).read_text(encoding="utf-8"))

    vt0 = int(summary["video_t0"])
    vt1 = vt0 + int(summary["n_frames"])

    gt_pts = [p for p in gt["load_cycle_points"] if vt0 <= p["t"] < vt1]
    pred = [c["t_start_abs"] for c in cycles]
    hits, fp, fn, pairs = match_points([p["t"] for p in gt_pts], pred, args.cycle_window)
    recall = hits / len(gt_pts) if gt_pts else 0.0
    precision = hits / len(pred) if pred else 0.0
    errs = sorted(abs(p["dt"]) for p in pairs)
    med = errs[len(errs) // 2] if errs else None

    precise_t = {p["t"] for p in gt_pts if not p.get("t_quantized")}
    perrs = sorted(abs(p["dt"]) for p in pairs if p["gt"] in precise_t)
    pmed = perrs[len(perrs) // 2] if perrs else None

    gt_work, gt_wait = gt["work_person_sec"], gt["wait_person_sec"]
    ai_work, ai_wait = summary["work_person_sec"], summary["wait_person_sec"]
    gt_tot, ai_tot = gt_work + gt_wait, ai_work + ai_wait

    def mape(ai, g):
        return round(abs(ai - g) / g, 4) if g else None

    report = {
        "window_abs": [vt0, vt1],
        "gt_source": str(args.gt_station),
        "cycle": {
            "gt_n": len(gt_pts),
            "pred_n": len(pred),
            "hits": hits,
            "fp": fp,
            "fn": fn,
            "recall": round(recall, 4),
            "precision": round(precision, 4),
            "median_abs_err_sec": med,
            "median_abs_err_sec_precise_gt_only": pmed,
            "count_ratio": round(len(pred) / len(gt_pts), 3) if gt_pts else None,
            "pass": recall >= 0.75 and precision >= 0.70 and (med is None or med <= 60),
            "pairs": pairs,
        },
        "occupancy_person_sec": {
            "gt_work": gt_work,
            "ai_work": ai_work,
            "work_mape": mape(ai_work, gt_work),
            "gt_wait": gt_wait,
            "ai_wait": ai_wait,
            "wait_mape": mape(ai_wait, gt_wait),
            "gt_total_presence": gt_tot,
            "ai_total_presence": ai_tot,
            "presence_mape": mape(ai_tot, gt_tot),
            "gt_work_share": round(gt_work / gt_tot, 3) if gt_tot else None,
            "ai_work_share": round(ai_work / ai_tot, 3) if ai_tot else None,
            "work_pass": (mape(ai_work, gt_work) or 1) <= 0.35,
            "wait_pass": (mape(ai_wait, gt_wait) or 1) <= 0.35,
        },
        "tracking": {
            "unique_ids": summary["n_unique_tracks"],
            "baseline_unique_ids": 707,
        },
    }
    report["phase_a1_gate"] = bool(
        report["cycle"]["pass"]
        and report["occupancy_person_sec"]["work_pass"]
        and report["occupancy_person_sec"]["wait_pass"]
    )
    Path(args.out).write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    c, o = report["cycle"], report["occupancy_person_sec"]
    print(
        f"cycle  GT={c['gt_n']} AI={c['pred_n']} hits={c['hits']} R={c['recall']:.2f} "
        f"P={c['precision']:.2f} med|dt|={c['median_abs_err_sec']}s (precise-GT {c['median_abs_err_sec_precise_gt_only']}s)\n"
        f"work   GT={o['gt_work']} AI={o['ai_work']} MAPE={o['work_mape']}\n"
        f"wait   GT={o['gt_wait']} AI={o['ai_wait']} MAPE={o['wait_mape']}\n"
        f"share  GT work={o['gt_work_share']} AI work={o['ai_work_share']}  presence MAPE={o['presence_mape']}\n"
        f"gate   {report['phase_a1_gate']}"
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
