#!/usr/bin/env python3
"""Exploratory test of the user-specified worsening-by-leadership claim."""
from __future__ import annotations

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

import numpy as np

ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "research-output"
AUTO = OUT / "article-coding.csv"
MANUAL = OUT / "university-manual-audit.csv"
RESULT = OUT / "leadership-exploratory-results.json"
REPS = 10000
SEED = 1502026

STAGES = [
    ("encampment_peak", date(2024, 4, 17), date(2024, 5, 31), "Encampment/NYPD peak"),
    ("post_encampment_148", date(2024, 6, 1), date(2024, 12, 10), "Later 148th board"),
    ("149th", date(2024, 12, 11), date(2025, 12, 9), "149th board"),
    ("150th", date(2025, 12, 10), date(2026, 8, 10), "150th board through cutoff"),
]


def read(path):
    with path.open(newline="", encoding="utf-8") as stream:
        return list(csv.DictReader(stream))


def stage(value: str) -> str:
    d = date.fromisoformat(value)
    return next((key for key, start, end, _ in STAGES if start <= d <= end), "outside_sequence")


def number(value: str):
    try:
        parsed = float(value)
        return parsed if math.isfinite(parsed) else None
    except (TypeError, ValueError):
        return None


def boot(values, seed):
    a = np.asarray([v for v in values if v is not None], dtype=float)
    if not len(a):
        return {"n": 0, "estimate": None, "ci95": [None, None]}
    rng = np.random.default_rng(seed)
    draws = rng.choice(a, (REPS, len(a)), replace=True).mean(axis=1)
    return {"n": len(a), "estimate": float(a.mean()), "ci95": [float(np.quantile(draws, .025)), float(np.quantile(draws, .975))], "method": f"within_stage_article_bootstrap_{REPS}_reps"}


def contrast(current, early, seed):
    a = np.asarray([v for v in current if v is not None], dtype=float)
    b = np.asarray([v for v in early if v is not None], dtype=float)
    if not len(a) or not len(b):
        return {"n_current": len(a), "n_encampment": len(b), "estimate": None, "ci95": [None, None]}
    rng = np.random.default_rng(seed)
    draws = rng.choice(a, (REPS, len(a)), replace=True).mean(axis=1) - rng.choice(b, (REPS, len(b)), replace=True).mean(axis=1)
    return {"n_current": len(a), "n_encampment": len(b), "estimate": float(a.mean() - b.mean()), "ci95": [float(np.quantile(draws, .025)), float(np.quantile(draws, .975))], "method": f"two_group_article_bootstrap_{REPS}_reps"}


def manual_values(row):
    if not (row["admin_present"] == row["challenger_present"] == "1"):
        return {}
    out = {}
    if row["admin_skepticism"] != "" and row["challenger_skepticism"] != "":
        out["scrutiny_gap"] = float(row["admin_skepticism"]) - float(row["challenger_skepticism"])
    if row["admin_tone"] != "" and row["challenger_tone"] != "":
        out["tone_gap"] = float(row["admin_tone"]) - float(row["challenger_tone"])
    if row["admin_headline_frame"] not in {"not_present", "unclear"} and row["challenger_headline_frame"] not in {"not_present", "unclear"}:
        out["adverse_headline_gap"] = float(row["admin_headline_frame"] == "adverse") - float(row["challenger_headline_frame"] == "adverse")
    if row["admin_lede_frame"] not in {"not_present", "unclear"} and row["challenger_lede_frame"] not in {"not_present", "unclear"}:
        out["adverse_lede_gap"] = float(row["admin_lede_frame"] == "adverse") - float(row["challenger_lede_frame"] == "adverse")
    out["first_quote_gap"] = float(row["first_quoted_actor"] == "administration") - float(row["first_quoted_actor"] == "challenger")
    return out


def monotonic(values, direction):
    if any(v is None for v in values):
        return None
    return all(values[i] <= values[i + 1] for i in range(len(values) - 1)) if direction == "increasing" else all(values[i] >= values[i + 1] for i in range(len(values) - 1))


