#!/usr/bin/env python3
"""Compare two archived fixture session folders (positions 1-4)."""

from __future__ import annotations

import argparse
import json
from collections import Counter, defaultdict
from pathlib import Path

WAGER_MSGS = frozenset({"30", "40", "60"})
REPLY_MSGS = frozenset({"39", "49", "69", "89"})
ACK_MSGS = frozenset({"31", "41", "61"})


def load_session(session_dir: Path, pos: int) -> list[dict]:
    p = session_dir / str(pos) / "data" / "events.jsonl"
    if not p.exists():
        return []
    rows = []
    for line in p.read_text(encoding="utf-8").splitlines():
        if line.strip():
            rows.append(json.loads(line))
    return rows


def packet_events(rows: list[dict]) -> list[dict]:
    return [r for r in rows if r.get("ev") == "packet_complete"]


def summarize_session(session_dir: Path, label: str) -> dict:
    out: dict = {"label": label, "path": str(session_dir), "positions": {}}
    for pos in (1, 2, 3, 4):
        rows = load_session(session_dir, pos)
        si = next((r for r in rows if r.get("ev") == "session_start"), {})
        bc37_pc = Counter(
            (r.get("msg"), r.get("len"), r.get("game"))
            for r in packet_events(rows)
            if r.get("tid") == "BC37"
        )
        bc37_bridge = Counter(
            (r.get("ev"), r.get("msg"), r.get("len"))
            for r in rows
            if r.get("tid") == "BC37"
            and r.get("ev") in ("eth_tx", "eth_rx", "eth_to_uart_done", "eth_rx_skip")
        )
        skips = Counter(
            r.get("reason") for r in rows if r.get("ev") == "eth_rx_skip"
        )
        games = Counter(
            r.get("game")
            for r in packet_events(rows)
            if r.get("tid") == "BC37" and r.get("game")
        )
        out["positions"][pos] = {
            "start": si.get("utc"),
            "role": si.get("role", si.get("mode")),
            "meta": {
                k: si[k]
                for k in si
                if k.startswith(("suppress_", "uart_", "pre_tx", "machine"))
            },
            "total_events": len(rows),
            "packet_complete": sum(1 for r in rows if r.get("ev") == "packet_complete"),
            "bc37_packet_complete": dict(bc37_pc),
            "bc37_bridge": dict(bc37_bridge),
            "bc37_games": dict(games),
            "eth_rx_skip": dict(skips),
            "errors": [r.get("message") for r in rows if r.get("ev") == "error"],
        }
    return out


def bc37_wager_sequences(rows: list[dict], *, max_rounds: int = 8) -> list[str]:
    """Compact BC37 wager->reply chains from packet_complete + bridge events."""
    seq: list[str] = []
    for r in rows:
        if r.get("tid") != "BC37":
            continue
        ev = r.get("ev")
        msg = r.get("msg")
        if ev == "packet_complete" and msg in WAGER_MSGS | ACK_MSGS:
            g = r.get("game") or "?"
            seq.append(f"pc:{msg}({r.get('len')}) g={g} s={r.get('seq')}")
        elif ev == "eth_tx" and msg in WAGER_MSGS:
            seq.append(f"tx:{msg}({r.get('len')})")
        elif ev == "eth_rx" and msg in REPLY_MSGS:
            seq.append(f"rx:{msg}({r.get('len')})")
        elif ev == "eth_to_uart_done" and msg in REPLY_MSGS:
            seq.append(f"inj:{msg}({r.get('len')})")
    return seq[:max_rounds]


def pos1_pos2_bc37_agreement(good_dir: Path, bad_dir: Path) -> None:
    for name, d in (("GOOD", good_dir), ("BAD", bad_dir)):
        p1 = {
            (r.get("msg"), r.get("len"), r.get("game"), r.get("seq"))
            for r in packet_events(load_session(d, 1))
            if r.get("tid") == "BC37"
        }
        p2 = {
            (r.get("msg"), r.get("len"), r.get("game"), r.get("seq"))
            for r in packet_events(load_session(d, 2))
            if r.get("tid") == "BC37"
        }
        only1 = p1 - p2
        only2 = p2 - p1
        print(f"\n  {name}: pos1 BC37 frames={len(p1)} pos2={len(p2)} common={len(p1 & p2)}")
        if only1:
            print(f"    only pos1 ({len(only1)}): {sorted(only1)[:5]}")
        if only2:
            print(f"    only pos2 ({len(only2)}): {sorted(only2)[:5]}")


