#!/usr/bin/env python3
"""v122 GDR-8050L atomic unlock log replay matrix.

Runs each captured command from the Red Faction II unlock log as a bounded
single-packet firmware probe.  This is still not one persistent multi-command
emulator session; it proves all packet DATA IN and DATA OUT phases for the full
captured handshake shape while returning each probe to the post-timer main loop.
"""
from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path

WORKSPACE = r"D:\vscode-workspace\xbox-dvd-tools-v2"
FWBIN = r"D:\vscode-workspace\xbox-dvd-tools-v2\firmware\GDR-8050L\8050L_fw_plaintext_256k.bin"

SECTION_RE = re.compile(r"^--- (?P<kind>Sending CDB|Received Data|Sending Data) \[(?P<label>[^\]]+)\] \((?P<n>\d+) bytes\) ---")
HEX_RE = re.compile(r"\b[0-9A-Fa-f]{2}\b")


def parse_log(path: Path) -> dict[tuple[str, str], bytes]:
    """Return latest occurrence of each (kind,label) section from log."""
    out: dict[tuple[str, str], bytes] = {}
    lines = path.read_text(encoding='utf-8', errors='replace').splitlines()
    i = 0
    while i < len(lines):
        m = SECTION_RE.match(lines[i].strip())
        if not m:
            i += 1
            continue
        kind = m.group('kind')
        label = m.group('label')
        n = int(m.group('n'))
        i += 1
        hex_toks: list[str] = []
        while i < len(lines) and not lines[i].startswith('--- '):
            if lines[i].startswith('---') or lines[i].startswith('----------------'):
                i += 1
                break
            hex_toks.extend(HEX_RE.findall(lines[i]))
            i += 1
        data = bytes(int(x, 16) for x in hex_toks[:n])
        if len(data) == n:
            out[(kind, label)] = data
    return out


def fmt_hex(data: bytes) -> str:
    return ' '.join(f'{b:02X}' for b in data)


def run_checked(cmd: list[str], *, cwd: Path, stdout_file: Path | None = None) -> None:
    if stdout_file is None:
        res = subprocess.run(cmd, cwd=str(cwd))
    else:
        with stdout_file.open('w', encoding='utf-8', errors='replace') as fh:
            res = subprocess.run(cmd, cwd=str(cwd), stdout=fh, stderr=subprocess.STDOUT)
    if res.returncode != 0:
        raise SystemExit(res.returncode)


def run_probe(script_dir: Path, args: argparse.Namespace, probe: dict) -> None:
    tag = probe['tag']
    title = probe['title']
    cdb = probe['cdb']
    direction = probe['direction']
    state = probe.get('state', 'locked')
    max_cycles = str(probe.get('max_cycles', 120000))
    payload = probe.get('payload', b'')
    stem = f'gdr8050l_auth_v122_{tag}_post_timer'
    trace_csv = f'{stem}_trace.csv'
    config_json = f'{stem}_config.json'
    output_txt = script_dir / f'{stem}.output.txt'

    print(f"\n[V122] Probe: {tag} direction={direction} CDB={fmt_hex(cdb)}", flush=True)

    payload_file = None
    if payload:
        suffix = 'data_out.bin' if direction == 'out' else 'data_in.bin'
        payload_file = script_dir / f'{stem}_{suffix}'
        payload_file.write_bytes(payload)

    run_cmd = [
        sys.executable, 'run_direct_firmware.py',
        '--workspace', WORKSPACE,
        '--bin', FWBIN,
        '--base', '0x90000000',
        '--entry', '0x90000020',
        '--model', 'GDR-8050L',
        '--name', title,
        '--max-cycles', max_cycles,
        '--sp', '0x003FF000',
        '--preset', 'gdr8050l-packet-full-cdb',
        '--packet-cdb', *[f'0x{b:02X}' for b in cdb],
        '--gdr-hw-model',
        '--clear-preset-stops',
        '--stop-pc', '0x90006608',
        '--stop-pc-hit-count', '4',
        '--stop-pc-min-cycle', '51200',
        '--auth-watch',
        '--trace-csv', trace_csv,
        '--trace-summary',
        '--config-out', config_json,
    ]
    if args.iso:
        run_cmd.extend(['--iso', args.iso])
    if direction == 'in':
        if payload_file:
            run_cmd.extend(['--packet-data-in-file', str(payload_file)])
        else:
            run_cmd.extend(['--xdvd-sidecar', args.xdvd_json, '--xdvd-state', state])
        run_cmd.extend([
            '--packet-data-in-source-mode', 'stream',
            '--packet-data-in-source-no-prelude',
            '--packet-data-in-model-match-len', '8',
        ])
    elif direction == 'out':
        run_cmd.extend(['--packet-data-out-file', str(payload_file)])
    else:
        raise SystemExit(f'bad probe direction {direction!r}')

    run_checked(run_cmd, cwd=script_dir, stdout_file=output_txt)
    run_checked([sys.executable, 'analyze_gdr8050l_v109_readcap_route.py', trace_csv, '--out', f'{stem}_summary.md'], cwd=script_dir)
    run_checked([sys.executable, 'extract_gdr_handshake.py', trace_csv, '--out-json', f'{stem}_handshake.json', '--config', config_json, '--out-df70', f'{stem}_df70_all.bin', '--out-df70-after-packet', f'{stem}_df70_after_packet.bin'], cwd=script_dir)

    if direction == 'in':
        run_checked([sys.executable, 'analyze_gdr_chunked_data_in.py', trace_csv, '--config', config_json, '--out', f'{stem}_verify.md', '--out-payload', f'{stem}_payload_reconstructed.bin'], cwd=script_dir)
    else:
        run_checked([sys.executable, 'analyze_gdr_packet_data_out.py', trace_csv, '--expected-file', str(payload_file), '--out', f'{stem}_data_out_verify.md', '--out-payload', f'{stem}_data_out_reconstructed.bin'], cwd=script_dir)

    print(f'[OK] {tag} passed.', flush=True)