def main():
    auto, manual = read(AUTO), read(MANUAL)
    auto_defs = {
        "scrutiny_gap": ("paired_scrutiny_gap_auto", "decreasing", "Lower means less scrutiny of administration; a decreasing sequence supports the claim."),
        "marked_framing_gap": ("paired_framing_gap_auto", "decreasing", "Lower means less adverse/marked framing of administration; a decreasing sequence supports the claim."),
        "first_quote_gap": ("first_quote_gap_auto", "increasing", "Higher means administration is quoted first more often; an increasing sequence supports the claim."),
        "quote_word_share_gap": ("direct_quote_word_share_gap_auto", "increasing", "Higher means a larger administration share of attributed quotation words; an increasing sequence supports the claim."),
    }
    manual_defs = {
        "scrutiny_gap": ("decreasing", "Lower means less manual skepticism toward administration."),
        "tone_gap": ("increasing", "Higher means more favorable manual tone toward administration."),
        "adverse_headline_gap": ("decreasing", "Lower means less adverse headline framing of administration."),
        "adverse_lede_gap": ("decreasing", "Lower means less adverse lede framing of administration."),
        "first_quote_gap": ("increasing", "Higher means administration is quoted first more often."),
    }
    auto_stage, manual_stage = {}, {}
    auto_vectors, manual_vectors = {}, {}
    for sidx, (key, start, end, label) in enumerate(STAGES):
        ars = [r for r in auto if stage(r["date"]) == key and r["paired_actor_article_strict"] == "1"]
        mrs = [r for r in manual if stage(r["date"]) == key]
        auto_stage[key] = {"label": label, "start": str(start), "end": str(end), "paired_n": len(ars), "topic_counts": dict(Counter(r["primary_topic"] for r in ars)), "outcomes": {}}
        manual_stage[key] = {"label": label, "sample_n": len(mrs), "paired_n": sum(bool(manual_values(r)) for r in mrs), "outcomes": {}}
        for midx, (measure, (field, _, _)) in enumerate(auto_defs.items()):
            vals = [number(r[field]) for r in ars]
            vals = [v for v in vals if v is not None]
            auto_vectors.setdefault(measure, {})[key] = vals
            auto_stage[key]["outcomes"][measure] = boot(vals, SEED + sidx * 100 + midx)
        for midx, measure in enumerate(manual_defs):
            vals = [manual_values(r).get(measure) for r in mrs]
            vals = [v for v in vals if v is not None]
            manual_vectors.setdefault(measure, {})[key] = vals
            manual_stage[key]["outcomes"][measure] = boot(vals, SEED + 1000 + sidx * 100 + midx)
    sequence = [key for key, *_ in STAGES]
    tests = {"automated_screening": {}, "manual_diversity_sample": {}}
    for idx, (measure, (_, expected, meaning)) in enumerate(auto_defs.items()):
        estimates = [auto_stage[key]["outcomes"][measure]["estimate"] for key in sequence]
        tests["automated_screening"][measure] = {
            "stage_estimates": dict(zip(sequence, estimates)),
            "pro_admin_expected_direction": expected,
            "monotonic_in_expected_direction": monotonic(estimates, expected),
            "current_minus_encampment": contrast(auto_vectors[measure]["150th"], auto_vectors[measure]["encampment_peak"], SEED + 2000 + idx),
            "meaning": meaning,
            "status": "suppressed_unvalidated_automated_semantic_screen",
        }
    for idx, (measure, (expected, meaning)) in enumerate(manual_defs.items()):
        estimates = [manual_stage[key]["outcomes"][measure]["estimate"] for key in sequence]
        tests["manual_diversity_sample"][measure] = {
            "stage_estimates": dict(zip(sequence, estimates)),
            "pro_admin_expected_direction": expected,
            "monotonic_in_expected_direction": monotonic(estimates, expected),
            "current_minus_encampment": contrast(manual_vectors[measure]["150th"], manual_vectors[measure]["encampment_peak"], SEED + 3000 + idx),
            "meaning": meaning,
            "status": "exploratory_nonrepresentative_manual_sample",
        }
    payload = {
        "status": "post_request_exploratory_not_preregistered",
        "claim_tested": "Coverage was least pro-administration during the April-May 2024 encampment peak and became progressively more pro-administration, especially during the 150th board under editor in chief Tsehai Alfred and University News co-editor Spencer Davis.",
        "causal_caveat": "Leadership periods organize time only. No assignment records or direct evidence connect either named editor to an article-level decision, and topic/event composition changes sharply across periods.",
        "right_censoring": "The 150th period runs only from 2025-12-10 through the 2026-08-10 cutoff; it is not a complete board term.",
        "sequence": sequence,
        "automated_stages": auto_stage,
        "manual_stages": manual_stage,
        "trend_tests": tests,
        "overall_assessment_rule": "Call the progressive-worsening claim supported only if substantively related, reliable measures move monotonically in the pro-administration direction and the current-minus-encampment contrast shares that direction. No automated semantic measure passed the preregistered reliability gate; manual estimates are nonrepresentative.",
    }
    RESULT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({
        "auto_monotonic_support": [k for k, v in tests["automated_screening"].items() if v["monotonic_in_expected_direction"]],
        "manual_monotonic_support": [k for k, v in tests["manual_diversity_sample"].items() if v["monotonic_in_expected_direction"]],
        "output": str(RESULT),
    }, indent=2))


if __name__ == "__main__":
    main()
