#!/usr/bin/env python3
"""GDR-8050L descriptor-gate microprobe.

This offline firmware CPU probe starts at the descriptor setup function around
0x9001B54E, not at the raw copy instruction itself.  It seeds the already
assembled READ LBA at SRAM 0x05EC and varies packet/media flag 0x05B6.  The
purpose is to prove the local firmware gate that decides whether the descriptor
builder at 0x9001B5EF copies 0x05EC into descriptor SRAM 0x06E4.

Scope is intentionally narrow.  This still does not prove the full natural READ
CDB route reaches 0x9001B54E/0x9001B5EF, and it does not promote the unlocked
Redump media-window delta to firmware-derived.
"""
from __future__ import annotations

import argparse
import csv
import json
import subprocess
import sys
from pathlib import Path
from typing import Any

try:
    from hlds_gdr_firmware_lba_cpu_smoke import resolve_firmware, load_json, write_json, parse_int
except Exception:  # pragma: no cover
    def load_json(path: Path) -> dict[str, Any]:
        with path.open("r", encoding="utf-8") as f:
            return json.load(f)
    def write_json(path: Path, obj: Any) -> None:
        path.parent.mkdir(parents=True, exist_ok=True)
        with path.open("w", encoding="utf-8") as f:
            json.dump(obj, f, indent=2)
            f.write("\n")
    def parse_int(v: Any, default: int = 0) -> int:
        if v is None or v == "":
            return default
        if isinstance(v, int):
            return v
        return int(str(v), 0)
    def resolve_firmware(config: dict[str, Any], config_path: Path, firmware_arg: str | None) -> str:
        return str(firmware_arg or config.get("custom_bin") or "")

VERSION = "v256"
DESCRIPTOR_SETUP_ENTRY_PC = 0x9001B54E
DESCRIPTOR_GATE_TEST_PC = 0x9001B5E8
DESCRIPTOR_COPY_ENTRY_PC = 0x9001B5EF
DESCRIPTOR_WRITE_PC = 0x9001B5F2
TRANSFER_FLAG_PC = 0x9001B5F8
AFTER_DESCRIPTOR_COPY_PC = 0x9001B5FB
FUNCTION_RETURN_PC = 0x9001B66E
SRAM_05B6 = 0x05B6
SRAM_05F0 = 0x05F0
SRAM_05EC = 0x05EC
SRAM_06E0 = 0x06E0
SRAM_06E1 = 0x06E1
SRAM_06E4 = 0x06E4
SRAM_06EC = 0x06EC
SRAM_06F1 = 0x06F1

# FE 82 B6 05 02 at 0x9001B5E8 is btst imm=2,(0x05B6).
# mn103s_emu currently treats compact bit immediates as 1 << n, so this is mask 0x04.
GATE_BIT_IMMEDIATE = 0x02
GATE_BIT_MASK = 0x04

CASES: list[dict[str, Any]] = [
    {"name": "gate_on_video_lba", "lba": 0x00000010, "packet_flags_05b6": 0x04, "packet_state_05f0": 0x13, "expect_copy": True},
    {"name": "gate_on_read12_sample_lba", "lba": 0x0004ABBF, "packet_flags_05b6": 0x04, "packet_state_05f0": 0x13, "expect_copy": True},
    {"name": "gate_on_boundary_lba", "lba": 0x00202000, "packet_flags_05b6": 0x04, "packet_state_05f0": 0x13, "expect_copy": True},
    {"name": "gate_on_unlocked_max_lba", "lba": 0x00345B5F, "packet_flags_05b6": 0x04, "packet_state_05f0": 0x13, "expect_copy": True},
    {"name": "gate_off_zero_flags", "lba": 0x0004ABBF, "packet_flags_05b6": 0x00, "packet_state_05f0": 0x13, "expect_copy": False},
    {"name": "gate_off_wrong_low_bit", "lba": 0x0004ABBF, "packet_flags_05b6": 0x02, "packet_state_05f0": 0x13, "expect_copy": False},
    {"name": "gate_on_all_flags", "lba": 0x0004ABBF, "packet_flags_05b6": 0xFF, "packet_state_05f0": 0x13, "expect_copy": True},
]


