#!/usr/bin/env python3
"""Inter-rater reliability for the IETF landscape survey.

Reads the two re-rating result files written by rerate-intercoder.py
(data/rerate/sonnet.jsonl, data/rerate/haiku.jsonl) plus the existing
production labels (ratings table), and reports:

  - Cohen's kappa on PRIMARY CATEGORY (nominal) for each rater pair
  - Quadratic-weighted kappa on each ordinal dimension (novelty, maturity,
    overlap, momentum, relevance)
  - Raw agreement %, and the category confusion (most-confused pairs)

Pairs compared: sonnet-rerate vs haiku-rerate (the controlled inter-coder run),
and each rerate vs the existing production labels (drift check).

Pure stdlib + numpy. No API calls. Read-only on the DB.
Writes data/reports/survey-kappa.md.

Usage: PYTHONPATH=src python3 scripts/survey-kappa.py
"""
from __future__ import annotations

import json
from collections import Counter
from pathlib import Path

import numpy as np

from ietf_analyzer.config import Config
from ietf_analyzer.db import Database

RERATE_DIR = Path("data/rerate")
OUT = Path("data/reports/survey-kappa.md")
DIMS = ["novelty", "maturity", "overlap", "momentum", "relevance"]
DIM_KEYS = {"novelty": ("n", "novelty"), "maturity": ("m", "maturity"),
            "overlap": ("o", "overlap"), "momentum": ("mo", "momentum"),
            "relevance": ("r", "relevance")}


def _strip_fence(text: str) -> str:
    t = text.strip()
    if t.startswith("```"):
        t = t.split("\n", 1)[1] if "\n" in t else t[3:]
        if t.rstrip().endswith("```"):
            t = t.rstrip()[:-3]
    return t.strip()


def _clamp(v):
    try:
        v = int(round(float(v)))
    except (TypeError, ValueError):
        return None
    return min(5, max(1, v))


def parse_rerate(alias: str) -> dict[str, dict]:
    """draft_name -> {primary_cat, dims...} from a rerate jsonl."""
    out = {}
    p = RERATE_DIR / f"{alias}.jsonl"
    if not p.exists():
        return out
    for line in p.read_text().splitlines():
        if not line.strip():
            continue
        rec = json.loads(line)
        if "raw" not in rec:
            continue
        try:
            data = json.loads(_strip_fence(rec["raw"]))
        except json.JSONDecodeError:
            continue
        cats = data.get("c", data.get("categories", []))
        primary = cats[0] if isinstance(cats, list) and cats else None
        entry = {"primary": primary}
        for dim, (k1, k2) in DIM_KEYS.items():
            entry[dim] = _clamp(data.get(k1, data.get(k2)))
        out[rec["draft_name"]] = entry
    return out


def load_prod(db: Database) -> dict[str, dict]:
    out = {}
    rows = db.conn.execute(
        """SELECT r.draft_name, r.categories, r.novelty, r.maturity,
                  r.overlap, r.momentum, r.relevance
           FROM drafts d JOIN ratings r ON d.name = r.draft_name
           WHERE d.source='ietf' AND (r.false_positive=0 OR r.false_positive IS NULL)"""
    ).fetchall()
    for name, cats_json, *dimvals in rows:
        try:
            cats = json.loads(cats_json) if cats_json else []
        except json.JSONDecodeError:
            cats = []
        e = {"primary": cats[0] if cats else None}
        for dim, v in zip(DIMS, dimvals):
            e[dim] = _clamp(v)
        out[name] = e
    return out


def cohen_kappa(a: list, b: list) -> tuple[float, float, int]:
    """Nominal Cohen's kappa. Returns (kappa, raw_agreement, n)."""
    labels = sorted(set(a) | set(b))
    idx = {l: i for i, l in enumerate(labels)}
    k = len(labels)
    m = np.zeros((k, k))
    for x, y in zip(a, b):
        m[idx[x], idx[y]] += 1
    n = m.sum()
    po = np.trace(m) / n
    pe = (m.sum(0) @ m.sum(1)) / (n * n)
    kappa = (po - pe) / (1 - pe) if (1 - pe) else 1.0
    return kappa, po, int(n)


