#!/usr/bin/env python3
"""Validate independent manual files, report reliability, and prepare private adjudication rows."""
from __future__ import annotations

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

ROOT = Path(__file__).resolve().parents[2]
WORK = ROOT / "work"
OUT = ROOT / "research-output"
CODE_A = WORK / "university-manual-coder-a.csv"
CODE_B = WORK / "university-manual-coder-b.csv"
ASSIGN_A = WORK / "university-coder-a-assignment.jsonl"
ASSIGN_B = WORK / "university-coder-b-assignment.jsonl"
PRIVATE_ADJ = WORK / "university-manual-adjudication-assignment.jsonl"
RELIABILITY = OUT / "university-manual-intercoder-reliability.csv"

FIELDS = [
    "article_id", "url", "manual_reviewer", "eligible_news", "article_type",
    "admin_present", "challenger_present", "admin_tone", "challenger_tone",
    "admin_headline_frame", "challenger_headline_frame", "admin_lede_frame",
    "challenger_lede_frame", "admin_skepticism", "challenger_skepticism",
    "official_assertion_tested", "challenger_assertion_tested", "release_reliance",
    "documents_records_data", "affected_people_consulted",
    "power_history_consequences_context", "follow_up_orientation", "first_actor",
    "first_quoted_actor", "vulnerable_identification", "evidence_excerpt", "manual_notes",
]
NOMINAL = [
    "eligible_news", "article_type", "admin_present", "challenger_present",
    "admin_headline_frame", "challenger_headline_frame", "admin_lede_frame",
    "challenger_lede_frame", "official_assertion_tested", "challenger_assertion_tested",
    "documents_records_data", "affected_people_consulted", "power_history_consequences_context",
    "follow_up_orientation", "first_actor", "first_quoted_actor",
]
ORDERED = {
    "admin_tone": ["-2", "-1", "0", "1", "2"],
    "challenger_tone": ["-2", "-1", "0", "1", "2"],
    "admin_skepticism": ["0", "1", "2", "3"],
    "challenger_skepticism": ["0", "1", "2", "3"],
    "release_reliance": ["0", "1", "2", "3"],
    "vulnerable_identification": ["0", "1", "2"],
}


def jsonl(path: Path) -> list[dict]:
    return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]


def read_codes(path: Path, assignment: Path, reviewer: str) -> dict[str, dict]:
    with path.open(newline="", encoding="utf-8") as stream:
        reader = csv.DictReader(stream)
        assert reader.fieldnames == FIELDS, f"{path.name}: incorrect columns"
        rows = list(reader)
    expected = {row["article_id"] for row in jsonl(assignment)}
    observed = {row["article_id"] for row in rows}
    assert len(rows) == 140 and len(observed) == 140 and observed == expected
    assert all(row["manual_reviewer"] == reviewer for row in rows)
    return {row["article_id"]: row for row in rows}


def cohen_kappa(pairs: list[tuple[str, str]]) -> tuple[float | None, float]:
    pairs = [(a, b) for a, b in pairs if a != "" and b != ""]
    if not pairs:
        return None, math.nan
    agreement = sum(a == b for a, b in pairs) / len(pairs)
    ca, cb = Counter(a for a, _ in pairs), Counter(b for _, b in pairs)
    cats = set(ca) | set(cb)
    expected = sum((ca[c] / len(pairs)) * (cb[c] / len(pairs)) for c in cats)
    kappa = None if expected == 1 else (agreement - expected) / (1 - expected)
    return kappa, agreement


def weighted_kappa(pairs: list[tuple[str, str]], order: list[str]) -> tuple[float | None, float]:
    pairs = [(a, b) for a, b in pairs if a in order and b in order]
    if not pairs:
        return None, math.nan
    n, m = len(pairs), len(order)
    idx = {value: pos for pos, value in enumerate(order)}
    denom = max(1, (m - 1) ** 2)
    def loss(a: str, b: str) -> float:
        return ((idx[a] - idx[b]) ** 2) / denom
    observed_loss = sum(loss(a, b) for a, b in pairs) / n
    ca, cb = Counter(a for a, _ in pairs), Counter(b for _, b in pairs)
    expected_loss = sum((ca[a] / n) * (cb[b] / n) * loss(a, b) for a in order for b in order)
    kappa = None if expected_loss == 0 else 1 - observed_loss / expected_loss
    agreement = sum(a == b for a, b in pairs) / n
    return kappa, agreement


def main() -> None:
    a = read_codes(CODE_A, ASSIGN_A, "coder_a")
    b = read_codes(CODE_B, ASSIGN_B, "coder_b")
    overlap = sorted(set(a) & set(b))
    assert len(overlap) == 50
    reliability_rows = []
    for field in NOMINAL:
        pairs = [(a[aid][field], b[aid][field]) for aid in overlap]
        valid = sum(x != "" and y != "" for x, y in pairs)
        kappa, agreement = cohen_kappa(pairs)
        reliability_rows.append({"variable": field, "statistic": "cohen_kappa", "n_pairs": valid, "raw_agreement": agreement, "reliability": kappa})
    for field, order in ORDERED.items():
        pairs = [(a[aid][field], b[aid][field]) for aid in overlap]
        valid = sum(x in order and y in order for x, y in pairs)
        kappa, agreement = weighted_kappa(pairs, order)
        reliability_rows.append({"variable": field, "statistic": "quadratic_weighted_kappa", "n_pairs": valid, "raw_agreement": agreement, "reliability": kappa})
    with RELIABILITY.open("w", newline="", encoding="utf-8") as stream:
        writer = csv.DictWriter(stream, fieldnames=["variable", "statistic", "n_pairs", "raw_agreement", "reliability"])
        writer.writeheader()
        writer.writerows(reliability_rows)

    assignment_rows = {row["article_id"]: row for row in jsonl(ASSIGN_A)}
    assignment_rows.update({row["article_id"]: row for row in jsonl(ASSIGN_B)})
    adjudication = []
    for aid in overlap:
        disagreements = {field: {"coder_a": a[aid][field], "coder_b": b[aid][field]}
                         for field in FIELDS[3:] if field not in {"evidence_excerpt", "manual_notes"} and a[aid][field] != b[aid][field]}
        item = dict(assignment_rows[aid])
        item["coder_a"] = a[aid]
        item["coder_b"] = b[aid]
        item["disagreements"] = disagreements
        adjudication.append(item)
    with PRIVATE_ADJ.open("w", encoding="utf-8") as stream:
        for row in adjudication:
            stream.write(json.dumps(row, ensure_ascii=False) + "\n")
    print(json.dumps({
        "overlap_n": len(overlap),
        "articles_with_any_disagreement": sum(bool(row["disagreements"]) for row in adjudication),
        "disagreement_cells": sum(len(row["disagreements"]) for row in adjudication),
        "reliability_output": str(RELIABILITY),
        "private_adjudication_output": str(PRIVATE_ADJ),
    }, indent=2))


if __name__ == "__main__":
    main()