def hex32(v: int) -> str:
    return f"0x{v & 0xFFFFFFFF:08X}"


def hex8(v: int) -> str:
    return f"0x{v & 0xFF:02X}"


def norm_pc(v: Any) -> str:
    try:
        return hex32(parse_int(v, 0))
    except Exception:
        return str(v or "")


def lba_le(lba: int) -> list[int]:
    return [(lba >> (8 * i)) & 0xFF for i in range(4)]


def read_csv(path: Path) -> list[dict[str, str]]:
    if not path.exists():
        return []
    with path.open("r", encoding="utf-8", newline="") as f:
        return list(csv.DictReader(f))


def case_config(base_config: dict[str, Any], firmware_path: str, case: dict[str, Any], case_dir: Path) -> dict[str, Any]:
    lba = int(case["lba"])
    flags = int(case["packet_flags_05b6"])
    state = int(case["packet_state_05f0"])
    out_csv = case_dir / f"{case['name']}.trace.csv"
    return {
        "test_name": f"GDR-8050L {VERSION} descriptor-gate microprobe: {case['name']}",
        "workspace": base_config.get("workspace", str(Path.cwd())),
        "target_profile": "GDR-8050L",
        "custom_bin": firmware_path,
        "custom_base_addr": "0x90000000",
        "entry_point": hex32(DESCRIPTOR_SETUP_ENTRY_PC),
        "max_cycles": 180,
        "header_skip_bytes": "0x0",
        "config_version": VERSION,
        "requires_emulator_version": base_config.get("requires_emulator_version", "V7-direct-config-trace-v120-gcc-version-sync-gdr163-v227-media-preflight"),
        "initial_registers": {"SP": "0x003FF000"},
        "memory_aliases": base_config.get("memory_aliases", [
            {"name": "SRAM alias 0x80000000->0x00000000", "from": "0x80000000", "to": "0x00000000", "size": "0x400000"}
        ]),
        "memory_injections": [
            {"address": hex32(SRAM_05EC), "data": lba_le(lba), "description": f"seed assembled LBA {hex32(lba)} at SRAM 0x05EC"},
            {"address": hex32(SRAM_05B6), "data": [flags & 0xFF], "description": f"seed packet gate flags 0x05B6={hex8(flags)}"},
            {"address": hex32(SRAM_05F0), "data": [state & 0xFF], "description": f"seed packet state 0x05F0={hex8(state)}"},
            {"address": hex32(SRAM_06E0), "data": [0x00], "description": "clear transfer flag 0x06E0"},
            {"address": hex32(SRAM_06E1), "data": [0x00], "description": "clear descriptor flag 0x06E1"},
            {"address": hex32(SRAM_06E4), "data": [0x00] * 8, "description": "clear descriptor LBA/aux region 0x06E4..0x06EB"},
            {"address": hex32(SRAM_06EC), "data": [0x00, 0x00], "description": "clear table-derived bytes 0x06EC..0x06ED"},
            {"address": hex32(SRAM_06F1), "data": [0xFF], "description": "mark packet phase so return-path clear is visible"},
        ],
        "trace": {
            "enabled": True,
            "live_instruction_log": False,
            "out_csv": str(out_csv),
            "watchpoints": [
                {"name": "watch_05ec_assembled_lba", "start": hex32(SRAM_05EC), "size": "0x04", "kind": "access", "description": "source LBA assembled by READ parser"},
                {"name": "watch_05b6_gate_flags", "start": hex32(SRAM_05B6), "size": "0x01", "kind": "access", "description": "descriptor gate flag byte"},
                {"name": "watch_05f0_packet_state", "start": hex32(SRAM_05F0), "size": "0x01", "kind": "access", "description": "packet state/table selector"},
                {"name": "watch_06e0_transfer_flag", "start": hex32(SRAM_06E0), "size": "0x01", "kind": "access", "description": "descriptor/transfer flag"},
                {"name": "watch_06e1_descriptor_flag", "start": hex32(SRAM_06E1), "size": "0x01", "kind": "access", "description": "table-derived descriptor flag"},
                {"name": "watch_06e4_descriptor_lba", "start": hex32(SRAM_06E4), "size": "0x08", "kind": "access", "description": "descriptor LBA destination"},
                {"name": "watch_06ec_table_bytes", "start": hex32(SRAM_06EC), "size": "0x02", "kind": "access", "description": "table-derived bytes copied from packet state table"},
                {"name": "watch_06f1_packet_phase", "start": hex32(SRAM_06F1), "size": "0x01", "kind": "access", "description": "packet phase cleared by return path"},
            ],
            "breakpoints": [
                {"address": hex32(DESCRIPTOR_SETUP_ENTRY_PC), "name": "descriptor_setup_entry_9001B54E", "capture_memory": [
                    {"name": "src_05ec", "address": hex32(SRAM_05EC), "length": 4},
                    {"name": "flags_05b6", "address": hex32(SRAM_05B6), "length": 1},
                    {"name": "state_05f0", "address": hex32(SRAM_05F0), "length": 1},
                ]},
                {"address": hex32(DESCRIPTOR_GATE_TEST_PC), "name": "descriptor_gate_btst_9001B5E8", "capture_memory": [
                    {"name": "flags_05b6", "address": hex32(SRAM_05B6), "length": 1},
                ]},
                {"address": hex32(DESCRIPTOR_COPY_ENTRY_PC), "name": "descriptor_copy_entry_9001B5EF", "capture_memory": [
                    {"name": "src_05ec", "address": hex32(SRAM_05EC), "length": 4},
                    {"name": "dst_06e4", "address": hex32(SRAM_06E4), "length": 8},
                ]},
                {"address": hex32(DESCRIPTOR_WRITE_PC), "name": "descriptor_copy_write_9001B5F2", "capture_memory": [
                    {"name": "src_05ec", "address": hex32(SRAM_05EC), "length": 4},
                    {"name": "dst_06e4", "address": hex32(SRAM_06E4), "length": 8},
                ]},
                {"address": hex32(TRANSFER_FLAG_PC), "name": "descriptor_transfer_flag_9001B5F8", "capture_memory": [
                    {"name": "dst_06e4", "address": hex32(SRAM_06E4), "length": 8},
                    {"name": "flag_06e0", "address": hex32(SRAM_06E0), "length": 1},
                ]},
                {"address": hex32(AFTER_DESCRIPTOR_COPY_PC), "name": "descriptor_after_copy_9001B5FB", "capture_memory": [
                    {"name": "dst_06e4", "address": hex32(SRAM_06E4), "length": 8},
                    {"name": "flag_06e0", "address": hex32(SRAM_06E0), "length": 1},
                ]},
                {"address": hex32(FUNCTION_RETURN_PC), "name": "descriptor_setup_return_9001B66E", "capture_memory": [
                    {"name": "dst_06e4", "address": hex32(SRAM_06E4), "length": 8},
                    {"name": "flag_06e0", "address": hex32(SRAM_06E0), "length": 1},
                    {"name": "phase_06f1", "address": hex32(SRAM_06F1), "length": 1},
                ]},
            ],
        },
    }


