#!/usr/bin/env python3
"""Run a runtime-only GDR-8050L Xbox auth-probing packet sequence.

No Stage6/profile metadata is used.  v97 keeps v95 transport work and adds raw Xbox .xdvd sidecar loading:
  * READ CAPACITY(10) before and after the auth-flow scaffold.
  * MODE SELECT(10) 28-byte page-0x3E data-out injection scaffolds.
  * capacity comparison JSON so a true unlock can be detected as a size change.

The default MODE SELECT payloads are scaffolds only.  They prove the data-out
path shape; they are not guaranteed cryptographic challenge responses.  Use
--mode-select-data1/2/3 to supply captured or calculated 28-byte payloads.

v100 safety change: sidecar post-auth state defaults to locked. Use
--xdvd-post-state unlocked or --xdvd-force-unlocked-after only for a forced
sidecar/plumbing sanity check; that is not proof that firmware authentication
completed.
"""
from __future__ import annotations

import argparse
import subprocess
import sys
from pathlib import Path

VERSION = "v100"


def resolve_default_bin(workspace: Path) -> Path | None:
    candidates = [
        workspace / "firmware" / "GDR-8050L" / "8050L_fw_plaintext_256k.bin",
        workspace / "GDR-8050L_0L23_5af3ae9b_firmware_4c61eb12_plaintext_7cab0f5c.bin",
    ]
    for p in candidates:
        if p.exists():
            return p
    return None


def _hex_args_from_text(text: str | None) -> list[str]:
    if not text:
        return []
    if text.startswith('@'):
        return [text]
    p = Path(text)
    if p.exists() and p.is_file():
        return [str(p)]
    return [x for x in text.replace(',', ' ').replace(';', ' ').split() if x]


def run_packet(ns: argparse.Namespace, name: str, packet_alias: str, extra: list[str] | None = None, state: str | None = None) -> tuple[int, Path | None]:
    script_dir = Path(__file__).resolve().parent
    runner = script_dir / "run_direct_firmware.py"
    trace_path = Path(f"{ns.out_prefix}_{name}_v100_trace.csv")
    cmd = [
        ns.python, str(runner),
        "--workspace", str(ns.workspace),
        "--model", ns.model,
        "--bin", str(ns.bin),
        "--base", ns.base,
        "--entry", ns.entry,
        "--max-cycles", str(ns.max_cycles),
        "--trace-csv", str(trace_path),
        "--preset", "gdr8050l-packet-full-cdb",
        "--packet-cdb", packet_alias,
        "--auth-watch",
        "--trace-summary",
    ]
    if ns.iso:
        cmd += ["--iso", str(ns.iso)]
    if getattr(ns, "xdvd_sidecar", None):
        cmd += ["--xdvd-sidecar", str(ns.xdvd_sidecar), "--xdvd-state", str(state or ns.xdvd_state)]
    if getattr(ns, "continue_after_timer", False):
        cmd += ["--clear-preset-stops"]
    if extra:
        cmd += extra
    print(f"\n[AUTH-SEQ] {name} packet={packet_alias}")
    print("[RUN] " + " ".join(cmd), flush=True)
    rc = subprocess.run(cmd).returncode
    if rc != 0:
        return rc, None
    extractor = script_dir / "extract_gdr_handshake.py"
    out_json = Path(f"{ns.out_prefix}_{name}_handshake_v100.json")
    out_all = Path(f"{ns.out_prefix}_{name}_df70_all_v100.bin")
    out_post = Path(f"{ns.out_prefix}_{name}_df70_after_packet_v100.bin")
    ecmd = [
        ns.python, str(extractor), str(trace_path),
        "--out-json", str(out_json),
        "--out-df70", str(out_all),
        "--out-df70-after-packet", str(out_post),
    ]
    print("[EXTRACT] " + " ".join(ecmd), flush=True)
    erc = subprocess.run(ecmd).returncode
    return erc, out_json if erc == 0 else None