def counter_diff(a: dict, b: dict) -> None:
    keys = sorted(set(a) | set(b))
    for k in keys:
        va, vb = a.get(k, 0), b.get(k, 0)
        if va != vb:
            print(f"    {k}: good={va} bad={vb} delta={vb - va:+d}")


def main() -> None:
    parser = argparse.ArgumentParser(description="Diff two fixture session archives")
    parser.add_argument(
        "good",
        nargs="?",
        default="../sessions/2026-06-11_10am-good_BC37-games",
    )
    parser.add_argument(
        "bad",
        nargs="?",
        default="../sessions/2026-06-11_18-12-UTC_poll-only-no-master-reply",
    )
    parser.add_argument("-o", "--output", help="Write report to file")
    args = parser.parse_args()

    root = Path(__file__).resolve().parent
    good_dir = (root / args.good).resolve()
    bad_dir = (root / args.bad).resolve()

    good = summarize_session(good_dir, "GOOD (~10am / 14:06 UTC)")
    bad = summarize_session(bad_dir, "BAD (18:12 UTC poll-only)")

    lines: list[str] = []

    def emit(s: str = "") -> None:
        lines.append(s)
        print(s)

    emit("=" * 78)
    emit("FIXTURE SESSION DIFF")
    emit(f"  GOOD: {good_dir.name}")
    emit(f"  BAD:  {bad_dir.name}")
    emit("=" * 78)

    emit("\n## SESSION START")
    for pos in (1, 2, 3, 4):
        g = good["positions"][pos]
        b = bad["positions"][pos]
        emit(f"  Pos {pos}: good {g['start']}  |  bad {b['start']}")

    emit("\n## SCRIPT METADATA (pos 3/4 session_start)")
    for pos in (3, 4):
        emit(f"  Pos {pos} good meta: {good['positions'][pos]['meta']}")
        emit(f"  Pos {pos} bad  meta: {bad['positions'][pos]['meta']}")

    emit("\n## VOLUME")
    emit(f"  {'Pos':<5} {'good events':>12} {'bad events':>12} {'good pkts':>10} {'bad pkts':>10}")
    for pos in (1, 2, 3, 4):
        g, b = good["positions"][pos], bad["positions"][pos]
        emit(
            f"  {pos:<5} {g['total_events']:>12} {b['total_events']:>12} "
            f"{g['packet_complete']:>10} {b['packet_complete']:>10}"
        )

    emit("\n## BC37 packet_complete (msg, len, game) — per position")
    for pos in (1, 2, 3, 4):
        emit(f"\n  --- Pos {pos} ---")
        emit("  GOOD:")
        for k, v in sorted(good["positions"][pos]["bc37_packet_complete"].items()):
            emit(f"    {k}: {v}")
        if not good["positions"][pos]["bc37_packet_complete"]:
            emit("    (none)")
        emit("  BAD:")
        for k, v in sorted(bad["positions"][pos]["bc37_packet_complete"].items()):
            emit(f"    {k}: {v}")
        if not bad["positions"][pos]["bc37_packet_complete"]:
            emit("    (none)")

    emit("\n## BC37 bridge events (pos 3/4 only)")
    for pos in (3, 4):
        emit(f"\n  --- Pos {pos} GOOD ---")
        for k, v in sorted(good["positions"][pos]["bc37_bridge"].items()):
            emit(f"    {k}: {v}")
        emit(f"  --- Pos {pos} BAD ---")
        for k, v in sorted(bad["positions"][pos]["bc37_bridge"].items()):
            emit(f"    {k}: {v}")

    emit("\n## BC37 games played (packet_complete game id)")
    for pos in (4, 1):
        emit(f"  Pos {pos} good: {good['positions'][pos]['bc37_games']}")
        emit(f"  Pos {pos} bad:  {bad['positions'][pos]['bc37_games']}")

    emit("\n## short_reply skips (good session only — policy before removal)")
    for pos in (3, 4):
        sr = sum(
            1
            for r in load_session(good_dir, pos)
            if r.get("ev") in ("eth_tx_skip", "eth_rx_skip") and r.get("reason") == "short_reply"
        )
        emit(f"  Pos {pos} good short_reply skips: {sr}")

    emit("\n## eth_rx_skip reasons")
    for pos in (3, 4):
        emit(f"  Pos {pos}:")
        emit(f"    GOOD: {dict(good['positions'][pos]['eth_rx_skip'])}")
        emit(f"    BAD:  {dict(bad['positions'][pos]['eth_rx_skip'])}")

    emit("\n## POS 1 vs POS 2 BC37 agreement")
    pos1_pos2_bc37_agreement(good_dir, bad_dir)

    emit("\n## BC37 TIMELINE SNIPPET (pos 4, first ~12 notable events)")
    for name, d in (("GOOD", good_dir), ("BAD", bad_dir)):
        emit(f"\n  {name}:")
        snippet = bc37_wager_sequences(load_session(d, 4), max_rounds=12)
        for s in snippet:
            emit(f"    {s}")
        if not snippet:
            emit("    (no BC37 wager/reply chain)")

    emit("\n## GOOD SESSION — sample Keno round (pos 4, first x40 BC37)")
    rows = load_session(good_dir, 4)
    started = False
    count = 0
    for r in rows:
        if r.get("tid") != "BC37":
            continue
        if r.get("msg") == "40" and r.get("ev") == "packet_complete" and not started:
            started = True
        if not started:
            continue
        if count > 20:
            break
        if r.get("ev") in ("packet_complete", "eth_tx", "eth_rx", "eth_to_uart_done", "eth_rx_skip"):
            emit(
                f"    {r.get('utc','')[11:23]} {r.get('ev'):<18} "
                f"msg={r.get('msg')} len={r.get('len')} "
                f"{('reason='+r['reason']) if r.get('reason') else ''}"
            )
            count += 1

    emit("\n## SIDE-BY-SIDE SUMMARY")
    metrics = []

    def cnt_pc(session: dict, pos: int, msg: str | None = None) -> int:
        c = session["positions"][pos]["bc37_packet_complete"]
        if msg:
            return sum(v for (m, ln, g), v in c.items() if m == msg)
        return sum(c.values())

    def cnt_br(session: dict, pos: int, ev: str, msg: str | None = None) -> int:
        c = session["positions"][pos]["bc37_bridge"]
        if msg:
            return sum(v for (e, m, ln), v in c.items() if e == ev and m == msg)
        return sum(v for (e, m, ln), v in c.items() if e == ev)

    rows_tbl = [
        ("BC37 x40 wagers pos4 (pc)", cnt_pc(good, 4, "40"), cnt_pc(bad, 4, "40")),
        ("BC37 x30 wagers pos4 (pc)", cnt_pc(good, 4, "30"), cnt_pc(bad, 4, "30")),
        ("BC37 x80 polls pos4 (pc)", cnt_pc(good, 4, "80"), cnt_pc(bad, 4, "80")),
        ("BC37 x49 replies pos4 (eth_rx)", cnt_br(good, 4, "eth_rx", "49"), cnt_br(bad, 4, "eth_rx", "49")),
        ("BC37 x49 inject pos4 (eth_to_uart)", cnt_br(good, 4, "eth_to_uart_done", "49"), cnt_br(bad, 4, "eth_to_uart_done", "49")),
        ("BC37 x80 polls pos1 (pc)", cnt_pc(good, 1, "80"), cnt_pc(bad, 1, "80")),
        ("BC37 game ids pos4", len(good["positions"][4]["bc37_games"]), len(bad["positions"][4]["bc37_games"])),
        ("pos4 total events", good["positions"][4]["total_events"], bad["positions"][4]["total_events"]),
        ("pos4 eth_rx_skip no_eop", good["positions"][4]["eth_rx_skip"].get("no_eop", 0), bad["positions"][4]["eth_rx_skip"].get("no_eop", 0)),
        ("pos4 uart_origin_echo skip", good["positions"][4]["eth_rx_skip"].get("uart_origin_echo", 0), bad["positions"][4]["eth_rx_skip"].get("uart_origin_echo", 0)),
    ]
    emit(f"\n  {'Metric':<38} {'GOOD':>8} {'BAD':>8}")
    emit("  " + "-" * 56)
    for name, gv, bv in rows_tbl:
        mark = " ***" if gv != bv else ""
        emit(f"  {name:<38} {gv:>8} {bv:>8}{mark}")

    emit("\n## CONCLUSIONS")
    emit("  1. GOOD: BC37 full game protocol (wagers + Master replies on pos4 ETH path).")
    emit("  2. BAD:  BC37 only x80 polls — outbound polls reach pos1, but no game wagers")
    emit("         and no Master replies to BC37 on any position.")
    emit("  3. Both sessions show other-TID trunk Keno — Master path alive in both.")
    emit("  4. Regression is BC37 round-trip (especially Master->pos4), not Ethernet")
    emit("         silence for the whole bench.")
    if good["positions"][4]["meta"] != bad["positions"][4]["meta"]:
        emit("  5. Script metadata differs between sessions — see SESSION START section.")

    if args.output:
        out = Path(args.output)
        out.write_text("\n".join(lines) + "\n", encoding="utf-8")
        print(f"\nWrote {out}")


if __name__ == "__main__":
    main()
