"""Score the sealed rule against the fetched releases.

    python analyse.py > results.txt

Reads ~/data/friday-releases/releases.csv (written by fetch.py) and applies
rule.py exactly: the filters, the two claims, their bars, the negative
control and the injection floor. Nothing here chooses a threshold; every
threshold is imported from rule.py, which was sealed before the fetch.
"""

import collections
import csv
import datetime as dt
import math
import os
import pathlib
import random

from packaging.version import InvalidVersion, Version

import rule

CSV = pathlib.Path(os.path.expanduser("~/data/friday-releases/releases.csv"))
START = dt.datetime(2016, 1, 1, tzinfo=dt.timezone.utc)
END = dt.datetime(2026, 9, 1, tzinfo=dt.timezone.utc)
DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]


def parse_time(s):
    return dt.datetime.fromisoformat(s.replace("Z", "+00:00"))


def load():
    drops = collections.Counter()
    rows = []
    with CSV.open() as fh:
        for r in csv.DictReader(fh):
            drops["rows read"] += 1
            if not r["first_upload_iso"]:
                drops["no file (deleted release)"] += 1
                continue
            try:
                v = Version(r["version"])
            except InvalidVersion:
                drops["version unparseable"] += 1
                continue
            if v.is_prerelease:
                drops["pre-release"] += 1
                continue
            t = parse_time(r["first_upload_iso"])
            if not (START <= t < END):
                drops["outside 2016..2026-09"] += 1
                continue
            rows.append((r["project"], t, r["yanked"] == "1"))
    drops["kept"] = len(rows)
    return rows, drops


def share_table(rows, offset_h=0.0):
    """Weekday counts under a clock offset, and the Friday share of Mon..Fri."""
    shift = dt.timedelta(hours=offset_h)
    counts = collections.Counter((t + shift).weekday() for _, t, _ in rows)
    weekday_n = sum(counts[d] for d in range(5))
    return counts, counts[4], weekday_n


def follow_ups(rows, window_h):
    """Per release: its weekday, and whether the same project released again
    inside `window_h` hours. Only releases with a full 168 h horizon count."""
    by_project = collections.defaultdict(list)
    for project, t, _ in rows:
        by_project[project].append(t)
    horizon = END - dt.timedelta(hours=rule.HORIZON_H)
    out = []
    for project, times in by_project.items():
        times.sort()
        for i, t in enumerate(times):
            if t > horizon:
                continue
            nxt = times[i + 1] if i + 1 < len(times) else None
            hit = nxt is not None and (nxt - t) <= dt.timedelta(hours=window_h)
            out.append((t.weekday(), hit))
    return out


def rr_for(pairs, day, against):
    k1 = sum(1 for d, hit in pairs if d == day and hit)
    n1 = sum(1 for d, _ in pairs if d == day)
    k2 = sum(1 for d, hit in pairs if d in against and hit)
    n2 = sum(1 for d, _ in pairs if d in against)
    return (k1, n1, k2, n2) + rule.rr_ci(k1, n1, k2, n2)


def injection_floor(k1, n1, k2, n2, trials=400):
    """The smallest true relative risk this rule confirms 80 % of the time."""
    p1, p2 = k1 / n1, k2 / n2
    rng = random.Random(66)
    out = []
    for r in (1.05, 1.10, 1.15, 1.20, 1.30):
        target = min(1.0, r * p2)
        wins = 0
        for _ in range(trials):
            a = sum(1 for _ in range(n1) if rng.random() < target)
            b = sum(1 for _ in range(n2) if rng.random() < p2)
            _, lo, _ = rule.rr_ci(a, n1, b, n2)
            if lo == lo and lo > rule.B_SURVIVE_LO:
                wins += 1
        out.append((r, wins / trials))
    return out, p1, p2