def weighted_kappa(a: list, b: list, k: int = 5) -> tuple[float, float, int]:
    """Quadratic-weighted kappa for ordinal 1..k ratings."""
    pairs = [(x, y) for x, y in zip(a, b) if x is not None and y is not None]
    if not pairs:
        return float("nan"), float("nan"), 0
    a2, b2 = zip(*pairs)
    o = np.zeros((k, k))
    for x, y in pairs:
        o[x - 1, y - 1] += 1
    n = o.sum()
    w = np.zeros((k, k))
    for i in range(k):
        for j in range(k):
            w[i, j] = (i - j) ** 2 / (k - 1) ** 2
    ha = np.array([a2.count(v) for v in range(1, k + 1)], float)
    hb = np.array([b2.count(v) for v in range(1, k + 1)], float)
    e = np.outer(ha, hb) / n
    num = (w * o).sum()
    den = (w * e).sum()
    kappa = 1 - num / den if den else 1.0
    raw_agree = np.trace(o) / n
    return kappa, raw_agree, int(n)


def interpret(k: float) -> str:
    if k != k:
        return "n/a"
    if k < 0: return "worse than chance"
    if k < 0.20: return "slight"
    if k < 0.40: return "fair"
    if k < 0.60: return "moderate"
    if k < 0.80: return "substantial"
    return "almost perfect"


def compare(name: str, A: dict, B: dict, lines: list):
    shared = sorted(set(A) & set(B))
    lines.append(f"\n## {name}  (n shared = {len(shared)})\n")
    # primary category
    pa = [A[d]["primary"] for d in shared if A[d]["primary"] and B[d]["primary"]]
    pb = [B[d]["primary"] for d in shared if A[d]["primary"] and B[d]["primary"]]
    kappa, po, n = cohen_kappa(pa, pb)
    lines.append(f"**Primary category** (Cohen's κ): κ = {kappa:.3f} ({interpret(kappa)}), "
                 f"raw agreement {po:.1%}, n = {n}\n")
    # confusion: top disagreements
    dis = Counter()
    for d in shared:
        x, y = A[d]["primary"], B[d]["primary"]
        if x and y and x != y:
            dis[tuple(sorted((x, y)))] += 1
    if dis:
        lines.append("\nMost-confused category pairs:\n")
        lines.append("| A | B | count |\n|---|---|------:|\n")
        for (x, y), c in dis.most_common(8):
            lines.append(f"| {x} | {y} | {c} |\n")
    # ordinal dims
    lines.append("\n**Ordinal dimensions** (quadratic-weighted κ):\n\n")
    lines.append("| dimension | κ_w | raw agree | n |\n|---|---:|---:|---:|\n")
    for dim in DIMS:
        a = [A[d][dim] for d in shared]
        b = [B[d][dim] for d in shared]
        kw, ra, n = weighted_kappa(a, b)
        lines.append(f"| {dim} | {kw:.3f} ({interpret(kw)}) | {ra:.1%} | {n} |\n")


def main():
    db = Database(Config.load())
    sonnet = parse_rerate("sonnet")
    haiku = parse_rerate("haiku")
    prod = load_prod(db)
    print(f"parsed: sonnet={len(sonnet)} haiku={len(haiku)} prod={len(prod)}")
    if not sonnet or not haiku:
        print("rerate files incomplete — run rerate-intercoder.py --collect first")
        return

    lines = ["# Inter-rater reliability — IETF landscape survey\n",
             f"\nCorpus: clean IETF (n≈524). Sonnet={len(sonnet)}, Haiku={len(haiku)}, prod labels={len(prod)}.\n",
             "\nκ interpretation (Landis & Koch): <0.2 slight, 0.2–0.4 fair, "
             "0.4–0.6 moderate, 0.6–0.8 substantial, >0.8 almost perfect.\n"]
    compare("Sonnet (re-rate) vs Haiku (re-rate) — controlled inter-coder", sonnet, haiku, lines)
    compare("Sonnet (re-rate) vs Production labels — drift/stability", sonnet, prod, lines)
    compare("Haiku (re-rate) vs Production labels", haiku, prod, lines)

    OUT.write_text("".join(lines))
    print(f"wrote {OUT}")
    print("".join(lines))


if __name__ == "__main__":
    main()
