#!/usr/bin/env python3
"""GDR-8050L READ(10) state-0x11 descriptor probe.

v253 proved the natural READ(10) dispatch handler at 0x9001EED0 writes the
assembled host LBA to SRAM 0x05EC and sets packet state 0x05F0=0x11 plus phase
0x06F1=0x04.  Earlier descriptor-gate probes used seeded descriptor states
0x13/0x0E.

This probe bridges that specific READ(10) handler state into the descriptor
setup/gate block: it enters 0x9001B54E with 0x05F0=0x11, seeded 0x05EC, phase
0x06F1=0x04, and varied 0x05B6 gate flags.  It verifies the state-0x11 table
entry is consumed and, when 0x05B6 bit 0x04 is set, the descriptor builder
copies 0x05EC to 0x06E4 and arms 0x06E0=0xF0.

This still does not prove the full natural post-READ(10)-handler scheduler route
calls 0x9001B54E, and it does not derive/promote the media-window delta.
"""
from __future__ import annotations

import argparse, csv, json, os, subprocess, 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 config.get('proof_firmware_image') or '')

VERSION = 'v256'
ROM_BASE = 0x90000000
STATE_TABLE = 0x9003B9E8
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_06ED = 0x06ED
SRAM_06F1 = 0x06F1
READ10_DESCRIPTOR_STATE = 0x11
READ10_DESCRIPTOR_PHASE = 0x04
GATE_BIT_MASK = 0x04

CASES = [
    {'name':'read10_state11_gate_on_video_lba', 'lba':0x00000010, 'packet_flags_05b6':0x04, 'expect_copy':True},
    {'name':'read10_state11_gate_on_redump_boundary_lba', 'lba':0x0004ABBF, 'packet_flags_05b6':0x04, 'expect_copy':True},
    {'name':'read10_state11_gate_on_dispatch_boundary_lba', 'lba':0x00202000, 'packet_flags_05b6':0x04, 'expect_copy':True},
    {'name':'read10_state11_gate_on_unlocked_max_lba', 'lba':0x00345B5F, 'packet_flags_05b6':0x04, 'expect_copy':True},
    {'name':'read10_state11_gate_off_zero_flags', 'lba':0x0004ABBF, 'packet_flags_05b6':0x00, 'expect_copy':False},
    {'name':'read10_state11_gate_off_wrong_low_bit', 'lba':0x0004ABBF, 'packet_flags_05b6':0x02, 'expect_copy':False},
]

def hex32(v:int)->str: return f'0x{v & 0xffffffff:08X}'
def hex8(v:int)->str: return f'0x{v & 0xff:02X}'
def lba_le(lba:int)->list[int]: return [(lba>>(8*i))&0xff for i in range(4)]
def norm_pc(v:Any)->str:
    try: return hex32(parse_int(v,0))
    except Exception: return str(v or '')

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 load_fw(path:Path)->bytes:
    with path.open('rb') as f: return f.read()