def require(sections: dict[tuple[str, str], bytes], kind: str, label: str) -> bytes:
    key = (kind, label)
    if key not in sections:
        raise SystemExit(f'[-] missing {kind} [{label}] in unlock log')
    return sections[key]


def main() -> int:
    ap = argparse.ArgumentParser(description='Run GDR-8050L v122 captured unlock-log packet replay')
    ap.add_argument('xdvd_json', help='Path to .xdvd.json sidecar')
    ap.add_argument('iso', nargs='?', help='Optional ISO path for metadata')
    ap.add_argument('--log', help='Unlock log path; default is .log next to xdvd_json')
    args = ap.parse_args()

    script_dir = Path(__file__).resolve().parent
    xdvd_path = Path(args.xdvd_json)
    log_path = Path(args.log) if args.log else xdvd_path.with_suffix('.log')
    if not log_path.exists():
        raise SystemExit(f'[-] unlock log not found: {log_path}; pass --log PATH')

    print(f'[V122] XDVD sidecar: {args.xdvd_json}', flush=True)
    if args.iso:
        print(f'[V122] ISO metadata: {args.iso}', flush=True)
    print(f'[V122] Unlock log: {log_path}', flush=True)
    print('[V122] Captured unlock-log replay: data-in + data-out packet phases.', flush=True)
    print('[V122] Each probe is independent/reset-start; this is not a persistent multi-command session yet.', flush=True)

    sections = parse_log(log_path)

    probes = [
        {'tag':'step01_locked_read_capacity', 'title':'GDR-8050L v122 Step 1 locked READ CAPACITY', 'cdb':require(sections,'Sending CDB','Step 1: Init Read Cap'), 'direction':'in', 'payload':require(sections,'Received Data','Step 1: Init Read Cap')},
        {'tag':'step02_locked_mode_sense_3e', 'title':'GDR-8050L v122 Step 2 locked MODE SENSE 3E', 'cdb':require(sections,'Sending CDB','Step 2: Mode Sense'), 'direction':'in', 'payload':require(sections,'Received Data','Step 2: Mode Sense')},
        {'tag':'step03_read_dvd_structure_ad_c0', 'title':'GDR-8050L v122 Step 3 READ DVD STRUCTURE AD C0', 'cdb':require(sections,'Sending CDB','Step 3: Read DVD Struct 0xAD'), 'direction':'in', 'payload':require(sections,'Received Data','Step 3: Read DVD Struct 0xAD'), 'max_cycles':120000},
        {'tag':'step04_mode_select_chal1', 'title':'GDR-8050L v122 Step 4 MODE SELECT challenge 1', 'cdb':require(sections,'Sending CDB','Step 4: Mode Select Chal 1'), 'direction':'out', 'payload':require(sections,'Sending Data','Step 4: Mode Select Chal 1')},
        {'tag':'step05_mode_sense_verify1', 'title':'GDR-8050L v122 Step 5 MODE SENSE verify 1', 'cdb':require(sections,'Sending CDB','Step 5: Mode Sense Verify 1'), 'direction':'in', 'payload':require(sections,'Received Data','Step 5: Mode Sense Verify 1')},
        {'tag':'step06_mode_select_chal2', 'title':'GDR-8050L v122 Step 6 MODE SELECT challenge 2', 'cdb':require(sections,'Sending CDB','Step 6: Mode Select Chal 2'), 'direction':'out', 'payload':require(sections,'Sending Data','Step 6: Mode Select Chal 2')},
        {'tag':'step07_mode_sense_verify2', 'title':'GDR-8050L v122 Step 7 MODE SENSE verify 2', 'cdb':require(sections,'Sending CDB','Step 7: Mode Sense Verify 2'), 'direction':'in', 'payload':require(sections,'Received Data','Step 7: Mode Sense Verify 2')},
        {'tag':'step08_mode_select_partition1_unlock', 'title':'GDR-8050L v122 Step 8 MODE SELECT partition1 unlock', 'cdb':require(sections,'Sending CDB','Step 8: Mode Select Part1 Unlock'), 'direction':'out', 'payload':require(sections,'Sending Data','Step 8: Mode Select Part1 Unlock')},
        {'tag':'step09_sticky_descramble_mode31', 'title':'GDR-8050L v122 Step 9 MODE SELECT(6) sticky descrambling', 'cdb':require(sections,'Sending CDB','Step 9: Sticky Descrambling'), 'direction':'out', 'payload':require(sections,'Sending Data','Step 9: Sticky Descrambling')},
        {'tag':'step10_unlocked_read_capacity', 'title':'GDR-8050L v122 Step 10 unlocked READ CAPACITY', 'cdb':require(sections,'Sending CDB','Step 10: Final Verify'), 'direction':'in', 'payload':require(sections,'Received Data','Step 10: Final Verify')},
    ]

    for probe in probes:
        run_probe(script_dir, args, probe)

    print('[OK] v122 captured unlock-log replay complete.', flush=True)
    print('[NEXT] Zip/send gdr8050l_auth_v122_*output.txt, *_summary.md, *_handshake.json, *_verify.md, *_data_out_verify.md, and *_payload_reconstructed.bin/*_data_out_reconstructed.bin.', flush=True)
    return 0


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