"""capture_ops.py — poll the in-page opcode hook for N seconds (accumulating all frames
Python-side, dedup by timestamp), then analyze: distinct frame types, decoded bodies,
sequence vs opcode, and match against the 93-command catalog. Run: python capture_ops.py [secs]"""
import sys, time, json, os, re, urllib.request
from collections import OrderedDict, Counter
from playwright.sync_api import sync_playwright

def port():
    for p in range(9222, 9251):
        try:
            urllib.request.urlopen(f"http://127.0.0.1:{p}/json/version", timeout=0.3); return p
        except Exception:
            pass
    return None

def load_catalog():
    """opcode -> serializer func, from command_vtables.json + opcode-getter constants."""
    base = os.path.join(os.path.dirname(__file__), "reverseEngineeringWASM")
    cv = os.path.join(base, "command_vtables.json")
    if not os.path.exists(cv): return {}
    cmds = json.load(open(cv))
    out = {}
    for c in cmds:
        fp = os.path.join(base, "functions", f"func_{c['opcodeGetter']:05d}.wat")
        if os.path.exists(fp):
            m = re.findall(r"i32.const (-?\d+)", open(fp).read())
            if m: out[int(m[0]) & 0xffffffff] = c["serializer"]
    return out

def u32(d, o):
    return (d[o] | (d[o+1] << 8) | (d[o+2] << 16) | (d[o+3] << 24)) & 0xffffffff if len(d) >= o+4 else None

def main():
    secs = float(sys.argv[1]) if len(sys.argv) > 1 else 180.0
    catalog = load_catalog()
    SER = {336: "target-id(i64)", 4112: "MOVE(xyz+flag)", 1315: "id+byte"}
    p = port()
    if p is None:
        print("no game"); return
    with sync_playwright() as pw:
        pg = pw.chromium.connect_over_cdp(f"http://127.0.0.1:{p}").contexts[0].pages[0]
        if not pg.evaluate("!!window.__opHook"):
            print("hook not active — run opcodelog.py reload first"); return
        frames = OrderedDict()
        print(f"accumulating frames for {secs}s — GRIND NORMALLY (move/select/attack/kill/heal)...")
        t0 = time.time()
        while time.time() - t0 < secs:
            for e in pg.evaluate("window.__opLog||[]"):
                k = (e["t"], e["op"], e["n"])
                if k not in frames: frames[k] = e
            time.sleep(0.4)
        fl = list(frames.values())
        print(f"\ncaptured {len(fl)} unique frames\n")

        # group by (frameBytes, bodyLen)
        groups = OrderedDict()
        for e in fl:
            groups.setdefault((e["n"], e["len"]), []).append(e)
        print("=== distinct frame types (frameBytes, bodyLen) x count ===")
        for (n, ln), es in sorted(groups.items()):
            d = es[0]["body"]
            f0 = u32(d, 0)
            seq = (f0 ^ 0x64000000) if (f0 and (f0 >> 24) == 0x64) else None   # 0x64 high byte => sequence field
            if seq is not None:
                op4 = u32(d, 4); op2 = (d[4] | (d[5] << 8)) if len(d) >= 6 else None
                opc = op4 if op4 in catalog else (op2 if op2 in catalog else None)
                lab = f"opcode@body[4]={opc} [{SER.get(catalog.get(opc),'?')}]" if opc else f"body[4:8]={op4} body[4:6]={op2} (not in catalog)"
                print(f"  {n}B/{ln} x{len(es):<4} SEQ-family seq~{seq} {lab}  raw={[hex(x) for x in d[:12]]}")
            else:
                opc = f0
                lab = f"[{SER.get(catalog.get(opc),'?')}]" if opc in catalog else ("(heartbeat/low)" if opc and opc < 1024 else "(not in catalog)")
                print(f"  {n}B/{ln} x{len(es):<4} OPCODE@body[0]={opc} {lab}  raw={[hex(x) for x in d[:12]]}")

        # timeline of first occurrence per frame type (to correlate with action order)
        print("\n=== timeline: first appearance of each frame type (relative s) ===")
        seen = {}
        base_t = fl[0]["t"] if fl else 0
        for e in fl:
            k = (e["n"], e["len"])
            if k not in seen:
                seen[k] = (e["t"] - base_t) / 1000.0
        for (n, ln), ts in sorted(seen.items(), key=lambda kv: kv[1]):
            print(f"  +{ts:6.1f}s  {n}B/{ln}")

if __name__ == "__main__":
    main()
