#!/usr/bin/env python3
"""Compare census automation with the adjudicated 230-article manual audit."""
from __future__ import annotations

import csv
import json
import math
from collections import Counter, defaultdict
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "research-output"
WORK = ROOT / "work"
MANUAL = OUT / "university-manual-audit.csv"
AUTO_ARTICLE = (WORK / "university-automated-article-coding.csv") if (WORK / "university-automated-article-coding.csv").exists() else OUT / "article-coding.csv"
AUTO_ACTOR = (WORK / "university-automated-actor-coding.csv") if (WORK / "university-automated-actor-coding.csv").exists() else OUT / "actor-level-analysis.csv"
RESULT_CSV = OUT / "university-automation-validation.csv"
RESULT_JSON = OUT / "university-validation-results.json"


def read_csv(path: Path) -> list[dict]:
    with path.open(newline="", encoding="utf-8") as stream:
        return list(csv.DictReader(stream))


def kappa(pairs: list[tuple[str, str]]) -> dict:
    pairs = [(a, b) for a, b in pairs if a != "" and b != ""]
    n = len(pairs)
    if not n:
        return {"n": 0, "raw_agreement": None, "kappa": None}
    po = sum(a == b for a, b in pairs) / n
    ca, cb = Counter(a for a, _ in pairs), Counter(b for _, b in pairs)
    cats = set(ca) | set(cb)
    pe = sum(ca[c] / n * cb[c] / n for c in cats)
    value = None if pe == 1 else (po - pe) / (1 - pe)
    out = {"n": n, "raw_agreement": po, "kappa": value, "manual_distribution": dict(ca), "automated_distribution": dict(cb)}
    if cats <= {"0", "1"}:
        tp = sum(a == b == "1" for a, b in pairs)
        tn = sum(a == b == "0" for a, b in pairs)
        fp = sum(a == "0" and b == "1" for a, b in pairs)
        fn = sum(a == "1" and b == "0" for a, b in pairs)
        out.update({
            "sensitivity": tp / (tp + fn) if tp + fn else None,
            "specificity": tn / (tn + fp) if tn + fp else None,
            "precision": tp / (tp + fp) if tp + fp else None,
            "false_positive_n": fp,
            "false_negative_n": fn,
        })
    return out


def pool_auto_actor(rows: list[dict], actor_group: str) -> dict:
    if actor_group == "administration":
        selected = [r for r in rows if r["actor_category"] == "administration_trustees"]
    else:
        selected = [r for r in rows if r["actor_group"] == "challenger"]
    present = [r for r in selected if r["strict_present"] == "1"]
    if not present:
        return {"present": "0", "marked": "", "headline_adverse": "", "lede_adverse": "", "high_scrutiny": ""}
    claim = [r for r in present if r["substantive_claim_proxy"] == "1" and r["scrutiny_score_auto"] != ""]
    mean_scrutiny = sum(float(r["scrutiny_score_auto"]) for r in claim) / len(claim) if claim else None
    return {
        "present": "1",
        "marked": str(int(any(r["marked_strict"] == "1" for r in present))),
        "headline_adverse": str(int(any(r["headline_frame"] == "adverse" for r in present))),
        "lede_adverse": str(int(any(r["lede_frame"] == "adverse" for r in present))),
        "high_scrutiny": "" if mean_scrutiny is None else str(int(mean_scrutiny >= 2)),
    }


def actor_simple(value: str) -> str:
    if value == "administration_trustees":
        return "administration"
    if value in {"students_orgs", "protesters", "workers_unions", "faculty_critics"}:
        return "challenger"
    if value == "government_officials":
        return "government"
    if value in {"none", "none_observed", ""}:
        return "none"
    if value == "other":
        return "other"
    return "unclear"


