#!/usr/bin/env python3
"""T^GPT v0.8.2 aggregation evaluation with distractor false-positive control."""
from __future__ import annotations
import argparse, json
from pathlib import Path


def analyze(data: dict) -> dict:
    cap = int(data["max_output_tokens"])
    rows = data["annotations"]
    if cap <= 0 or not rows:
        raise ValueError("positive max_output_tokens and annotations are required")
    by_agg = {}
    for r in rows:
        aid = str(r["aggregate_id"]); cid = str(r["candidate_cluster_id"]); typ = str(r["candidate_type"])
        if typ not in ("TRUE_POOL", "DISTRACTOR"):
            raise ValueError(f"bad candidate_type {typ}")
        rep = r["represented"]
        if rep not in (0, 1, False, True):
            raise ValueError("represented must be boolean/0/1")
        tok = int(r["aggregate_output_tokens"])
        if tok > cap:
            raise ValueError(f"{aid}: output cap exceeded")
        rec = by_agg.setdefault(aid, {"true": {}, "dist": {}, "tokens": tok, "condition": r.get("condition_blind_id")})
        if rec["tokens"] != tok:
            raise ValueError(f"{aid}: inconsistent token accounting")
        target = rec["true"] if typ == "TRUE_POOL" else rec["dist"]
        if cid in target:
            raise ValueError(f"duplicate candidate {aid}/{cid}")
        target[cid] = 1 if bool(rep) else 0
    weights = {str(x["id"]): float(x.get("weight", 1.0)) for x in data["true_pool_clusters"]}
    if not weights or any(v < 0 for v in weights.values()) or sum(weights.values()) <= 0:
        raise ValueError("valid nonnegative true-pool weights required")
    expected_true = set(weights); totalw = sum(weights.values()); results = []
    for aid, x in sorted(by_agg.items()):
        missing = expected_true - set(x["true"])
        if missing:
            raise ValueError(f"{aid}: missing true cluster annotations {sorted(missing)}")
        if not x["dist"]:
            raise ValueError(f"{aid}: distractor annotations required")
        tpw = sum(weights[c] * x["true"][c] for c in weights); recall = tpw / totalw
        fp = sum(x["dist"].values()); nd = len(x["dist"]); fpr = fp / nd
        tp = sum(x["true"].values()); precision = tp / (tp + fp) if tp + fp else None
        results.append({"aggregate_id": aid, "condition_blind_id": x["condition"], "aggregate_output_tokens": x["tokens"], "AGG_COVERAGE": recall, "REPRESENTATION_FPR": fpr, "REPRESENTATION_PRECISION": precision, "n_true_clusters": len(x["true"]), "n_distractors": nd})
    return {"schema": "T_GPT_AGGREGATION_EVAL_V0_8_2", "max_output_tokens": cap, "aggregates": results, "guardrail": "AGG_COVERAGE is uninterpretable without reported REPRESENTATION_FPR and identical output-token caps."}


def main():
    ap = argparse.ArgumentParser(); ap.add_argument("input", type=Path); ap.add_argument("-o", "--output", type=Path)
    a = ap.parse_args(); out = analyze(json.loads(a.input.read_text(encoding="utf-8"))); payload = json.dumps(out, ensure_ascii=False, indent=2) + "\n"
    a.output.write_text(payload, encoding="utf-8") if a.output else print(payload, end="")


if __name__ == "__main__":
    main()
