#!/usr/bin/env python3
"""Compute Phase A metrics: AI vs GT for cycles and occupancy."""

from __future__ import annotations

import argparse
import json
from pathlib import Path


def iou(a0: float, a1: float, b0: float, b1: float) -> float:
    inter = max(0.0, min(a1, b1) - max(a0, b0))
    union = max(a1, b0) - min(a0, b1)  # wrong
    union = (a1 - a0) + (b1 - b0) - inter
    return inter / union if union > 0 else 0.0


def match_points(gt_ts: list[float], pred_ts: list[float], window: float) -> tuple[int, int, int, list[float]]:
    used = set()
    hits = 0
    errs: list[float] = []
    for g in gt_ts:
        best_i = None
        best_d = window + 1
        for i, p in enumerate(pred_ts):
            if i in used:
                continue
            d = abs(p - g)
            if d < best_d:
                best_d = d
                best_i = i
        if best_i is not None and best_d <= window:
            used.add(best_i)
            hits += 1
            errs.append(best_d)
    fp = len(pred_ts) - hits
    fn = len(gt_ts) - hits
    return hits, fp, fn, errs


def match_intervals(gt: list[tuple[float, float]], pred: list[tuple[float, float]], thr: float = 0.3):
    used = set()
    hits = 0
    for g0, g1 in gt:
        best_i = None
        best = 0.0
        for i, (p0, p1) in enumerate(pred):
            if i in used:
                continue
            v = iou(g0, g1, p0, p1)
            if v > best:
                best = v
                best_i = i
        if best_i is not None and best >= thr:
            used.add(best_i)
            hits += 1
    fp = len(pred) - hits
    fn = len(gt) - hits
    return hits, fp, fn


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--run-dir", required=True)
    ap.add_argument("--gt-json", 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.json").read_text(encoding="utf-8"))
    occ = json.loads((run / "occupancy_intervals.json").read_text(encoding="utf-8"))
    cycles = json.loads((run / "load_cycles.json").read_text(encoding="utf-8"))["cycles"]
    gt = json.loads(Path(args.gt_json).read_text(encoding="utf-8"))
    vt0 = int(summary["video_t0"])
    vt1 = vt0 + int(summary["n_frames"])

    gt_cycles = [p for p in gt.get("load_cycle_points") or [] if vt0 <= p["t"] < vt1]
    pred_starts = [c["t_start_abs"] for c in cycles]
    hits, fp, fn, errs = match_points([p["t"] for p in gt_cycles], pred_starts, args.cycle_window)
    recall = hits / len(gt_cycles) if gt_cycles else 0.0
    precision = hits / (hits + fp) if (hits + fp) else 0.0
    med_err = sorted(errs)[len(errs) // 2] if errs else None

    gt_wait = [(i["t0"], i["t1"]) for i in gt.get("intervals") or [] if i["kind"] == "wait" and i["t1"] > vt0 and i["t0"] < vt1]
    gt_work = [(i["t0"], i["t1"]) for i in gt.get("intervals") or [] if i["kind"] == "work" and i["priority"] >= 2 and i["t1"] > vt0 and i["t0"] < vt1]
    # clip to window
    def clip(ivs):
        out = []
        for a, b in ivs:
            out.append((max(a, vt0), min(b, vt1)))
        return [(a, b) for a, b in out if b > a]

    gt_wait, gt_work = clip(gt_wait), clip(gt_work)
    ai_wait = [(i["t0"], i["t1"]) for i in occ.get("wait") or []]
    ai_work = [(i["t0"], i["t1"]) for i in occ.get("work") or []]
    wh, wfp, wfn = match_intervals(gt_wait, ai_wait)
    oh, ofp, ofn = match_intervals(gt_work, ai_work)

    gt_wait_sec = sum(b - a for a, b in gt_wait)
    ai_wait_sec = sum(b - a for a, b in ai_wait)
    gt_work_sec = sum(b - a for a, b in gt_work)
    ai_work_sec = sum(b - a for a, b in ai_work)
    wait_mape = abs(ai_wait_sec - gt_wait_sec) / gt_wait_sec if gt_wait_sec else None
    work_mape = abs(ai_work_sec - gt_work_sec) / gt_work_sec if gt_work_sec else None

    report = {
        "window_abs": [vt0, vt1],
        "cycle": {
            "gt_n": len(gt_cycles),
            "pred_n": len(cycles),
            "hits": hits,
            "fp": fp,
            "fn": fn,
            "recall": recall,
            "precision": precision,
            "median_abs_err_sec": med_err,
            "pass": recall >= 0.75 and precision >= 0.70 and (med_err is None or med_err <= 60),
        },
        "wait": {
            "gt_sec": gt_wait_sec,
            "ai_sec": ai_wait_sec,
            "mape": wait_mape,
            "interval_hits": wh,
            "fp": wfp,
            "fn": wfn,
            "pass": wait_mape is not None and wait_mape <= 0.35,
        },
        "work": {
            "gt_sec": gt_work_sec,
            "ai_sec": ai_work_sec,
            "mape": work_mape,
            "interval_hits": oh,
            "fp": ofp,
            "fn": ofn,
            "coverage_recall": oh / len(gt_work) if gt_work else 0.0,
            "pass": (oh / len(gt_work) if gt_work else 0.0) >= 0.80,
        },
        "unique_tracks": len(summary.get("unique_track_ids") or []),
    }
    report["phase_a1_gate"] = bool(report["cycle"]["pass"] and report["wait"]["pass"] and report["work"]["pass"])
    Path(args.out).write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    md = Path(args.out).with_suffix(".md")
    md.write_text(
        "\n".join(
            [
                "# Phase A1 metrics",
                "",
                f"- window abs video_t: `{vt0}`–`{vt1}`",
                f"- cycles: R={recall:.2f} P={precision:.2f} med|Δt|={med_err} gt={len(gt_cycles)} pred={len(cycles)} pass={report['cycle']['pass']}",
                f"- wait MAPE={wait_mape} gt={gt_wait_sec}s ai={ai_wait_sec}s pass={report['wait']['pass']}",
                f"- work coverage_recall={report['work']['coverage_recall']:.2f} pass={report['work']['pass']}",
                f"- **phase_a1_gate={report['phase_a1_gate']}**",
                "",
            ]
        ),
        encoding="utf-8",
    )
    print(json.dumps(report, ensure_ascii=False, indent=2))
    return 0


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