#!/usr/bin/env python3
"""Summarises the raw results in ../results into ../results/summary.json.

Standard library only. Percentiles use linear interpolation between closest
ranks (the 'linear' method, numpy's default). Confidence intervals are
two-sided 95% Student-t intervals on the mean of independent runs.
"""
import glob
import json
import math
import os
import re
import statistics as st

HERE = os.path.dirname(os.path.abspath(__file__))
RES = os.path.join(HERE, "..", "results")
T95 = {1: 12.706, 2: 4.303, 3: 3.182, 4: 2.776, 5: 2.571, 6: 2.447, 7: 2.365, 8: 2.306, 9: 2.262, 10: 2.228,
       14: 2.145, 15: 2.131, 29: 2.045, 30: 2.042}


def pct(xs, p):
    xs = sorted(xs)
    if len(xs) == 1:
        return xs[0]
    k = (len(xs) - 1) * p / 100
    lo, hi = math.floor(k), math.ceil(k)
    return xs[lo] + (xs[hi] - xs[lo]) * (k - lo)


def ci(xs):
    m = st.mean(xs)
    if len(xs) < 2:
        return {"mean": m, "ci95": None, "n": len(xs)}
    h = T95.get(len(xs) - 1, 1.96) * st.stdev(xs) / math.sqrt(len(xs))
    return {"mean": m, "ci95": h, "lo": m - h, "hi": m + h, "sd": st.stdev(xs), "n": len(xs)}


def jl(path):
    with open(path) as f:
        return [json.loads(line) for line in f if line.strip().startswith("{")]


def dist(xs):
    return {"n": len(xs), "p50": pct(xs, 50), "p95": pct(xs, 95), "min": min(xs), "max": max(xs), "mean": st.mean(xs)}


out = {}

# Experiment 1
e1 = {}
for path in sorted(glob.glob(os.path.join(RES, "exp1_lockscope_*.jsonl"))):
    rows = jl(path)
    v = rows[0]["variant"]
    e1[v] = {
        "hold_ms": rows[0]["hold_ms"],
        "same_tenant_ms": dist([r["same_tenant_ms"] for r in rows]),
        "other_tenant_ms": dist([r["other_tenant_ms"] for r in rows]),
        "holder_commit_ms": dist([r["holder_commit_ms"] for r in rows]),
    }
out["exp1_lockscope"] = e1

# Experiment 4
e4 = {}
for kind in ("interleave", "stress"):
    for path in sorted(glob.glob(os.path.join(RES, f"exp4_{kind}_*.jsonl"))):
        rows = jl(path)
        v = rows[0]["variant"]
        trials = [r for r in rows if not r.get("summary")]
        e4.setdefault(v, {})[kind] = {
            "trials": len(trials),
            "chains_failing_verification": sum(1 for r in trials if not r["ok"]),
            "trials_with_shared_prev_hash": sum(1 for r in trials if r["shared_prev"] > 0),
            "trials_with_more_than_one_tip": sum(1 for r in trials if r["tips"] != 1),
            "trials_with_writer_errors": sum(1 for r in trials if r.get("writer_errors") or r.get("error")),
            "rows_per_trial": dist([r["rows"] for r in trials]),
            "breaks_per_trial": dist([r["breaks"] for r in trials]),
        }
    for path in sorted(glob.glob(os.path.join(RES, "exp4_tamper_*.jsonl"))):
        rows = jl(path)
        v = rows[0]["variant"]
        e4.setdefault(v, {})["tamper"] = [
            {"case": r["case"], "result": r.get("result"),
             "detected": (not r["after"]["ok"]) if "after" in r else None,
             "clean_before": r["before"]["ok"] if "before" in r else None,
             "after": r.get("after") or r.get("chain")}
            for r in rows]
out["exp4_audit_chain"] = e4

# Experiment 2a: per-query server execution time
e2q = {}
for path in sorted(p for p in glob.glob(os.path.join(RES, "exp2_query_*.jsonl")) if "aborted" not in p):
    rows = jl(path)
    if not rows:
        continue
    scale = os.path.basename(path).split("_")[-1].split(".")[0]
    for q in ("club", "discovery"):
        rs = [r for r in rows if r["query"] == q]
        if not rs:
            continue
        rl = [r["rls"]["exec_ms"] for r in rs]
        ow = [r["owner_filter"]["exec_ms"] for r in rs]
        ratios = [a / b for a, b in zip(rl, ow) if b > 0]
        e2q.setdefault(scale, {})[q] = {
            "measurements": len(rs),
            "rls_exec_ms": dist(rl),
            "owner_filter_exec_ms": dist(ow),
            "ratio_median": pct(ratios, 50),
            "rows_equal_fraction": sum(1 for r in rs if r["rls"]["rows"] == r["owner_filter"]["rows"]) / len(rs),
            "rls_rows_mean": st.mean(r["rls"]["rows"] for r in rs),
            "owner_rows_mean": st.mean(r["owner_filter"]["rows"] for r in rs),
        }
out["exp2_query_timing"] = e2q

# Experiment 2b: pgbench
e2b = {}
for path in sorted(glob.glob(os.path.join(RES, "pgbench", "*.txt"))):
    m = re.match(r"(wc_bench_x\d+)_(rls_plus_filter|owner_filter|rls)_run(\d+)\.txt", os.path.basename(path))
    if not m:
        continue
    db, mode, run = m.groups()
    txt = open(path).read()
    tps = re.search(r"^tps = ([\d.]+)", txt, re.M)
    lat = re.search(r"latency average = ([\d.]+) ms", txt)
    fail = re.search(r"number of failed transactions: (\d+)", txt)
    done = re.search(r"number of transactions actually processed: (\d+)", txt)
    stmts = re.findall(r"^\s+([\d.]+)\s+(\d+)\s+(\S.*)$", txt, re.M)
    d = e2b.setdefault(db, {}).setdefault(mode, {"tps": [], "latency_ms": [], "failed": 0, "txns": 0, "stmt": {}})
    if tps:
        d["tps"].append(float(tps.group(1)))
        d["latency_ms"].append(float(lat.group(1)))
        d["failed"] += int(fail.group(1)) if fail else 0
        d["txns"] += int(done.group(1)) if done else 0
        for ms, _f, text in stmts:
            d["stmt"].setdefault(text[:60], []).append(float(ms))
for db, modes in e2b.items():
    for mode, d in modes.items():
        d["tps_ci"] = ci(d["tps"]) if d["tps"] else None
        d["latency_ci"] = ci(d["latency_ms"]) if d["latency_ms"] else None
        d["stmt_mean_ms"] = {k: st.mean(v) for k, v in d["stmt"].items()}
        del d["stmt"]
out["exp2_pgbench"] = e2b

# Experiment 2c: helper overhead
hp = os.path.join(RES, "exp2_helper_overhead.jsonl")
if os.path.exists(hp):
    rows = jl(hp)
    out["exp2_helper_overhead"] = {
        v: {"exec_ms": dist([r["exec_ms"] for r in rows if r["variant"] == v]),
            "count": sorted({r["count"] for r in rows if r["variant"] == v})}
        for v in dict.fromkeys(r["variant"] for r in rows)}

with open(os.path.join(RES, "summary.json"), "w") as f:
    json.dump(out, f, indent=2)
print(json.dumps(out, indent=2)[:6000])