def main() -> None:
    manual = read_csv(MANUAL)
    articles = {r["article_id"]: r for r in read_csv(AUTO_ARTICLE)}
    actors = defaultdict(list)
    for row in read_csv(AUTO_ACTOR):
        actors[row["article_id"]].append(row)
    assert len(manual) == 230 and set(r["article_id"] for r in manual) <= set(articles)

    pairs: dict[str, list[tuple[str, str]]] = defaultdict(list)
    evidence = defaultdict(list)
    for m in manual:
        aid, a = m["article_id"], articles[m["article_id"]]
        aa = pool_auto_actor(actors[aid], "administration")
        ac = pool_auto_actor(actors[aid], "challenger")
        mappings = {
            "admin_presence": (m["admin_present"], aa["present"]),
            "challenger_presence": (m["challenger_present"], ac["present"]),
            "admin_negative_tone_vs_marked_window": ("" if m["admin_tone"] == "" else str(int(int(m["admin_tone"]) < 0)), aa["marked"]),
            "challenger_negative_tone_vs_marked_window": ("" if m["challenger_tone"] == "" else str(int(int(m["challenger_tone"]) < 0)), ac["marked"]),
            "admin_headline_adverse": ("" if m["admin_headline_frame"] in {"not_present", "unclear"} else str(int(m["admin_headline_frame"] == "adverse")), aa["headline_adverse"]),
            "challenger_headline_adverse": ("" if m["challenger_headline_frame"] in {"not_present", "unclear"} else str(int(m["challenger_headline_frame"] == "adverse")), ac["headline_adverse"]),
            "admin_lede_adverse": ("" if m["admin_lede_frame"] in {"not_present", "unclear"} else str(int(m["admin_lede_frame"] == "adverse")), aa["lede_adverse"]),
            "challenger_lede_adverse": ("" if m["challenger_lede_frame"] in {"not_present", "unclear"} else str(int(m["challenger_lede_frame"] == "adverse")), ac["lede_adverse"]),
            "admin_high_scrutiny": ("" if m["admin_skepticism"] == "" else str(int(int(m["admin_skepticism"]) >= 2)), aa["high_scrutiny"]),
            "challenger_high_scrutiny": ("" if m["challenger_skepticism"] == "" else str(int(int(m["challenger_skepticism"]) >= 2)), ac["high_scrutiny"]),
            "official_assertion_tested": (m["official_assertion_tested"], a["official_assertion_tested_auto"]),
            "challenger_assertion_tested": (m["challenger_assertion_tested"], a["challenger_assertion_tested_auto"]),
            "release_reliance_high": ("" if m["release_reliance"] == "" else str(int(int(m["release_reliance"]) >= 2)), a["high_release_reliance_auto"]),
            "documents_records_data": (m["documents_records_data"], a["documents_records_data_auto"]),
            "affected_people_consulted": (m["affected_people_consulted"], a["affected_people_consulted_auto"]),
            "power_history_consequences_context": (m["power_history_consequences_context"], a["power_history_consequences_context_auto"]),
            "follow_up_orientation": (m["follow_up_orientation"], a["follow_up_orientation_auto"]),
            "first_actor": (m["first_actor"], actor_simple(a["first_actor_auto"])),
            "first_quoted_actor": (m["first_quoted_actor"], actor_simple(a["first_quoted_actor_auto"])),
        }
        for measure, pair in mappings.items():
            pairs[measure].append(pair)
            if pair[0] != "" and pair[1] != "" and pair[0] != pair[1] and len(evidence[measure]) < 10:
                evidence[measure].append(aid)
    rows = []
    results = {}
    for measure in sorted(pairs):
        item = kappa(pairs[measure])
        results[measure] = item
        rows.append({
            "measure": measure,
            "n": item["n"],
            "raw_agreement": item["raw_agreement"],
            "cohen_kappa": item["kappa"],
            "passes_0_60_gate": int(item["kappa"] is not None and item["kappa"] >= 0.60),
            "sensitivity": item.get("sensitivity"),
            "specificity": item.get("specificity"),
            "precision": item.get("precision"),
            "false_positive_n": item.get("false_positive_n", ""),
            "false_negative_n": item.get("false_negative_n", ""),
            "example_disagreement_article_ids": ";".join(evidence[measure]),
        })
    with RESULT_CSV.open("w", newline="", encoding="utf-8") as stream:
        writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)
    gate = {measure: item["kappa"] is not None and item["kappa"] >= 0.60 for measure, item in results.items()}
    payload = {
        "manual_audit_n": len(manual),
        "threshold": 0.60,
        "statistics": results,
        "population_semantic_gate": gate,
        "unvalidated_automated_measures": [
            "direct_quote_word_share", "attribution_verb_counts", "unique_source_count",
            "category_specific_actor_presence", "vulnerable_identification_from_images",
        ],
        "interpretation": "Only automated semantic measures with Cohen's kappa at or above 0.60 may be promoted from screening to population estimates. Mechanical metadata are exempt.",
    }
    RESULT_JSON.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({"measures": len(rows), "passing": sum(gate.values()), "output": str(RESULT_JSON)}, indent=2))


if __name__ == "__main__":
    main()
