#!/usr/bin/env python3
"""
GDR-8050L real auth flag hunt v105.

This is a follow-up to v104. v104 proved the known transition side-effects,
but it passed auth-page length as bare "12", which run_direct_firmware parsed
as decimal 12 (0x0C). v105 passes all packet data bytes as explicit 0xNN and
adds high-alias/auth-media state watchpoints around 0x8003E7BA..0x8003E80F.
"""
from __future__ import annotations

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

MODE_SELECT_PARTITION1 = [
    "0x00", "0x1A", "0x00", "0x00", "0x00", "0x00", "0x00", "0x00",
    "0x3E", "0x12", "0x01", "0x01", "0x01", "0xD1", "0x01", "0xDC",
    "0xA2", "0xE7", "0xC8", "0xED", "0x00", "0x00", "0x00", "0x00",
    "0x00", "0x00", "0x00", "0x00",
]

# Seed real pre-Step-8 values after reset/init, at auth entry.  This lets us
# see whether the firmware itself rewrites them during the partition/unlock path.
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 state candidate"),
    ("0x00000722", "00", "real pre-Step-8 0x0722 state candidate"),
    ("0x0000076D", "0103", "real pre-Step-8 0x076D..0x076E candidate word bytes"),
    ("0x0000077C", "0C010000", "real pre-Step-8 0x077C..0x077F aggregate cache"),
    ("0x000007F8", "2100", "real pre-Step-8 0x07F8..0x07F9 candidate word bytes"),
    # High-alias physical mirror block from real capture: 0x8003E7BA == low physical 0x0003E7BA.
    ("0x0003E7BA", "0000000003000000", "real pre-Step-8 high auth/media block 0x8003E7BA..0x8003E7C1"),
    ("0x0003E7DC", "14000000", "real pre-Step-8 high countdown/latch block 0x8003E7DC..0x8003E7DF"),
    ("0x0003E804", "00010000500000000C000000", "real pre-Step-8 high aggregate sources 0x8003E804..0x8003E80F"),
]

WATCHPOINTS = [
    # Low-RAM transition candidates and live packet flags.
    "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",
    "0x0000071F:6@access",
    "0x00000722:4@access",
    "0x0000076D:2@access",
    "0x0000077C:4@access",
    "0x000007F8:2@access",
    "0x0000D95C:1@access",
    # High-auth/media block, watched via low physical aliases because the emulator aliases 0x80000000->0x00000000.
    "0x0003E7BA:8@access",   # high 0x8003E7BA..E7C1
    "0x0003E7DC:4@access",   # high 0x8003E7DC..E7DF
    "0x0003E7FC:4@access",   # high 0x8003E7FC..E7FF
    "0x0003E804:16@access",  # high 0x8003E804..E813
]

BREAKPOINTS = [
    # Auth entry and low state setters.
    "0x9001B71B", "0x9001B72C", "0x9001B731", "0x9001B735", "0x9001B73E",
    "0x9001B743", "0x9001B749", "0x9001B757", "0x9001B75C", "0x9001B75F",
    "0x9001B764", "0x9001B76E",
    "0x90021696", "0x9001B953", "0x9001B966", "0x9001B96C",
    "0x9001CFCA",
    "0x9001D056", "0x9001D05B", "0x9001D06B", "0x9001D078", "0x9001D086",
    "0x9001D090", "0x9001D096", "0x9001D0C6",
    # High alias/latch compare and writers.
    "0x9001EF1E", "0x9001EF26", "0x9001EF37", "0x9001EF54", "0x9001EF5A",
    "0x9001EF60", "0x9001EF66", "0x9001EF69", "0x9001EF71",
    "0x90006434", "0x90006446", "0x90024165", "0x9002424D", "0x9002878A",
    # Later consumers of high auth/media block.
    "0x900396FD", "0x9003971A", "0x90039767", "0x90039778", "0x900397B4",
]

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",
    0x0003E7BA: "0x8003E7BA", 0x0003E7BB: "0x8003E7BB", 0x0003E7BC: "0x8003E7BC",
    0x0003E7BD: "0x8003E7BD", 0x0003E7BE: "0x8003E7BE", 0x0003E7BF: "0x8003E7BF",
    0x0003E7DC: "0x8003E7DC", 0x0003E7DE: "0x8003E7DE",
    0x0003E7FC: "0x8003E7FC", 0x0003E804: "0x8003E804", 0x0003E80C: "0x8003E80C",
}
KEY_PCS = {bp.lower() for bp in BREAKPOINTS}


