"""Friday commits: run the sealed rule on the kernel's commits. Prints and writes results.json.

    git clone --bare --filter=tree:0 --single-branch --no-tags \
        https://github.com/torvalds/linux ~/data/friday-commits/linux.git
    python -B research/friday-commits/run.py
"""

import json
import os
import subprocess
import sys
from datetime import datetime, timedelta, timezone

import numpy as np

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import rule as r

REPO_DIR = os.path.expanduser("~/data/friday-commits/linux.git")
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "results.json")
FMT = "%H%x00%ae%x00%ad%x00%ct%x00%s%x00%b%x01"


def read_commits():
    cmd = ["git", "-C", REPO_DIR, "log", "--no-merges", "--date=raw", f"--format={FMT}", r.HEAD_SHA]
    raw = subprocess.run(cmd, capture_output=True, check=True).stdout.decode("utf-8", "replace")
    rows = []
    for rec in raw.split("\x01"):
        rec = rec.lstrip("\n")
        if not rec:
            continue
        sha, email, ad, ct, subject, body = rec.split("\x00", 5)
        ts, off = ad.split()
        sign = -1 if off[0] == "-" else 1
        rows.append((sha, email.lower(), int(ts), sign * (int(off[1:3]) * 60 + int(off[3:5])), int(ct), subject, body))
    return rows


def first_word(subject):
    return subject.split(":", 1)[0].strip().lower() if ":" in subject else ""


