"""
money_analysis.py  —  the A-block estimator, saved.

Written 2026-08-23 after the published headline (pooled r=0.82 / incumbents-only
r=0.70) failed to reproduce from the frozen artifacts. Root cause of that failure:
the figure was computed in an ad-hoc session and never committed to a script, so it
could not be re-run when the district universe expanded. Same failure mode as the
ALL-CAPS feature. This file exists so it cannot happen a third time.

Everything here implements ANALYSIS_SPEC_v1.md §0 and §1 exactly:
  X = tenure (cumulative elected House+Senate years through 2026-11-03), AGE_TENURE
  Y = 100 * (Corporate PAC + Trade assoc PAC) / total receipts   [Corp_PAC_Pct_Receipts]
  incumbent := tenure > 0
  viability filter: receipts < 25000 AND disbursements < 25000 -> excluded
                    (disbursements are not carried in the frozen CSV; the receipts
                     leg is applied and the gap is DECLARED, not hidden)
  no outlier removal, ever.

Run:  python3 scripts/money_analysis.py
"""
import csv, math, os, sys
import numpy as np
from scipy import stats as sps

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import phase2  # noqa: E402

# Single source of truth for the input. In the repo it is data/finance_nominees.csv;
# in the published bundle everything sits flat beside this script. The build failed its
# own fault-seed gate on 2026-08-23 because a second copy lived in scripts/ and the
# estimator read that one while the bundle shipped the other — the exact two-files
# divergence the pipeline exists to prevent. The scripts/ copy is deleted.
_CANDIDATES = [os.path.join(HERE, "..", "data", "finance_nominees.csv"),
               os.path.join(HERE, "finance_nominees.csv")]
CSV = next((c for c in _CANDIDATES if os.path.exists(c)), _CANDIDATES[0])
VINTAGE = ("receipts per the FEC_Coverage_Through column; most committees report "
           "through 2026-06-30 but the file spans 2026-06-16 to 2026-08-18. "
           "Corrected 2026-08-30: this line used to state a narrower mixed range "
           "that the file itself contradicts.")
RECEIPTS_FLOOR = 25000.0


def load():
    at = phase2.AGE_TENURE
    out = []
    for r in csv.DictReader(open(CSV)):
        name = r["Nominee_Name"]
        if name not in at:
            raise SystemExit("roster mismatch: %r not in AGE_TENURE" % name)
        rec = float(r["Total_Receipts"])
        if rec < RECEIPTS_FLOOR:
            continue  # viability filter, receipts leg
        out.append(dict(
            name=name, district=r["District"], party=r["Party"], tier=r["Tier"],
            inc_code=r["Incumbency"], tenure=float(at[name][1]),
            corp_pct=float(r["Corp_PAC_Pct_Receipts"]), receipts=rec,
        ))
    return out


def block(label, rows):
    x = np.array([d["tenure"] for d in rows], float)
    y = np.array([d["corp_pct"] for d in rows], float)
    r, p = sps.pearsonr(x, y)
    rho, prho = sps.spearmanr(x, y)
    # bootstrap 95% CI on Pearson r, 10k resamples, fixed seed
    rng = np.random.default_rng(20260823)
    bs = []
    for _ in range(10000):
        i = rng.integers(0, len(x), len(x))
        if np.std(x[i]) == 0 or np.std(y[i]) == 0:
            continue
        bs.append(np.corrcoef(x[i], y[i])[0, 1])
    lo, hi = np.percentile(bs, [2.5, 97.5])
    z = math.atanh(max(min(r, 0.999999), -0.999999))
    print(f"{label:34s} n={len(rows):3d}  Pearson r={r:+.4f} (p={p:.2e})  "
          f"boot95=[{lo:+.3f},{hi:+.3f}]  Spearman rho={rho:+.4f}  Fisher z={z:+.4f}")
    return r


def partial_r(rows, ctrl_key):
    x = np.array([d["tenure"] for d in rows], float)
    y = np.array([d["corp_pct"] for d in rows], float)
    c = np.array([d[ctrl_key] for d in rows], float)
    rxy = sps.pearsonr(x, y)[0]
    rxc = sps.pearsonr(x, c)[0]
    ryc = sps.pearsonr(y, c)[0]
    return (rxy - rxc * ryc) / math.sqrt((1 - rxc ** 2) * (1 - ryc ** 2))


if __name__ == "__main__":
    rows = load()
    inc = [d for d in rows if d["tenure"] > 0]
    print("A-BLOCK ESTIMATORS — ANALYSIS_SPEC_v1 §1, vintage:", VINTAGE)
    print("rows after viability filter: %d (incumbents %d / non-incumbents %d)\n"
          % (len(rows), len(inc), len(rows) - len(inc)))

    print("--- A1a PRIMARY: incumbents only (tenure > 0) ---")
    r_inc = block("incumbents-only", inc)
    print("    threshold as registered: r >= 0.60  ->  %s"
          % ("HIT" if round(r_inc, 2) >= 0.60 else "MISS"))

    print("\n--- A1b SECONDARY: pooled, contains the incumbent/challenger contrast ---")
    r_pool = block("pooled (all candidates)", rows)
    signs = {}
    for p in ("DEM", "REP"):
        sub = [d for d in rows if d["party"] == p]
        signs[p] = block(f"  pooled within {p}", sub)
    ok = (0.70 <= round(r_pool, 2) <= 0.90) and signs["DEM"] > 0 and signs["REP"] > 0
    print("    threshold as registered: 0.70 <= r <= 0.90 and both party signs > 0  ->  %s"
          % ("HIT" if ok else "MISS"))

    print("\n--- context: within-party, incumbents only ---")
    for p in ("DEM", "REP"):
        block(f"  incumbents {p}", [d for d in inc if d["party"] == p])

    print("\n--- context: zero-inflation, the reviewer's charge ---")
    ch = [d for d in rows if d["tenure"] == 0]
    print("    non-incumbents at tenure 0: n=%d, mean corp share %.2f%%"
          % (len(ch), np.mean([d["corp_pct"] for d in ch])))
    print("    incumbents:                 n=%d, mean corp share %.2f%%"
          % (len(inc), np.mean([d["corp_pct"] for d in inc])))
    print("    -> the pooled figure is largely this two-group gap, exactly as charged.")

    print("\n--- context: leverage, DECLARED not removed ---")
    for d in sorted(inc, key=lambda d: -d["tenure"])[:3]:
        print("    %-22s %-6s tenure %5.1f  corp share %6.2f%%"
              % (d["name"], d["district"], d["tenure"], d["corp_pct"]))