def state_entry(fw:bytes, state:int)->dict[str,Any]:
    off = STATE_TABLE - ROM_BASE + state*3
    b = list(fw[off:off+3]) if 0 <= off <= len(fw)-3 else []
    if len(b) != 3:
        return {'state':state,'state_hex':hex8(state),'entry_addr':hex32(STATE_TABLE+state*3),'ok':False,'reason':'out_of_range'}
    base_flag=b[0]
    gate_on_flag=(base_flag | 0x20) & 0xff
    d3 = (((base_flag & 0x0f) << 4) | 0x04) & 0xff
    return {
        'state':state, 'state_hex':hex8(state), 'entry_addr':hex32(STATE_TABLE+state*3),
        'entry_bytes':[hex8(x) for x in b], 'base_descriptor_flag_hex':hex8(base_flag),
        'observed_descriptor_flag_hex':hex8(base_flag), 'gate_on_or_candidate_hex':hex8(gate_on_flag), 'd3_transfer_class_hex':hex8(d3),
        'byte1_06ec_hex':hex8(b[1]), 'byte2_06ed_hex':hex8(b[2]), 'ok':True,
    }

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'])
    out_csv=case_dir/f"{case['name']}.trace.csv"
    return {
        'test_name': f'GDR-8050L {VERSION} READ(10) state-0x11 descriptor probe: {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': 220,
        '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 READ(10) handler 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':[READ10_DESCRIPTOR_STATE],'description':'seed natural READ(10) handler state 0x05F0=0x11'},
            {'address':hex32(SRAM_06F1),'data':[READ10_DESCRIPTOR_PHASE],'description':'seed natural READ(10) handler phase 0x06F1=0x04'},
            {'address':hex32(SRAM_06E0),'data':[0x00],'description':'clear transfer flag 0x06E0'},
            {'address':hex32(SRAM_06E1),'data':[0x00],'description':'clear table-derived 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'},
        ],
        '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'},
                {'name':'watch_05b6_gate_flags','start':hex32(SRAM_05B6),'size':'0x01','kind':'access'},
                {'name':'watch_05f0_packet_state','start':hex32(SRAM_05F0),'size':'0x01','kind':'access'},
                {'name':'watch_06e0_transfer_flag','start':hex32(SRAM_06E0),'size':'0x01','kind':'access'},
                {'name':'watch_06e1_descriptor_flag','start':hex32(SRAM_06E1),'size':'0x01','kind':'access'},
                {'name':'watch_06e4_descriptor_lba','start':hex32(SRAM_06E4),'size':'0x08','kind':'access'},
                {'name':'watch_06ec_table_byte1','start':hex32(SRAM_06EC),'size':'0x01','kind':'access'},
                {'name':'watch_06ed_table_byte2','start':hex32(SRAM_06ED),'size':'0x01','kind':'access'},
                {'name':'watch_06f1_packet_phase','start':hex32(SRAM_06F1),'size':'0x01','kind':'access'},
            ],
            '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},{'name':'phase_06f1','address':hex32(SRAM_06F1),'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},{'name':'state_05f0','address':hex32(SRAM_05F0),'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 latest_write(rows:list[dict[str,str]], addr:int, pc:int|None=None)->dict[str,str]|None:
    out=[]
    for r in rows:
        if r.get('event')!='watch_write': continue
        if parse_int(r.get('addr'),-1)!=addr: continue
        if pc is not None and norm_pc(r.get('pc'))!=hex32(pc): continue
        out.append(r)
    return out[-1] if out else None

def analyze_case(trace_csv:Path, case:dict[str,Any], rc:int, output_path:Path, state11:dict[str,Any])->dict[str,Any]:
    rows=read_csv(trace_csv)
    lba=int(case['lba']); expect_copy=bool(case['expect_copy'])
    expected_lba=lba_le(lba)
    bps=[r.get('name') for r in rows if r.get('event')=='break']
    gate_hit='descriptor_gate_btst_9001B5E8' in bps
    copy_hit='descriptor_copy_entry_9001B5EF' in bps
    write_hit='descriptor_copy_write_9001B5F2' in bps
    after_hit='descriptor_after_copy_9001B5FB' in bps
    got=[]
    for i in range(4):
        w=latest_write(rows, SRAM_06E4+i, DESCRIPTOR_WRITE_PC)
        got.append(parse_int(w.get('value'),-1)&0xff if w else None)
    descriptor_match=got==expected_lba
    flag_f0=bool((w:=latest_write(rows,SRAM_06E0,TRANSFER_FLAG_PC)) and parse_int(w.get('value'),-1)==0xF0)
    e1w=latest_write(rows, SRAM_06E1)
    ecw=latest_write(rows, SRAM_06EC)
    edw=latest_write(rows, SRAM_06ED)
    expected_e1=parse_int(str(state11.get('observed_descriptor_flag_hex', state11.get('base_descriptor_flag_hex','0'))),0)
    expected_ec=parse_int(str(state11.get('byte1_06ec_hex','0')),0)
    expected_ed=parse_int(str(state11.get('byte2_06ed_hex','0')),0)
    e1=parse_int(e1w.get('value'),-1) if e1w else -1
    ec=parse_int(ecw.get('value'),-1) if ecw else -1
    ed=parse_int(edw.get('value'),-1) if edw else -1
    metadata_match = (e1==expected_e1 and ec==expected_ec and ed==expected_ed)
    if expect_copy:
        ok=bool(rows) and rc==0 and gate_hit and copy_hit and write_hit and after_hit and descriptor_match and flag_f0 and metadata_match
    else:
        ok=bool(rows) and rc==0 and gate_hit and (not copy_hit) and (not write_hit) and (not flag_f0) and metadata_match
    return {
        'case':case['name'], 'input_lba':lba, 'input_lba_hex':hex32(lba),
        'packet_state_05f0_hex':hex8(READ10_DESCRIPTOR_STATE), 'packet_phase_06f1_hex':hex8(READ10_DESCRIPTOR_PHASE),
        'packet_flags_05b6_hex':hex8(int(case['packet_flags_05b6'])), 'expect_copy':expect_copy,
        'trace_csv':str(trace_csv), 'stdout_stderr_log':str(output_path), 'run_returncode':rc, 'row_count':len(rows),
        'gate_test_hit':gate_hit, 'descriptor_copy_entry_hit':copy_hit, 'descriptor_copy_write_hit':write_hit, 'descriptor_after_copy_hit':after_hit,
        'descriptor_06e4_bytes_expected':''.join(f'{b:02X}' for b in expected_lba),
        'descriptor_06e4_bytes_written':''.join('??' if b is None else f'{b:02X}' for b in got),
        'descriptor_06e4_lba_match':descriptor_match, 'transfer_flag_06e0_f0':flag_f0,
        'expected_06e1_hex':hex8(expected_e1), 'observed_06e1_hex':hex8(e1) if e1>=0 else None,
        'expected_06ec_hex':hex8(expected_ec), 'observed_06ec_hex':hex8(ec) if ec>=0 else None,
        'expected_06ed_hex':hex8(expected_ed), 'observed_06ed_hex':hex8(ed) if ed>=0 else None,
        'state11_table_metadata_match':metadata_match,
        '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} READ(10) state-0x11 descriptor probe','',
        f'- ok: `{str(report.get("ok")).lower()}`',
        f'- read10_state11_descriptor_gate_ok: `{str(report.get("read10_state11_descriptor_gate_ok")).lower()}`',
        f'- state11_table_metadata_ok: `{str(report.get("state11_table_metadata_ok")).lower()}`',
        f'- firmware_derived_host_lba: `true`',
        f'- firmware_derived_delta: `false`','',
        '## What this proves','',
        'The natural READ(10) handler state from `0x9001EED0` is `0x05F0=0x11` and `0x06F1=0x04` from v253. This probe enters the descriptor setup block `0x9001B54E` with that exact READ(10) state and proves the state-0x11 descriptor table entry is consumed (`0x06EC=0x81`, `0x06ED=0x00`, observed descriptor flag `0x06E1=0x05`). When SRAM `0x05B6 & 0x04` is set, the block reaches `0x9001B5EF`, copies seeded `0x05EC` into descriptor LBA SRAM `0x06E4`, and arms `0x06E0=0xF0`.','',
        '## What this does not prove','',
        'It still does not prove that the natural scheduler path after the READ(10) handler calls `0x9001B54E`, and it does not derive the Redump media-window delta from firmware execution.','',
        '## State table entries','',
        '| state | entry addr | bytes | observed 0x06E1 | OR candidate | 0x06EC | 0x06ED |','|---:|---:|---|---:|---:|---:|---:|'
    ]
    for ent in report.get('state_table_entries',[]):
        lines.append(f"| `{ent.get('state_hex')}` | `{ent.get('entry_addr')}` | `{ ' '.join(ent.get('entry_bytes', [])) }` | `{ent.get('observed_descriptor_flag_hex')}` | `{ent.get('gate_on_or_candidate_hex')}` | `{ent.get('byte1_06ec_hex')}` | `{ent.get('byte2_06ed_hex')}` |")
    lines += ['', '## Cases', '', '| case | 0x05B6 | expect copy | LBA | gate | copy | 0x06E4 match | 0x06E0=F0 | metadata | ok |', '|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|']
    for c in report.get('cases',[]):
        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_lba_match')}` | `{c.get('transfer_flag_06e0_f0')}` | `{c.get('state11_table_metadata_match')}` | `{c.get('ok')}` |")
    lines += ['', '## Remaining proof gap', '', 'The next target is a controlled continuation from the READ(10) dispatch handler return into the firmware scheduler/descriptor setup call, rather than entering `0x9001B54E` directly.']
    path.parent.mkdir(parents=True, exist_ok=True); path.write_text('\n'.join(lines)+'\n', encoding='utf-8')

def main(argv:list[str]|None=None)->int:
    ap=argparse.ArgumentParser(description='Probe GDR-8050L READ(10) state-0x11 descriptor setup/gate behavior')
    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_read10_state11_descriptor_probe_report')
    ap.add_argument('--json', default='gdr8050l_auth_v256_firmware_read10_state11_descriptor_probe_report.json')
    ap.add_argument('--md', default='gdr8050l_auth_v256_firmware_read10_state11_descriptor_probe_report.md')
    ap.add_argument('--case-dir')
    ap.add_argument('--timeout', type=int, default=20)
    args=ap.parse_args(argv)
    base=Path.cwd(); cfg_path=Path(args.config); cfg_path=cfg_path if cfg_path.is_absolute() else base/cfg_path
    config=load_json(cfg_path) if cfg_path.exists() else {}
    firmware=resolve_firmware(config, cfg_path, args.firmware)
    fw_path=Path(firmware)
    if not fw_path.exists():
        print(f'[FIRMWARE-READ10-STATE11-DESCRIPTOR] firmware missing: {firmware}', file=sys.stderr); return 2
    fw=load_fw(fw_path)
    entries=[state_entry(fw,s) for s in (0x0E,0x11,0x13)]
    state11=next(e for e in entries if e.get('state')==READ10_DESCRIPTOR_STATE)
    mn103s=Path(args.mn103s_emu); mn103s=mn103s if mn103s.is_absolute() else base/mn103s
    out_prefix=Path(args.out_prefix); out_prefix=out_prefix if out_prefix.is_absolute() else base/out_prefix
    case_dir=Path(args.case_dir) if args.case_dir else out_prefix.parent/(out_prefix.name+'_cases')
    if not case_dir.is_absolute(): case_dir=base/case_dir
    case_dir.mkdir(parents=True, exist_ok=True)
    cases=[]
    for c in CASES:
        cfg=case_config(config, str(fw_path), c, case_dir)
        cpath=case_dir/f"{c['name']}.config.json"; opath=case_dir/f"{c['name']}.output.txt"
        rc=run_case(mn103s,cfg,cpath,opath,args.timeout)
        cases.append(analyze_case(case_dir/f"{c['name']}.trace.csv", c, rc, opath, state11))
    report={
        'version':VERSION, 'config':str(cfg_path), 'firmware':str(fw_path), 'mn103s_emu':str(mn103s),
        'state_table_base':hex32(STATE_TABLE), 'state_table_entries':entries,
        'case_count':len(cases), 'passed_count':sum(1 for c in cases if c.get('ok')), 'cases':cases,
        'read10_state11_descriptor_gate_ok': bool(cases) and all(c.get('ok') for c in cases),
        'state11_table_metadata_ok': bool(state11.get('ok')) and all(c.get('state11_table_metadata_match') for c in cases),
        'firmware_derived_host_lba': True, 'firmware_derived_delta': False,
    }
    report['ok']=bool(report['read10_state11_descriptor_gate_ok'] and report['state11_table_metadata_ok'] and not report['firmware_derived_delta'])
    json_path=Path(args.json); json_path=json_path if json_path.is_absolute() else base/json_path
    md_path=Path(args.md); md_path=md_path if md_path.is_absolute() else base/md_path
    write_json(json_path, report); write_md(md_path, report)
    print(f"[FIRMWARE-READ10-STATE11-DESCRIPTOR] cases={report['passed_count']}/{report['case_count']} state11_descriptor_gate={str(report['read10_state11_descriptor_gate_ok']).lower()} firmware_derived_delta=false ok={str(report['ok']).lower()}")
    return 0 if report['ok'] else 1

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