def add_seed_hook(trace: dict) -> None:
    trace.setdefault("synthetic_hooks", []).append({
        "name": "seed_real_pre_step8_auth_flag_hunt_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 ensure_breakpoints(trace: dict) -> None:
    bps = trace.setdefault("breakpoints", [])
    seen = {str(bp.get("address", "")).lower() for bp in bps if isinstance(bp, dict)}
    for bp in BREAKPOINTS:
        if bp.lower() not in seen:
            bps.append({"address": bp, "name": f"bp_{bp}"})
            seen.add(bp.lower())


def write_analysis(csv_path: Path, out_report: Path, out_events: Path) -> None:
    if not csv_path.exists():
        out_report.write_text(f"# v105 real auth flag hunt\n\nTrace missing: `{csv_path}`\n", encoding="utf-8")
        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 "").lower()
            addr = None
            try:
                addr = int(addr_s, 16) if addr_s else None
            except Exception:
                pass
            if addr in KEY_ADDRS or pc_s in KEY_PCS or row.get("event") == "synthetic_write":
                if addr in KEY_ADDRS:
                    row["addr_label"] = KEY_ADDRS[addr]
                rows.append(row)
    if rows:
        fields = list(rows[0].keys())
        if "addr_label" not in fields:
            fields.append("addr_label")
        with out_events.open("w", newline="", encoding="utf-8") as f:
            w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
            w.writeheader(); w.writerows(rows)
    hit_pcs = {r.get("pc") for r in rows if r.get("event") == "break"}
    def pc_hit(pc: str) -> str:
        return "HIT" if pc in hit_pcs else "miss"
    # Find the last writes for key labels.
    last_writes = {}
    reads = {}
    for r in rows:
        label = r.get("addr_label")
        if not label:
            continue
        if r.get("event") in ("watch_write", "synthetic_write"):
            last_writes[label] = r
        if r.get("event") == "watch_read":
            reads.setdefault(label, []).append(r)
    lines = [
        "# GDR-8050L v105 real auth flag hunt report", "",
        "## Packet data correction", "",
        "v105 forces the Step-8 MODE SELECT parameter byte 9 to `0x12`, not bare decimal `12` / `0x0C`.", "",
        "## Key PC hits", "",
        "| PC | Meaning | Hit |", "|---|---|---|",
        f"| 0x9001D086 | writes low state `0x071E = 02` and calls downstream high mirror path | {pc_hit('0x9001D086')} |",
        f"| 0x9001D090 | writes high mirror `0x8003E7BA = 01` | {pc_hit('0x9001D090')} |",
        f"| 0x900396FD | reads `0x8003E7BB` for capacity/layout path | {pc_hit('0x900396FD')} |",
        f"| 0x9003971A | reads `0x8003E7BC` base/limit word | {pc_hit('0x9003971A')} |",
        f"| 0x90039767 | reads `0x8003E7BB` later capacity mapper | {pc_hit('0x90039767')} |",
        f"| 0x90039778 | reads `0x8003E7BC` later capacity mapper | {pc_hit('0x90039778')} |",
        f"| 0x9001EF26 | checks transient latch `0x8003E7DE` | {pc_hit('0x9001EF26')} |",
        f"| 0x9001EF60 | writes transient latch `0x8003E7DE` | {pc_hit('0x9001EF60')} |",
        "", "## Last observed writes", "",
        "| Address | Last event | PC | Bytes | Notes |", "|---|---|---|---|---|",
    ]
    for label in ["0x071E", "0x8003E7BA", "0x8003E7BB", "0x8003E7BC", "0x8003E7BD", "0x8003E7BE", "0x8003E7DE", "0x8003E804", "0x8003E80C", "0x077C", "0x05CD", "0xD95C"]:
        r = last_writes.get(label)
        if r:
            lines.append(f"| {label} | {r.get('event')} | {r.get('pc')} | {r.get('bytes_hex')} | {r.get('notes','')} |")
        else:
            lines.append(f"| {label} | — | — | — | no write in relevant rows |")
    lines += [
        "", "## Interpretation rule", "",
        "If `0x9001D086` and `0x9001D090` hit, then the strongest real auth-state pair is `0x071E` plus high mirror `0x8003E7BA`.",
        "If `0x8003E7BB` and `0x8003E7BC..0x8003E7BF` become/read as the post-Step-8 values seen in hardware (`01` and `00 06 06 00`), then the high block is the capacity/unlocked-layout authority.",
        "If only `0x9001EF60` hits, treat `0x8003E7DE` as a transient countdown/latch, not the persistent auth flag.",
    ]
    out_report.write_text("\n".join(lines) + "\n", encoding="utf-8")


def summarize_console(csv_path: Path) -> None:
    report = csv_path.with_name("gdr8050l_partition_watch_v105_real_auth_flag_hunt_report.md")
    events = csv_path.with_name("gdr8050l_partition_watch_v105_real_auth_flag_hunt_events.csv")
    write_analysis(csv_path, report, events)
    print(f"[OK] analysis report: {report}")
    print(f"[OK] relevant events CSV: {events}")
    if report.exists():
        print(report.read_text(encoding="utf-8", errors="replace"))


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_v105_real_auth_flag_hunt_trace.csv"
    cfg_out = emudir / "gdr8050l_partition_watch_v105_real_auth_flag_hunt_config.json"
    if not fw.exists():
        print(f"[-] firmware not found: {fw}"); return 2
    with tempfile.TemporaryDirectory(prefix="mn103s_v105_authflag_") as td:
        tmp_cfg = Path(td) / "direct_firmware_config_v105_authflag.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", "mode-select-auth-xbox",
            "--packet-data-out", *MODE_SELECT_PARTITION1,
            "--auth-watch", "--trace-csv", str(out_csv), "--config-out", str(tmp_cfg), "--no-run",
        ]
        for wp in WATCHPOINTS:
            cmd += ["--watchpoint", wp]
        for bp in BREAKPOINTS:
            cmd += ["--breakpoint", bp]
        # Stop after the high aggregate path; remove this stop in later experiments if we need capacity consumers.
        cmd += ["--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(tmp_cfg.read_text(encoding="utf-8"))
        trace = cfg.setdefault("trace", {})
        trace["out_csv"] = str(out_csv)
        ensure_breakpoints(trace)
        add_seed_hook(trace)
        cfg.setdefault("notes", []).append("v105: real auth-flag hunt; explicit 0x12 auth page length; high auth/media block seeded/watched via low physical aliases.")
        cfg_out.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
        print(f"[OK] wrote config: {cfg_out}")
        print(f"[RUN] {sys.executable} {emu} {cfg_out}", flush=True)
        rc = subprocess.run([sys.executable, str(emu), str(cfg_out)]).returncode
    if out_csv.exists():
        print(f"[OK] trace CSV written: {out_csv}")
        summarize_console(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))
