#!/usr/bin/env python3
"""
Optional v107 probe: seed the GDR-8050L high auth/layout block after reset and
run a READ CAPACITY packet trace to see whether the current emulator harness
reaches the native capacity/layout consumers.

This probe is not required to prove the hardware high-block transition; it is
for emulator path development. If the capacity consumer PCs miss, the packet
harness still needs work.
"""
from __future__ import annotations

import csv
import json
import subprocess
import sys
import tempfile
from pathlib import Path

BREAKPOINTS = [
    "0x90024325", "0x9002439A", "0x9002068B",
    "0x9001D05B", "0x9001D086", "0x9001D090",
    "0x900396FD", "0x9003971A", "0x90039767", "0x90039778", "0x900397B4",
    "0x90021696", "0x9001EF54", "0x9001EF66",
]
WATCHPOINTS = [
    "0x0003E7BA:8@access", "0x0003E7BC:4@access", "0x0003E804:16@access",
    "0x0000071E:1@access", "0x000005CD:1@access", "0x0000077C:4@access",
    "0x0000DF70:64@access", "0x0000DF30:8@access", "0x000007E8:8@access",
]

VARIANTS = {
    "locked": {
        "seed_hex": "0000000003000000",
        "notes": "locked/pre high auth-layout block 0x8003E7BA..E7C1",
    },
    "unlocked": {
        "seed_hex": "0101000606000000",
        "notes": "unlocked/post-Step8 high auth-layout block 0x8003E7BA..E7C1",
    },
}


def add_seed_hook(trace: dict, variant: str) -> None:
    v = VARIANTS[variant]
    trace.setdefault("synthetic_hooks", []).append({
        "name": f"seed_{variant}_high_auth_layout_at_packet_service",
        "pc": "0x90024325",
        "once": True,
        "actions": [
            {"write_memory": {"address": "0x0003E7BA", "data_hex": v["seed_hex"]}, "description": v["notes"]},
            {"write_memory": {"address": "0x0000071E", "data_hex": "02" if variant == "unlocked" else "00"}, "description": "low shadow seed"},
        ],
    })


def ensure_trace_items(trace: dict) -> None:
    bps = trace.setdefault("breakpoints", [])
    seen = {str(x.get("address", "")).lower() for x in bps if isinstance(x, dict)}
    for bp in BREAKPOINTS:
        if bp.lower() not in seen:
            bps.append({"address": bp, "name": f"bp_{bp}"})
            seen.add(bp.lower())
    wps = trace.setdefault("watchpoints", [])
    # Add string watchpoint specs as dicts; mn103s_emu accepts config dict style.
    seen_wp = {(str(x.get("address", "")).lower(), str(x.get("size", ""))) for x in wps if isinstance(x, dict)}
    for spec in WATCHPOINTS:
        addr, rest = spec.split(":", 1)
        size, kind = rest.split("@", 1)
        key = (addr.lower(), size)
        if key not in seen_wp:
            wps.append({"address": addr, "size": size, "kind": kind})
            seen_wp.add(key)


def summarize(csv_path: Path, out_report: Path) -> None:
    rows = []
    if csv_path.exists():
        with csv_path.open(newline="", encoding="utf-8", errors="replace") as f:
            rows = list(csv.DictReader(f))
    hit_pcs = {r.get("pc", "").lower() for r in rows if r.get("event") == "break"}
    def hit(pc: str) -> str:
        return "HIT" if pc.lower() in hit_pcs else "miss"
    lines = [
        f"# {csv_path.stem}", "",
        "## Capacity/high-block consumer PC hits", "",
        "| PC | Meaning | Hit |", "|---|---|---|",
        f"| 0x900396FD | read 0x8003E7BB capacity/layout gate | {hit('0x900396FD')} |",
        f"| 0x9003971A | read 0x8003E7BC layout word | {hit('0x9003971A')} |",
        f"| 0x90039767 | read 0x8003E7BB later mapper | {hit('0x90039767')} |",
        f"| 0x90039778 | read 0x8003E7BC later mapper | {hit('0x90039778')} |",
        f"| 0x900397B4 | read 0x8003E7BA auth flag | {hit('0x900397B4')} |",
        "", "## Note", "",
        "If all of these miss, the current READ CAPACITY emulator harness still short-circuits before the firmware native capacity mapper.",
    ]
    out_report.write_text("\n".join(lines) + "\n", encoding="utf-8")


def run_variant(emudir: Path, workspace: Path, variant: str) -> int:
    run_direct = emudir / "run_direct_firmware.py"
    emu = emudir / "mn103s_emu.py"
    fw = workspace / "firmware" / "GDR-8050L" / "8050L_fw_plaintext_256k.bin"
    out_csv = emudir / f"gdr8050l_readcap_highblock_{variant}_v107_trace.csv"
    cfg_out = emudir / f"gdr8050l_readcap_highblock_{variant}_v107_config.json"
    report = emudir / f"gdr8050l_readcap_highblock_{variant}_v107_report.md"
    with tempfile.TemporaryDirectory(prefix=f"mn103s_v107_{variant}_") as td:
        tmp_cfg = Path(td) / "direct_config.json"
        cmd = [
            sys.executable, str(run_direct),
            "--workspace", str(workspace), "--model", "GDR-8050L",
            "--bin", str(fw), "--base", "0x90000000", "--entry", "0x90000020",
            "--max-cycles", "180000", "--preset", "gdr8050l-packet-full-cdb", "--clear-preset-stops",
            "--packet-cdb", "read-capacity", "--auth-watch", "--trace-csv", str(out_csv),
            "--config-out", str(tmp_cfg), "--no-run",
        ]
        for bp in BREAKPOINTS:
            cmd += ["--breakpoint", bp]
        for wp in WATCHPOINTS:
            cmd += ["--watchpoint", wp]
        print("[BUILD-CONFIG] " + " ".join(cmd), flush=True)
        rc = subprocess.run(cmd).returncode
        if rc != 0:
            return rc
        cfg = json.loads(tmp_cfg.read_text(encoding="utf-8"))
        trace = cfg.setdefault("trace", {})
        trace["out_csv"] = str(out_csv)
        ensure_trace_items(trace)
        add_seed_hook(trace, variant)
        cfg.setdefault("notes", []).append(f"v107 {variant}: high auth-layout block seeded at packet service for READ CAPACITY path probe.")
        cfg_out.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
        print(f"[RUN-{variant.upper()}] {sys.executable} {emu} {cfg_out}", flush=True)
        rc = subprocess.run([sys.executable, str(emu), str(cfg_out)]).returncode
    summarize(out_csv, report)
    print(f"[OK] {variant} csv: {out_csv}")
    print(f"[OK] {variant} report: {report}")
    if report.exists():
        print(report.read_text(encoding="utf-8", errors="replace"))
    return rc


def main(argv: list[str]) -> int:
    emudir = Path(__file__).resolve().parent
    workspace = Path(argv[1] if len(argv) > 1 else r"D:\vscode-workspace\xbox-dvd-tools-v2").resolve()
    rc1 = run_variant(emudir, workspace, "locked")
    rc2 = run_variant(emudir, workspace, "unlocked")
    return rc1 or rc2

if __name__ == "__main__":
    raise SystemExit(main(sys.argv))