def main():
    rows = read_commits()
    print("non-merge commits reachable:", len(rows))
    index = {row[0]: i for i, row in enumerate(rows)}
    resolve = r.resolver(index)

    # Every (target index, fix index) pair from the Fixes lines, in the whole history.
    links, lines, resolved, self_links = [], 0, 0, 0
    carries = np.zeros(len(rows), bool)
    for i, row in enumerate(rows):
        for prefix in r.FIXES_RE.findall(row[6]):
            lines += 1
            carries[i] = True
            sha = resolve(prefix)
            if sha is None:
                continue
            resolved += 1
            j = index[sha]
            if j == i:
                self_links += 1
                continue
            links.append((j, i))
    print("Fixes lines:", lines, "resolved:", resolved, "self:", self_links)

    keep, days, hours, years = [], [], [], []
    for i, (_, _, ts, off, ct, _, _) in enumerate(rows):
        if abs(off) > r.MAX_OFFSET_MIN or abs(ts - ct) > r.MAX_SKEW_S:
            continue
        local = datetime.fromtimestamp(ts, timezone.utc) + timedelta(minutes=off)
        if r.Y0 <= local.year <= r.Y1:
            keep.append(i)
            days.append(local.weekday())
            hours.append(local.hour)
            years.append(local.year)
    keep = np.array(keep)
    days, hours, years = np.array(days), np.array(hours), np.array(years)
    pos = {int(i): k for k, i in enumerate(keep)}
    n = len(keep)
    ct = np.array([rows[int(i)][4] for i in keep])
    offs = np.array([rows[int(i)][3] for i in keep])
    print("frame:", n)

    def outcome(limit, other_author=False):
        out = np.zeros(n, bool)
        for j, i in links:
            k = pos.get(j)
            if k is None:
                continue
            dt = rows[i][4] - ct[k]
            if 0 <= dt <= limit and (not other_author or rows[i][1] != rows[j][1]):
                out[k] = True
        return out

    fixed = outcome(r.FOLLOW_S)
    authors = {}
    author = np.array([authors.setdefault(rows[int(i)][1], len(authors)) for i in keep])
    strata = {}
    stratum = np.array([strata.setdefault((int(a), int(y)), len(strata)) for a, y in zip(author, years)])
    friday = days == r.FRIDAY
    work = np.isin(days, r.WORKDAYS)
    print("authors:", len(authors), "strata:", len(strata))

    def est(fx, ex, ctl, st=stratum, au=author):
        a, A, B = r.mh_terms(st, au, fx, ex, ctl)
        return a, A, B, r.rr_of(A, B)

    a, A, B, rr = est(fixed, friday, work)
    se_boot = r.bootstrap_se(a, A, B, r.DRAWS, r.SEED)
    placebos = {}
    for d in r.WORKDAYS:
        others = np.isin(days, [x for x in r.WORKDAYS if x != d])
        placebos[d] = est(fixed, days == d, others)[3]
    sd_pl = float(np.std(np.log(list(placebos.values())), ddof=1))
    se = max(se_boot, sd_pl)
    verdict, lo, hi = r.verdict(rr, se)

    compared = int((friday | work).sum())
    fixed_compared = int((fixed & (friday | work)).sum())
    weekend = float(np.isin(days, [5, 6]).mean())
    h916 = int(((hours >= 9) & (hours <= 16)).sum())
    h14 = int(((hours >= 1) & (hours <= 4)).sum())

    plants = []
    for k in range(r.PLANTS):
        fx = r.plant(fixed, friday, r.SEED + 1 + k)
        pa, pA, pB, prr = est(fx, friday, work)
        pse = max(r.bootstrap_se(pa, pA, pB, r.PLANT_DRAWS, r.SEED + 100 + k), sd_pl)
        pv, plo, phi = r.verdict(prr, pse)
        plants.append({"rr": prr, "lo": plo, "hi": phi, "verdict": pv})
    planted_ok = sum(p["verdict"] == "survived" for p in plants)

    head_ct = int(subprocess.run(["git", "-C", REPO_DIR, "log", "-1", "--format=%ct", r.HEAD_SHA],
                                 capture_output=True, check=True, text=True).stdout)
    gates = {
        "V1": {"value": 2.8 * se, "limit": float(np.log(r.BAR)), "fail": 2.8 * se >= np.log(r.BAR)},
        "V2": {"compared": compared, "fixed": fixed_compared, "fail": compared < 300_000 or fixed_compared < 10_000},
        "V3": {"share": resolved / max(lines, 1), "fail": resolved / max(lines, 1) < 0.80},
        "V4": {"weekend": weekend, "h9_16": h916, "h1_4": h14, "fail": weekend >= 0.20 or h916 < 3 * h14},
        "V5": {"survived": planted_ok, "of": r.PLANTS, "fail": planted_ok < 8},
        "V6": {"head_ct": head_ct, "fail": head_ct < int(datetime(2026, 1, 1, tzinfo=timezone.utc).timestamp())},
    }
    for g in gates.values():
        g["fail"] = bool(g["fail"])
    void = any(g["fail"] for g in gates.values())
    final = "void" if void else verdict

    # Printed only.
    printed = {}
    names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
    printed["weekday"] = {names[d]: {"commits": int((days == d).sum()), "fixed_rate": float(fixed[days == d].mean()),
                                     "carries_fixes": float(carries[keep][days == d].mean())} for d in range(7)}
    printed["crude_rr"] = float(fixed[friday].mean() / fixed[work].mean())
    printed["sat_sun_vs_work"] = est(fixed, np.isin(days, [5, 6]), work)[3]
    printed["night_0_4_vs_9_16"] = est(fixed, hours <= 4, (hours >= 9) & (hours <= 16))[3]
    # Added after the verdict, not in the sealed list: crude rates and an interval for the night.
    night, day = hours <= 4, (hours >= 9) & (hours <= 16)
    weekend_days = np.isin(days, [5, 6])
    printed["after_verdict"] = {
        "night_commits": int(night.sum()),
        "night_rate": float(fixed[night].mean()),
        "day_rate": float(fixed[day].mean()),
        "night_crude_rr": float(fixed[night].mean() / fixed[day].mean()),
        "night_se_boot": r.bootstrap_se(*est(fixed, night, day)[:3], r.DRAWS, r.SEED + 7),
        "weekend_crude_rr": float(fixed[weekend_days].mean() / fixed[work].mean()),
    }
    nz = offs != 0
    printed["no_plus0000"] = est(fixed, friday & nz, work & nz)[3]
    printed["follow_365"] = est(outcome(365 * 86400), friday, work)[3]
    words = {}
    w = [words.setdefault((int(s), first_word(rows[int(i)][5])), len(words)) for i, s in zip(keep, stratum)]
    printed["strata_with_subject_word"] = est(fixed, friday, work, st=np.array(w))[3]
    for y0, y1 in ((2014, 2018), (2019, 2023)):
        m = (years >= y0) & (years <= y1)
        printed[f"era_{y0}_{y1}"] = est(fixed, friday & m, work & m)[3]
    plain = ~carries[keep]
    printed["no_fixes_line_itself"] = est(fixed, friday & plain, work & plain)[3]
    printed["fixes_by_another_author"] = est(outcome(r.FOLLOW_S, other_author=True), friday, work)[3]

    res = {
        "head": r.HEAD_SHA,
        "reachable": len(rows),
        "fixes_lines": lines,
        "resolved": resolved,
        "frame": n,
        "compared": compared,
        "fixed_compared": fixed_compared,
        "authors": len(authors),
        "strata_used": len(A),
        "rr": rr,
        "se_boot": se_boot,
        "placebos": {names[d]: v for d, v in placebos.items()},
        "sd_placebos": sd_pl,
        "se": se,
        "lo": lo,
        "hi": hi,
        "rule_verdict": verdict,
        "final": final,
        "gates": gates,
        "plants": plants,
        "printed_only": printed,
    }
    with open(OUT, "w") as f:
        json.dump(res, f, indent=1)
    print(json.dumps({k: v for k, v in res.items() if k != "plants"}, indent=1))


if __name__ == "__main__":
    main()
