#!/usr/bin/env python3
"""
GDR-8050L partition/auth watch v104.

Why this exists:
  v103 initial --mem8 seeds were applied before firmware reset/init, then the
  reset clear loop at PC 0x90000044 zeroed the candidate bytes before the
  partition/auth service executed.  This runner builds the same direct-firmware
  config, then injects the real pre-Step-8 candidate bytes with a one-shot
  synthetic hook at PC 0x9001B71B, immediately as the packet/auth service is
  entered.  It intentionally does NOT overwrite live scheduler/control flags
  such as 0x05B1, 0x05B6, 0x06F1, or 0xDA0C.
"""
from __future__ import annotations

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

MODE_SELECT_PARTITION1 = [
    "00", "1A", "00", "00", "00", "00", "00", "00",
    "3E", "12", "01", "01", "01", "D1", "01", "DC",
    "A2", "E7", "C8", "ED", "00", "00", "00", "00",
    "00", "00", "00", "00",
]

STATIC_SEED_ACTIONS = [
    ("0x000005CD", "40", "real pre-Step-8 0x05CD command-completion/status byte"),
    ("0x0000D95C", "24", "real pre-Step-8 0xD95C packet/status byte"),
    ("0x0000071E", "01", "real pre-Step-8 0x071E candidate"),
    ("0x00000722", "00", "real pre-Step-8 0x0722 candidate"),
    ("0x0000076D", "0103", "real pre-Step-8 0x076D..0x076E candidate word bytes"),
    ("0x0000077C", "0C010000", "real pre-Step-8 0x077C..0x077F aggregate candidate"),
    ("0x000007F8", "2100", "real pre-Step-8 0x07F8..0x07F9 candidate word bytes"),
]

EXTRA_BREAKPOINTS = [
    "0x9001B953",  # D95C intermediate clear/write path
    "0x9001B966",  # D95C read before final write
    "0x9001B96C",  # D95C = 0x04 write site
    "0x9001EF5A",  # non-equal branch after 077C aggregate compare
    "0x9001EF66",  # 077C aggregate clear/write site
    "0x9001EF69",  # common post-aggregate path
]

WATCHPOINTS = [
    "0x000005CD:1@access",
    "0x000005B1:1@access",
    "0x000005B6:1@access",
    "0x000006E1:1@access",
    "0x000006EC:2@access",
    "0x000006F1:1@access",
    "0x0000DA0C:1@access",
    "0x0000DA24:1@access",
    "0x0000DA28:1@access",
    "0x0000DA3C:1@access",
    "0x0000071E:1@access",
    "0x00000722:1@access",
    "0x00000724:1@access",
    "0x0000076D:2@access",
    "0x0000077C:4@access",
    "0x000007F8:2@access",
    "0x0000D95C:1@access",
]

BREAKPOINTS = [
    "0x9001CFCA",
    "0x90021696",
    "0x9001B71B",
    "0x9001B72C",
    "0x9001B731",
    "0x9001B735",
    "0x9001B73E",
    "0x9001B743",
    "0x9001B749",
    "0x9001B757",
    "0x9001B75C",
    "0x9001B75F",
    "0x9001B764",
    "0x9001B76E",
    "0x9001EF37",
    "0x9001EF54",
    "0x9001EF66",
    "0x9001EF71",
    "0x9001B96C",
    "0x9001BC3B",
    "0x9001D086",
    "0x9001EE4A",
]

KEY_ADDRS = {
    0x000005CD: "0x05CD",
    0x0000D95C: "0xD95C",
    0x0000071E: "0x071E",
    0x00000722: "0x0722",
    0x0000076D: "0x076D",
    0x0000076E: "0x076E",
    0x0000077C: "0x077C",
    0x0000077D: "0x077D",
    0x0000077E: "0x077E",
    0x0000077F: "0x077F",
    0x000007F8: "0x07F8",
    0x000007F9: "0x07F9",
    0x000005B1: "0x05B1",
    0x000005B6: "0x05B6",
    0x000006F1: "0x06F1",
    0x0000DA0C: "0xDA0C",
}
KEY_PCS = {
    "0x9001B71B", "0x90021696", "0x9001B953", "0x9001B966", "0x9001B96C",
    "0x9001EF54", "0x9001EF5A", "0x9001EF66", "0x9001EF69", "0x9001EF71",
}


def add_unique_breakpoints(trace: dict) -> None:
    bps = trace.setdefault("breakpoints", [])
    seen = {str(bp.get("address", bp.get("addr", ""))).lower() for bp in bps if isinstance(bp, dict)}
    for raw in EXTRA_BREAKPOINTS:
        if raw.lower() not in seen:
            bps.append({"address": raw, "name": f"bp_{raw}"})
            seen.add(raw.lower())