def main(argv: list[str]) -> int:
    ap = argparse.ArgumentParser(description="Run GDR-8050L runtime-only auth/capacity discovery sequence")
    ap.add_argument("--workspace", required=True, type=Path)
    ap.add_argument("--bin", type=Path, help="GDR-8050L plaintext firmware binary; defaults to common workspace paths")
    ap.add_argument("--iso", type=Path, help="optional ISO metadata for packet runs")
    ap.add_argument("--model", default="GDR-8050L")
    ap.add_argument("--base", default="0x90000000")
    ap.add_argument("--entry", default="0x90000020")
    ap.add_argument("--max-cycles", type=int, default=70000)
    ap.add_argument("--out-prefix", default="gdr8050l_auth_v100")
    ap.add_argument("--minimal", action="store_true", help="run only read-cap-before, 5A, AD, 55 scaffold, 5A, read-cap-after")
    ap.add_argument("--continue-after-timer", action="store_true", help="suppress preset stop at 0x90008824 so traces can continue past the timer-done landmark")
    ap.add_argument("--skip-mode-select", action="store_true", help="skip MODE SELECT data-out probes")
    ap.add_argument("--mode-select-data1", help="explicit 28-byte payload for first MODE SELECT; hex string or file/@file")
    ap.add_argument("--mode-select-data2", help="explicit 28-byte payload for second MODE SELECT; hex string or file/@file")
    ap.add_argument("--mode-select-data3", help="explicit 28-byte payload for final/partition MODE SELECT; hex string or file/@file")
    ap.add_argument("--xdvd-sidecar", "--xdvd-json", dest="xdvd_sidecar", type=Path, help="raw Xbox .xdvd sidecar JSON with captured state responses and external READ DVD STRUCTURE payloads")
    ap.add_argument("--xdvd-state", choices=["locked", "unlocked"], default="locked", help="default sidecar state for commands not categorized as pre/post")
    ap.add_argument("--xdvd-pre-state", choices=["locked", "unlocked"], default="locked", help="sidecar state for pre-auth READ CAPACITY/MODE SENSE/control commands")
    ap.add_argument("--xdvd-post-state", choices=["locked", "unlocked"], default="locked", help="sidecar state for post-auth MODE SENSE and final READ CAPACITY commands; default locked avoids implying auth success")
    ap.add_argument("--xdvd-force-unlocked-after", action="store_true", help="shortcut for --xdvd-post-state unlocked; use only as a sidecar plumbing/control test, not as auth proof")
    ap.add_argument("--python", default=sys.executable)
    ns = ap.parse_args(argv[1:])
    if getattr(ns, "xdvd_force_unlocked_after", False):
        ns.xdvd_post_state = "unlocked"

    ns.workspace = ns.workspace.resolve()
    if ns.bin is None:
        default_bin = resolve_default_bin(ns.workspace)
        if default_bin is None:
            print("[-] could not find default GDR-8050L firmware binary; pass --bin", file=sys.stderr)
            return 2
        ns.bin = default_bin
    ns.bin = ns.bin.resolve()
    if ns.iso:
        ns.iso = ns.iso.resolve()
    if ns.xdvd_sidecar:
        ns.xdvd_sidecar = ns.xdvd_sidecar.resolve()
        if not ns.xdvd_sidecar.exists():
            print(f"[-] XDVD sidecar does not exist: {ns.xdvd_sidecar}", file=sys.stderr)
            return 2
    if not ns.bin.exists():
        print(f"[-] firmware binary does not exist: {ns.bin}", file=sys.stderr)
        return 2
    if ns.iso and not ns.iso.exists():
        print(f"[-] ISO does not exist: {ns.iso}", file=sys.stderr)
        return 2

    print(f"[AUTH-SEQ] version={VERSION} firmware={ns.bin} iso={ns.iso or '(none)'} xdvd={ns.xdvd_sidecar or '(none)'}")
    if ns.xdvd_sidecar and ns.xdvd_post_state == "unlocked":
        print("[XDVD] post-state=unlocked is a forced sidecar response selection; this validates plumbing but is not auth proof")
    elif ns.xdvd_sidecar:
        print("[XDVD] post-state=locked; capacity should remain locked unless a future persistent auth model changes state")

    steps: list[tuple[str, str, list[str], str]] = []
    pre = ns.xdvd_pre_state
    post = ns.xdvd_post_state
    default_state = ns.xdvd_state
    steps.append(("read_capacity_before", "read-capacity", ["--read-capacity-data-in"], pre))
    if not ns.minimal:
        steps.extend([
            ("test_unit_ready_control", "test-unit-ready", [], pre),
            ("inquiry_control", "inquiry", [], pre),
        ])
    steps.extend([
        ("mode_sense_auth_pre", "mode-sense-auth-xbox", [], pre),
        ("read_dvd_structure", "read-dvd-structure-xbox", [], default_state),
    ])
    if not ns.skip_mode_select:
        def ms_extra(stage: str, explicit: str | None) -> list[str]:
            if explicit:
                return ["--packet-data-out", *_hex_args_from_text(explicit)]
            return ["--mode-select-auth-stage", stage]
        steps.append(("mode_select_auth_challenge1", "mode-select-auth-xbox", ms_extra("challenge1", ns.mode_select_data1), default_state))
        steps.append(("mode_sense_auth_after_challenge1", "mode-sense-auth-xbox", [], post))
        if not ns.minimal:
            steps.append(("mode_select_auth_challenge2", "mode-select-auth-xbox", ms_extra("challenge2", ns.mode_select_data2), default_state))
            steps.append(("mode_sense_auth_after_challenge2", "mode-sense-auth-xbox", [], post))
            steps.append(("mode_select_auth_partition1", "mode-select-auth-xbox", ms_extra("partition1", ns.mode_select_data3), default_state))
            steps.append(("mode_sense_auth_final", "mode-sense-auth-xbox", [], post))
    else:
        steps.append(("mode_sense_auth_post", "mode-sense-auth-xbox", [], post))
    steps.append(("read_capacity_after", "read-capacity", ["--read-capacity-data-in"], post))

    jsons: list[Path] = []
    cap_before: Path | None = None
    cap_after: Path | None = None
    for name, alias, extra, state in steps:
        rc, out_json = run_packet(ns, name, alias, extra, state=state)
        if rc != 0:
            print(f"[-] auth sequence stopped at {name} rc={rc}", file=sys.stderr)
            return rc
        if out_json:
            jsons.append(out_json)
            if name == "read_capacity_before":
                cap_before = out_json
            elif name == "read_capacity_after":
                cap_after = out_json

    if len(jsons) >= 2:
        cmp_script = Path(__file__).resolve().parent / "compare_gdr_auth_candidates.py"
        out_cmp = Path(f"{ns.out_prefix}_auth_compare_v100.json")
        ccmd = [ns.python, str(cmp_script), *map(str, jsons), "--out-json", str(out_cmp)]
        print("\n[COMPARE] " + " ".join(ccmd), flush=True)
        rc = subprocess.run(ccmd).returncode
        if rc != 0:
            return rc
    if cap_before and cap_after:
        cap_script = Path(__file__).resolve().parent / "compare_gdr_read_capacity.py"
        out_cap = Path(f"{ns.out_prefix}_read_capacity_compare_v100.json")
        cap_cmd = [ns.python, str(cap_script), "--before", str(cap_before), "--after", str(cap_after), "--out-json", str(out_cap)]
        print("\n[READ-CAPACITY-COMPARE] " + " ".join(cap_cmd), flush=True)
        rc = subprocess.run(cap_cmd).returncode
        if rc != 0:
            return rc
    print("\n[OK] auth sequence complete")
    return 0


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