def analyze_case(trace_csv: Path, case: dict[str, Any], run_returncode: int, output_path: Path) -> dict[str, Any]:
    rows = read_csv(trace_csv)
    lba = int(case["lba"])
    expected = lba_le(lba)
    expect_copy = bool(case["expect_copy"])
    bps = [r.get("name") for r in rows if r.get("event") == "break"]
    gate_hit = "descriptor_gate_btst_9001B5E8" in bps
    descriptor_hit = "descriptor_copy_entry_9001B5EF" in bps
    write_hit = "descriptor_copy_write_9001B5F2" in bps
    after_hit = "descriptor_after_copy_9001B5FB" in bps
    return_hit = "descriptor_setup_return_9001B66E" in bps
    writes_06e4 = [r for r in rows if r.get("event") == "watch_write" and r.get("name") == "watch_06e4_descriptor_lba" and norm_pc(r.get("pc")) == hex32(DESCRIPTOR_WRITE_PC)]
    by_addr: dict[int, int] = {}
    for r in writes_06e4:
        a = parse_int(r.get("addr"), -1)
        if SRAM_06E4 <= a <= SRAM_06E4 + 3:
            by_addr[a] = parse_int(r.get("value"), 0) & 0xFF
    got = [by_addr.get(SRAM_06E4 + i) for i in range(4)]
    got_hex = "".join("??" if b is None else f"{b:02X}" for b in got)
    expected_hex = "".join(f"{b:02X}" for b in expected)
    descriptor_match = got == expected
    flag_writes = [r for r in rows if r.get("event") == "watch_write" and r.get("name") == "watch_06e0_transfer_flag" and norm_pc(r.get("pc")) == hex32(TRANSFER_FLAG_PC)]
    flag_f0 = any(parse_int(r.get("value"), -1) == 0xF0 for r in flag_writes)
    gate_reads = [r for r in rows if r.get("event") == "watch_read" and r.get("name") == "watch_05b6_gate_flags"]
    gate_values = []
    for r in gate_reads:
        if norm_pc(r.get("pc")) in (hex32(DESCRIPTOR_GATE_TEST_PC), hex32(0x9001B660)):
            gate_values.append(parse_int(r.get("value"), 0) & 0xFF)
    table_state_reads = [r for r in rows if r.get("event") == "watch_read" and r.get("name") == "watch_05f0_packet_state"]
    phase_clears = [r for r in rows if r.get("event") == "watch_write" and r.get("name") == "watch_06f1_packet_phase" and parse_int(r.get("value"), -1) == 0]
    if expect_copy:
        ok = bool(rows) and run_returncode == 0 and gate_hit and descriptor_hit and write_hit and after_hit and descriptor_match and flag_f0
    else:
        ok = bool(rows) and run_returncode == 0 and gate_hit and not descriptor_hit and not write_hit and not flag_f0
    return {
        "case": str(case["name"]),
        "input_lba": lba,
        "input_lba_hex": hex32(lba),
        "packet_flags_05b6": int(case["packet_flags_05b6"]),
        "packet_flags_05b6_hex": hex8(int(case["packet_flags_05b6"])),
        "packet_state_05f0": int(case["packet_state_05f0"]),
        "packet_state_05f0_hex": hex8(int(case["packet_state_05f0"])),
        "expect_copy": expect_copy,
        "trace_csv": str(trace_csv),
        "stdout_stderr_log": str(output_path),
        "run_returncode": run_returncode,
        "row_count": len(rows),
        "gate_test_hit": gate_hit,
        "gate_values_seen": [hex8(v) for v in gate_values[:8]],
        "descriptor_copy_entry_hit": descriptor_hit,
        "descriptor_copy_write_hit": write_hit,
        "descriptor_after_copy_hit": after_hit,
        "return_hit": return_hit,
        "descriptor_06e4_bytes_expected": expected_hex,
        "descriptor_06e4_bytes_written": got_hex,
        "descriptor_06e4_lba_match": descriptor_match,
        "transfer_flag_06e0_f0": flag_f0,
        "phase_06f1_clear_count": len(phase_clears),
        "table_state_read_count": len(table_state_reads),
        "ok": ok,
    }