def add_seed_hook(trace: dict) -> None:
    hooks = trace.setdefault("synthetic_hooks", [])
    hooks.append({
        "name": "seed_real_pre_step8_static_candidates_at_auth_entry",
        "pc": "0x9001B71B",
        "once": True,
        "actions": [
            {
                "write_memory": {"address": addr, "data_hex": data_hex},
                "description": desc,
            }
            for addr, data_hex, desc in STATIC_SEED_ACTIONS
        ],
    })


def summarize_trace(csv_path: Path) -> None:
    if not csv_path.exists():
        print(f"[-] no trace CSV generated: {csv_path}")
        return
    rows = []
    with csv_path.open(newline="", encoding="utf-8", errors="replace") as f:
        for row in csv.DictReader(f):
            addr_s = row.get("addr_physical") or row.get("addr") or ""
            pc_s = row.get("pc") or ""
            try:
                addr = int(addr_s, 16) if addr_s else None
            except Exception:
                addr = None
            if addr in KEY_ADDRS or pc_s in KEY_PCS or row.get("event") == "synthetic_write":
                rows.append(row)
    print(f"[SUMMARY] relevant_rows={len(rows)}")
    for row in rows[-80:]:
        addr_s = row.get("addr_physical") or row.get("addr") or ""
        label = ""
        try:
            label = KEY_ADDRS.get(int(addr_s, 16), "") if addr_s else ""
        except Exception:
            pass
        print(
            f"[EV] seq={row.get('seq')} cyc={row.get('cycle')} {row.get('event')} "
            f"pc={row.get('pc')} addr={addr_s} {label} size={row.get('size')} "
            f"val={row.get('value')} bytes={row.get('bytes_hex')} name={row.get('name')} {row.get('notes','')}"
        )
    pcs = {row.get("pc") for row in rows if row.get("event") == "break"}
    for pc in ["0x90021696", "0x9001B953", "0x9001B96C", "0x9001EF54", "0x9001EF5A", "0x9001EF66", "0x9001EF71"]:
        print(f"[CHECK] {pc} {'HIT' if pc in pcs else 'miss'}")


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()
    fw = workspace / "firmware" / "GDR-8050L" / "8050L_fw_plaintext_256k.bin"
    run_direct = emudir / "run_direct_firmware.py"
    emu = emudir / "mn103s_emu.py"
    out_csv = emudir / "gdr8050l_partition_watch_v104_seed_at_auth_entry_trace.csv"
    if not fw.exists():
        print(f"[-] firmware not found: {fw}")
        return 2
    if not run_direct.exists() or not emu.exists():
        print(f"[-] expected run_direct_firmware.py and mn103s_emu.py next to this script in {emudir}")
        return 2

    with tempfile.TemporaryDirectory(prefix="mn103s_v104_seed_") as td:
        cfg_path = Path(td) / "direct_firmware_config_v104_seed_at_auth_entry.json"
        cmd = [
            sys.executable, str(run_direct),
            "--workspace", str(workspace),
            "--model", "GDR-8050L",
            "--bin", str(fw),
            "--base", "0x90000000",
            "--entry", "0x90000020",
            "--max-cycles", "120000",
            "--preset", "gdr8050l-packet-full-cdb",
            "--clear-preset-stops",
            "--packet-cdb", "mode-select-auth-xbox",
            "--packet-data-out", *MODE_SELECT_PARTITION1,
            "--auth-watch",
            "--trace-csv", str(out_csv),
            "--config-out", str(cfg_path),
            "--no-run",
        ]
        for wp in WATCHPOINTS:
            cmd.extend(["--watchpoint", wp])
        for bp in BREAKPOINTS:
            cmd.extend(["--breakpoint", bp])
        cmd.extend(["--stop-pc", "0x9001EF71", "--stop-pc-min-cycle", "54000"])

        print("[BUILD-CONFIG] " + " ".join(cmd), flush=True)
        rc = subprocess.run(cmd).returncode
        if rc != 0:
            return rc
        cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
        trace = cfg.setdefault("trace", {})
        trace["out_csv"] = str(out_csv)
        add_unique_breakpoints(trace)
        add_seed_hook(trace)
        cfg.setdefault("notes", []).append(
            "v104: real pre-Step-8 static candidate bytes are seeded at PC 0x9001B71B via trace.synthetic_hooks, not by initial memory_injections, because reset/init zeroes low SRAM."
        )
        patched_cfg = emudir / "gdr8050l_partition_watch_v104_seed_at_auth_entry_config.json"
        patched_cfg.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
        print(f"[OK] wrote patched config: {patched_cfg}")
        print(f"[RUN] {sys.executable} {emu} {patched_cfg}", flush=True)
        rc = subprocess.run([sys.executable, str(emu), str(patched_cfg)]).returncode
        if out_csv.exists():
            print(f"[OK] trace CSV written: {out_csv}")
            summarize_trace(out_csv)
        else:
            print(f"[-] trace CSV missing: {out_csv}")
            if rc == 0:
                return 3
        return rc


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