def main():
    rows, drops = load()
    print("=" * 72)
    print("FRIDAY RELEASES — the sealed rule scored")
    print("rule.py sha256 ca7ccbdb5f1ab66e99c88396ab284f817c8448bdd82918f782310c79c145b822")
    print("seal rule.friday-releases at 1789585933404 ms, 2026-09-16 21:12 Rome")
    print("=" * 72)

    print("\nFILTERS (registered: every drop is published)")
    for k in ("rows read", "no file (deleted release)", "version unparseable",
              "pre-release", "outside 2016..2026-09", "kept"):
        print(f"  {k:<32} {drops[k]:>8,}")
    print(f"  {'distinct projects kept':<32} {len({p for p, _, _ in rows}):>8,}")

    # ---------------------------------------------------------------- claim A
    counts, fri, weekday_n = share_table(rows)
    lo, hi = rule.wilson(fri, weekday_n)
    p = fri / weekday_n
    print("\n" + "-" * 72)
    print("CLAIM A — 'nobody ships on a Friday'")
    print("-" * 72)
    print("  weekday histogram (UTC), all kept releases:")
    total = sum(counts.values())
    for d in range(7):
        print(f"    {DAYS[d]}  {counts[d]:>7,}  {counts[d]/total:6.2%}")
    print(f"\n  Friday share of Mon..Fri   {p:.4f}   95% CI [{lo:.4f}, {hi:.4f}]"
          f"   n = {weekday_n:,}")
    print(f"  flat week would be         0.2000")
    print(f"  bars (sealed): survived hi < {rule.A_SURVIVE_HI}, "
          f"killed lo > {rule.A_KILL_LO}")
    va = rule.verdict_a(lo, hi)
    print(f"  VERDICT (pooled): {va.upper()}")
    print(f"  relative dip vs a flat week: {(0.2 - p) / 0.2:+.1%}")

    # per-project vote
    per = collections.defaultdict(lambda: [0, 0])
    for project, t, _ in rows:
        if t.weekday() < 5:
            per[project][1] += 1
            if t.weekday() == 4:
                per[project][0] += 1
    shares = sorted(f / n for f, n in per.values() if n >= 20)
    med = shares[len(shares) // 2] if shares else float("nan")
    print(f"\n  one vote per project ({len(shares):,} projects with >= 20 weekday releases)")
    print(f"    median project Friday share  {med:.4f}")
    q1, q3 = shares[len(shares) // 4], shares[3 * len(shares) // 4]
    print(f"    quartiles                    {q1:.4f} .. {q3:.4f}")
    same_side = (med < rule.A_KILL_LO) == (p < rule.A_KILL_LO)
    print(f"    agrees with the pooled side of {rule.A_KILL_LO}: {'yes' if same_side else 'NO'}")
    if not same_side:
        va = "inconclusive"
        print("    sealed consequence: the two disagree, so CLAIM A IS INCONCLUSIVE")

    print("\n  the same share under six clocks (reported, not scored):")
    for off in rule.CLOCK_OFFSETS_H:
        _, f2, n2 = share_table(rows, off)
        print(f"    UTC{off:+.1f}   {f2/n2:.4f}")

    print("\n  Friday share of Mon..Fri by year (reported, not scored):")
    by_year = collections.defaultdict(lambda: [0, 0])
    for _, t, _ in rows:
        if t.weekday() < 5:
            by_year[t.year][1] += 1
            if t.weekday() == 4:
                by_year[t.year][0] += 1
    for y in sorted(by_year):
        f, n = by_year[y]
        print(f"    {y}  {f/n:.4f}   n = {n:,}")

    # ---------------------------------------------------------------- claim B
    print("\n" + "-" * 72)
    print("CLAIM B — 'Friday releases break'  (proxy: another release soon after)")
    print("-" * 72)
    pairs = follow_ups(rows, rule.PRIMARY_WINDOW_H)
    print(f"  eligible releases (full {rule.HORIZON_H} h horizon): {len(pairs):,}")
    print(f"\n  follow-up within {rule.PRIMARY_WINDOW_H} h, by day (levels, reported):")
    for d in range(7):
        n = sum(1 for dd, _ in pairs if dd == d)
        k = sum(1 for dd, hit in pairs if dd == d and hit)
        print(f"    {DAYS[d]}  {k:>6,} / {n:>6,}  {k/n:6.2%}")

    k1, n1, k2, n2, rr, blo, bhi = rr_for(pairs, 4, (0, 1, 2, 3))
    print(f"\n  Friday {k1:,}/{n1:,} = {k1/n1:.4f}   Mon..Thu {k2:,}/{n2:,} = {k2/n2:.4f}")
    print(f"  RR = {rr:.4f}   95% CI [{blo:.4f}, {bhi:.4f}]")
    print(f"  bars (sealed): survived lo > {rule.B_SURVIVE_LO}; "
          f"killed CI inside {rule.B_KILL_BAND}; "
          f"floor needs Friday n >= {rule.B_FLOOR_MIN_FRIDAY_N:,}")
    vb = rule.verdict_b(blo, bhi, n1)
    print(f"  VERDICT: {vb.upper()}")

    print("\n  registered robustness windows (not scored):")
    for w in rule.ROBUSTNESS_WINDOWS_H:
        pw = follow_ups(rows, w)
        a, b, c, d2, r2, l2, h2 = rr_for(pw, 4, (0, 1, 2, 3))
        print(f"    {w:>3} h   Fri {a/b:.4f}  Mon..Thu {c/d2:.4f}  "
              f"RR {r2:.4f}  [{l2:.4f}, {h2:.4f}]")

    print("\n  NEGATIVE CONTROL — Tuesday against Mon/Wed/Thu (sealed, scored):")
    tk1, tn1, tk2, tn2, trr, tlo, thi = rr_for(pairs, 1, (0, 2, 3))
    print(f"    RR = {trr:.4f}   95% CI [{tlo:.4f}, {thi:.4f}]")
    clean = rule.CONTROL_TOLERANCE[0] <= trr <= rule.CONTROL_TOLERANCE[1] or (tlo < 1 < thi)
    print(f"    within the sealed tolerance {rule.CONTROL_TOLERANCE} or CI covering 1: "
          f"{'yes' if clean else 'NO — claim B is VOID by the sealed rule'}")

    print("\n  POSITIVE CONTROL — injection floor (sealed):")
    grid, p1, p2 = injection_floor(k1, n1, k2, n2)
    for r, power in grid:
        print(f"    true RR {r:.2f}   confirmed in {power:6.1%} of 400 runs")
    caught = [r for r, power in grid if power >= 0.80]
    print(f"    smallest true RR this rule catches 80 % of the time: "
          f"{min(caught) if caught else '> 1.30'}")

    # ------------------------------------------------------- reported extras
    print("\n" + "-" * 72)
    print("REPORTED, registered, not scored")
    print("-" * 72)
    hours = collections.Counter(t.hour for _, t, _ in rows)
    peak = max(hours, key=hours.get)
    print(f"  hour of day (UTC): peak {peak:02d}:00 with {hours[peak]/len(rows):.2%}, "
          f"quietest {min(hours, key=hours.get):02d}:00 with {min(hours.values())/len(rows):.2%}")
    yank = collections.defaultdict(lambda: [0, 0])
    for _, t, y in rows:
        yank[t.weekday()][1] += 1
        if y:
            yank[t.weekday()][0] += 1
    print("  yank rate by weekday:")
    for d in range(7):
        k, n = yank[d]
        print(f"    {DAYS[d]}  {k:>5,} / {n:>7,}  {k/n:.3%}")

    print("\n" + "=" * 72)
    print(f"CLAIM A: {va.upper()}    CLAIM B: {vb.upper()}")
    print("=" * 72)


if __name__ == "__main__":
    main()
