#!/usr/bin/env python3
"""Oracle and analysis for the rotation differential test.

Oracle: majority / pairwise agreement among the implementations on the pool
index shown at each query instant. There is no independent ground truth; the
Dart app's reconciliation is treated as the reference behaviour because the app
republishes the widget whenever it is opened, so any native answer that differs
from it is a visible jump. Every disagreement is assigned a root-cause class by
explicit, mechanical rules (below) and the residue is reported as 'unexplained'.
"""
import json, sys, glob, os, collections
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo

R = sys.argv[1]
OUT = os.path.join(R, "out")
scen = {s["id"]: s for s in json.load(open(os.path.join(OUT, "scenarios.json")))["scenarios"]}
zones = json.load(open(os.path.join(OUT, "scenarios.json")))["zones"]

def minute_status(zone, q_ms, minute):
    """Is local minute `minute` on q's local date a gap, an overlap or normal?"""
    tz = ZoneInfo(zone)
    d = datetime.fromtimestamp(q_ms / 1000, tz).date()
    a = datetime(d.year, d.month, d.day, minute // 60, minute % 60, tzinfo=tz, fold=0)
    b = a.replace(fold=1)
    ua, ub = a.astimezone(timezone.utc), b.astimezone(timezone.utc)
    if ua.astimezone(tz).replace(tzinfo=None) != a.replace(tzinfo=None):
        return "gap"
    if ua != ub:
        return "overlap"
    return "normal"

tot = collections.Counter()           # (kind) -> queries
agree = collections.Counter()         # (kind, pair) -> agreements
cls = collections.Counter()           # root-cause class -> query count
cls_scen = collections.defaultdict(set)
cls_zone = collections.defaultdict(set)
examples = collections.defaultdict(list)
per_zone = collections.defaultdict(lambda: collections.Counter())
timing = collections.defaultdict(dict)
runtimes = {}
PAIRS = ["app=swift", "app=kotlin", "swift=kotlin", "all3", "cpi=app", "swiftShown=swift", "kotlinShown=kotlin", "swiftShown=kotlinShown"]

for zone in zones:
    f = zone.replace("/", "_")
    d = json.load(open(os.path.join(OUT, f"dart_{f}.json")))
    s = json.load(open(os.path.join(OUT, f"swift_{f}.json")))
    k = json.load(open(os.path.join(OUT, f"kotlin_{f}.json")))
    assert s["tzCurrent"] in (zone, "GMT") and k["tzDefault"] == zone, (zone, s["tzCurrent"], k["tzDefault"])
    runtimes["dart"] = d["runtime"]; runtimes["swift_tz"] = s["tzDataVersion"]; runtimes["kotlin"] = k["runtime"]; runtimes["kotlin_tz"] = k["tzDataVersion"]
    timing[zone] = {"dart_us": d["elapsedMicros"], "swift_us": s["elapsedMicros"], "kotlin_us": k["elapsedMicros"], "queries": d["queries"]}
    assert s["ids"] == [r["id"] for r in d["results"]] == k["ids"]
    for i, r in enumerate(d["results"]):
        sc = scen[r["id"]]
        kind = r["kind"]
        p = r["payload"]
        for j, q in enumerate(r["queries"]):
            a, c = r["dart_app"][j], r["dart_cpi"][j]
            si, ki = s["instant"][i][j], k["instant"][i][j]
            ss, ks = s["shown"][i][j], k["shown"][i][j]
            tot[kind] += 1; tot["ALL"] += 1
            per_zone[zone]["queries"] += 1
            res = {"app=swift": a == si, "app=kotlin": a == ki, "swift=kotlin": si == ki,
                   "all3": a == si == ki, "cpi=app": c == a,
                   "swiftShown=swift": ss < 0 or ss == si, "kotlinShown=kotlin": ks < 0 or ks == ki,
                   "swiftShown=kotlinShown": ss < 0 or ks < 0 or ss == ks}
            for pr, ok in res.items():
                if ok:
                    agree[(kind, pr)] += 1; agree[("ALL", pr)] += 1
            if not res["all3"]:
                per_zone[zone]["instantDisagree"] += 1
            # ---- root-cause rules ----
            labels = []
            if not res["all3"]:
                if si == ki and a != si:
                    t0 = sc["t0"]; I = p["adelante_refresh_interval_seconds"] * 1000
                    if p["adelante_schedule_dates"] == "[]" and I > 0:
                        nb = (t0 // 1000) * 1000; ph_n = (q - nb) % I; ph_a = (q - t0) % I
                        labels.append("D1 sub-second anchor truncation" if ph_n < ph_a else "app-vs-native other (interval)")
                    elif p["adelante_initial_quote_only"]:
                        labels.append("D2 Today-override +24h across DST (app)")
                    else:
                        labels.append("D7 Dart elapsed count vs its own published list (gap)")
                elif a == si and ki != si:
                    st = minute_status(zone, q, sc["minute"]) if sc["minute"] is not None else "n/a"
                    labels.append(f"D3 Kotlin tail boundary ({st})")
                elif a == ki and si != ki:
                    dates = json.loads(p["adelante_schedule_dates"])
                    last_day = datetime.fromtimestamp(dates[-1], ZoneInfo(zone)).date() if dates else None
                    if last_day and minute_status(zone, int(datetime(last_day.year, last_day.month, last_day.day, 12, tzinfo=ZoneInfo(zone)).timestamp() * 1000), 0) == "gap":
                        labels.append("D4b Swift tail day count after a midnight transition")
                    else:
                        st = minute_status(zone, q, sc["minute"]) if sc["minute"] is not None else "n/a"
                        labels.append(f"D4a Swift tail boundary ({st})")
                else:
                    labels.append("three-way disagreement")
            if not res["swiftShown=swift"]:
                st = minute_status(zone, q, sc["minute"]) if sc["minute"] is not None else "n/a"
                labels.append("D5 Swift timeline shows stale entry")
            if not res["kotlinShown=kotlin"]:
                st = minute_status(zone, q, sc["minute"]) if sc["minute"] is not None else "n/a"
                labels.append("D6 Kotlin alarm shows stale render")
            if not res["cpi=app"]:
                labels.append("C currentPoolIndex != app" + (" (daily-time)" if p["adelante_schedule_dates"] != "[]" else " (interval)"))
            for L in labels:
                cls[L] += 1; cls_scen[L].add(r["id"]); cls_zone[L].add(zone)
                if len(examples[L]) < 6:
                    tz = ZoneInfo(zone)
                    examples[L].append({"scenario": r["id"], "zone": zone, "kind": kind, "minute": sc["minute"],
                        "mode": sc["mode"], "n": r["n"], "initialOnly": p["adelante_initial_quote_only"],
                        "t0_local": datetime.fromtimestamp(sc["t0"] / 1000, tz).isoformat(),
                        "q_ms": q, "q_local": datetime.fromtimestamp(q / 1000, tz).isoformat(timespec="milliseconds"),
                        "dart_app": a, "dart_cpi": c, "swift": si, "kotlin": ki, "swift_shown": ss, "kotlin_shown": ks})

summary = {
    "runtimes": runtimes,
    "queries": dict(tot),
    "scenarios": len(scen),
    "agreement": {f"{kd}|{pr}": {"agree": agree[(kd, pr)], "total": tot[kd], "rate": round(agree[(kd, pr)] / tot[kd], 6)} for kd in tot for pr in PAIRS},
    "classes": {L: {"queries": cls[L], "scenarios": len(cls_scen[L]), "zones": sorted(cls_zone[L])} for L in sorted(cls)},
    "examples": examples,
    "perZone": {z: dict(v) for z, v in per_zone.items()},
    "timing": timing,
}
json.dump(summary, open(os.path.join(OUT, "summary.json"), "w"), indent=1)
print(json.dumps({k: summary[k] for k in ("runtimes", "queries", "scenarios")}, indent=1))
for kd in ["ALL", "interval", "daily", "daily-tail", "long", "leap"]:
    print(kd, {pr: summary["agreement"][f"{kd}|{pr}"]["rate"] for pr in PAIRS})
for L, v in summary["classes"].items():
    print(f"{L}: {v['queries']} queries, {v['scenarios']} scenarios, {len(v['zones'])} zones")