def run_case(mn103s_emu: Path, cfg: dict[str, Any], cfg_path: Path, output_path: Path, timeout: int) -> int:
    write_json(cfg_path, cfg)
    cmd = [sys.executable, str(mn103s_emu), str(cfg_path)]
    with output_path.open("w", encoding="utf-8", errors="replace") as f:
        try:
            p = subprocess.run(cmd, cwd=str(mn103s_emu.parent), stdout=f, stderr=subprocess.STDOUT, timeout=timeout)
            return int(p.returncode)
        except subprocess.TimeoutExpired as exc:
            f.write(f"\n[TIMEOUT] {exc}\n")
            return 124


def write_md(path: Path, report: dict[str, Any]) -> None:
    lines = [
        f"# GDR-8050L {VERSION} descriptor-gate microprobe",
        "",
        f"- ok: `{str(report.get('ok')).lower()}`",
        f"- descriptor_gate_proven: `{str(report.get('descriptor_gate_proven')).lower()}`",
        f"- descriptor_gate_bit_mask: `{report.get('descriptor_gate_bit_mask_hex')}` at SRAM `0x05B6`",
        f"- descriptor_setup_to_builder_proven: `{str(report.get('descriptor_setup_to_builder_proven')).lower()}`",
        f"- firmware_derived_delta: `{str(report.get('firmware_derived_delta')).lower()}`",
        "",
        "## What this proves",
        "",
        "This offline CPU microprobe starts at firmware `0x9001B54E`, reaches the gate test at `0x9001B5E8`, and proves that when SRAM `0x05B6` has mask `0x04` set, the function naturally enters `0x9001B5EF`, copies the seeded assembled LBA from `0x05EC` to descriptor SRAM `0x06E4`, and writes transfer flag `0x06E0 = 0xF0`.",
        "",
        "It also includes negative cases proving that the same function skips `0x9001B5EF` when that gate bit is clear.",
        "",
        "## What this does not prove",
        "",
        "It still does **not** prove that the full live READ parser route reaches `0x9001B54E`/`0x9001B5EF`, and it does **not** promote the unlocked Redump delta to firmware-derived.",
        "",
        "## Cases",
        "",
        "| case | 0x05B6 | expect copy | input LBA | gate hit | 0x9001B5EF hit | 0x06E4 bytes | 0x06E0=F0 | ok |",
        "|---|---:|---:|---:|---:|---:|---:|---:|---:|",
    ]
    for c in report.get("cases", []) or []:
        lines.append(
            f"| `{c.get('case')}` | `{c.get('packet_flags_05b6_hex')}` | `{c.get('expect_copy')}` | `{c.get('input_lba_hex')}` | `{c.get('gate_test_hit')}` | `{c.get('descriptor_copy_entry_hit')}` | `{c.get('descriptor_06e4_bytes_written')}` | `{c.get('transfer_flag_06e0_f0')}` | `{c.get('ok')}` |"
        )
    lines += [
        "",
        "## Handoff",
        "",
        "The next proof target is now narrower: drive the already-proven firmware READ CDB parser path ending at `0x9001E541 -> 0x05EC` into the descriptor setup function at `0x9001B54E`, then through the gate bit at `0x05B6` into `0x9001B5EF`.",
        "",
    ]
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text("\n".join(lines), encoding="utf-8")


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description="Probe GDR-8050L descriptor gate 0x05B6 -> 0x9001B5EF")
    ap.add_argument("--config", default="gdr8050l_auth_v256_stateful_relock_keep_loaded_iscsi_config.json")
    ap.add_argument("--firmware")
    ap.add_argument("--mn103s-emu", default="mn103s_emu.py")
    ap.add_argument("--out-prefix", default="gdr8050l_auth_v256_firmware_descriptor_gate_probe_report")
    ap.add_argument("--json", default="gdr8050l_auth_v256_firmware_descriptor_gate_probe_report.json")
    ap.add_argument("--md", default="gdr8050l_auth_v256_firmware_descriptor_gate_probe_report.md")
    ap.add_argument("--case-dir")
    ap.add_argument("--timeout", type=int, default=30)
    args = ap.parse_args(argv)

    cwd = Path.cwd()
    config_path = Path(args.config)
    if not config_path.is_absolute():
        config_path = cwd / config_path
    config = load_json(config_path) if config_path.exists() else {}
    firmware = resolve_firmware(config, config_path, args.firmware)
    if not firmware or not Path(firmware).exists():
        report = {
            "version": VERSION,
            "ok": False,
            "config": str(config_path),
            "firmware": str(firmware),
            "firmware_resolved": False,
            "error": "firmware not found; run analyzer or pass --firmware",
            "firmware_derived_delta": False,
        }
        write_json(Path(args.json), report)
        write_md(Path(args.md), report)
        print(f"[FIRMWARE-DESCRIPTOR-GATE] ok=false firmware_missing={firmware}")
        return 1

    mn103s_emu = Path(args.mn103s_emu)
    if not mn103s_emu.is_absolute():
        mn103s_emu = cwd / mn103s_emu
    case_dir = Path(args.case_dir) if args.case_dir else Path(f"{args.out_prefix}_cases")
    if not case_dir.is_absolute():
        case_dir = cwd / case_dir
    case_dir.mkdir(parents=True, exist_ok=True)

    cases_out = []
    for case in CASES:
        cfg = case_config(config, firmware, case, case_dir)
        cfg_path = case_dir / f"{case['name']}.config.json"
        out_path = case_dir / f"{case['name']}.output.txt"
        rc = run_case(mn103s_emu, cfg, cfg_path, out_path, args.timeout)
        cases_out.append(analyze_case(Path(cfg["trace"]["out_csv"]), case, rc, out_path))

    copy_cases = [c for c in cases_out if c.get("expect_copy")]
    skip_cases = [c for c in cases_out if not c.get("expect_copy")]
    descriptor_setup_to_builder_proven = bool(copy_cases) and all(c.get("descriptor_copy_entry_hit") and c.get("descriptor_06e4_lba_match") and c.get("transfer_flag_06e0_f0") for c in copy_cases)
    descriptor_gate_negative_proven = bool(skip_cases) and all((not c.get("descriptor_copy_entry_hit")) and (not c.get("transfer_flag_06e0_f0")) for c in skip_cases)
    ok = all(c.get("ok") for c in cases_out) and descriptor_setup_to_builder_proven and descriptor_gate_negative_proven
    report = {
        "version": VERSION,
        "config": str(config_path),
        "firmware": str(firmware),
        "firmware_resolved": True,
        "mn103s_emu": str(mn103s_emu),
        "case_dir": str(case_dir),
        "case_count": len(cases_out),
        "passed_count": sum(1 for c in cases_out if c.get("ok")),
        "descriptor_setup_entry_pc": hex32(DESCRIPTOR_SETUP_ENTRY_PC),
        "descriptor_gate_test_pc": hex32(DESCRIPTOR_GATE_TEST_PC),
        "descriptor_copy_entry_pc": hex32(DESCRIPTOR_COPY_ENTRY_PC),
        "descriptor_gate_sram": "0x05B6",
        "descriptor_gate_bit_immediate": GATE_BIT_IMMEDIATE,
        "descriptor_gate_bit_mask": GATE_BIT_MASK,
        "descriptor_gate_bit_mask_hex": hex8(GATE_BIT_MASK),
        "descriptor_gate_proven": descriptor_gate_negative_proven and descriptor_setup_to_builder_proven,
        "descriptor_setup_to_builder_proven": descriptor_setup_to_builder_proven,
        "descriptor_gate_negative_proven": descriptor_gate_negative_proven,
        "firmware_descriptor_builder_seeded_lba_proven": descriptor_setup_to_builder_proven,
        "firmware_derived_delta": False,
        "proof_scope": "seeded_05ec_through_9001b54e_gate_to_9001b5ef_descriptor_builder_only_not_full_live_read_route",
        "cases": cases_out,
        "ok": ok,
    }
    json_path = Path(args.json)
    md_path = Path(args.md)
    if not json_path.is_absolute():
        json_path = cwd / json_path
    if not md_path.is_absolute():
        md_path = cwd / md_path
    write_json(json_path, report)
    write_md(md_path, report)
    print(f"[FIRMWARE-DESCRIPTOR-GATE] cases={len(cases_out)} passed={report['passed_count']} gate_proven={str(report['descriptor_gate_proven']).lower()} setup_to_builder={str(descriptor_setup_to_builder_proven).lower()} firmware_derived_delta=false ok={str(ok).lower()}")
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())
