#!/usr/bin/env python3
"""GDR-8050L READ descriptor-state bridge probe.

This is a deliberately narrow offline proof step between the already proven
READ CDB parser and the descriptor setup/gate block.

v250 proved the raw descriptor builder copies seeded SRAM 0x05EC -> 0x06E4.
v251 proved the descriptor setup/gate at 0x9001B54E reaches that builder when
0x05B6 mask 0x04 is set.  v253 adds two checks:

1. Parse the READ(10)/READ(12) CPU sweep traces and verify the firmware READ
   parser writes the descriptor-state prerequisites:
     0x05EC = assembled host LBA
     0x05F0 = 0x0E
     0x05D0 = CDB opcode (0x28 or 0xA8)
     0x06F1 = 0x04

2. Re-enter the descriptor setup/gate using the exact READ parser state
   0x05F0=0x0E and seeded READ opcode, then verify 0x05EC flows through
   0x9001B54E/0x9001B5EF into descriptor SRAM 0x06E4 when 0x05B6 bit 0x04 is
   set, and does not when that gate bit is clear.

This still does not prove the full natural live READ route calls 0x9001B54E,
and it does not promote the Redump media-window delta to firmware-derived.
"""
from __future__ import annotations

import argparse
import csv
import json
import os
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
READ_CDB_LBA_STORE_PC = '0x9001E541'
READ_STATE_WRITE_PC = '0x9001E546'
READ_OPCODE_SHADOW_WRITE_PC = '0x9001E551'
READ_PHASE_WRITE_PC = '0x9001E556'
SRAM_05B6 = 0x05B6
SRAM_05D0 = 0x05D0
SRAM_05F0 = 0x05F0
SRAM_05EC = 0x05EC
SRAM_06E0 = 0x06E0
SRAM_06E1 = 0x06E1
SRAM_06E4 = 0x06E4
SRAM_06EC = 0x06EC
SRAM_06F1 = 0x06F1
READ_DESCRIPTOR_STATE = 0x0E
READ_DESCRIPTOR_PHASE = 0x04
GATE_BIT_MASK = 0x04

CASES: list[dict[str, Any]] = [
    {'name': 'read10_state0e_gate_on_video_lba', 'kind': 'READ(10)', 'opcode': 0x28, 'lba': 0x00000010, 'packet_flags_05b6': 0x04, 'expect_copy': True},
    {'name': 'read12_state0e_gate_on_sample_lba', 'kind': 'READ(12)', 'opcode': 0xA8, 'lba': 0x0004ABBF, 'packet_flags_05b6': 0x04, 'expect_copy': True},
    {'name': 'read10_state0e_gate_on_boundary_lba', 'kind': 'READ(10)', 'opcode': 0x28, 'lba': 0x00202000, 'packet_flags_05b6': 0x04, 'expect_copy': True},
    {'name': 'read10_state0e_gate_on_unlocked_max_lba', 'kind': 'READ(10)', 'opcode': 0x28, 'lba': 0x00345B5F, 'packet_flags_05b6': 0x04, 'expect_copy': True},
    {'name': 'read10_state0e_gate_off_zero_flags', 'kind': 'READ(10)', 'opcode': 0x28, 'lba': 0x0004ABBF, 'packet_flags_05b6': 0x00, '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 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 resolve_case_dir(sweep_json: dict[str, Any], explicit: str | None, base_dir: Path) -> Path | None:
    if explicit:
        p = Path(explicit)
        return p if p.is_absolute() else base_dir / p
    raw = sweep_json.get('case_dir')
    if raw:
        p = Path(str(raw).replace('\\', os.sep))
        if p.exists():
            return p
        q = base_dir / p.name
        if q.exists():
            return q
    return None


def resolve_trace_path(case_report: dict[str, Any], case_dir: Path | None) -> Path | None:
    raw = case_report.get('trace_csv')
    if raw:
        p = Path(str(raw).replace('\\', os.sep))
        if p.exists():
            return p
        if case_dir:
            q = case_dir / p.name
            if q.exists():
                return q
    if case_dir and case_report.get('name'):
        q = case_dir / f"{case_report['name']}.trace.csv"
        if q.exists():
            return q
    return None


def latest_write(rows: list[dict[str, str]], addr: int, pc: str | None = None) -> dict[str, str] | None:
    matches = []
    for r in rows:
        if r.get('event') != 'watch_write':
            continue
        if parse_int(r.get('addr'), -1) != addr:
            continue
        if pc and norm_pc(r.get('pc')) != pc:
            continue
        matches.append(r)
    return matches[-1] if matches else None


def parse_parser_prereq_case(case_report: dict[str, Any], trace_path: Path | None) -> dict[str, Any]:
    name = case_report.get('name') or (trace_path.stem if trace_path else '')
    expected_lba = parse_int(case_report.get('expected_host_lba'), 0)
    kind = str(case_report.get('kind') or '')
    expected_opcode = 0xA8 if '12' in kind else 0x28
    out: dict[str, Any] = {
        'case': name,
        'kind': kind,
        'expected_host_lba_hex': hex32(expected_lba),
        'expected_opcode_hex': hex8(expected_opcode),
        'trace_csv': str(trace_path) if trace_path else None,
        'exists': bool(trace_path and trace_path.exists()),
        'ok': False,
    }
    if not trace_path or not trace_path.exists():
        out['reason'] = 'trace_missing'
        return out
    rows = read_csv(trace_path)
    lba_writes = [latest_write(rows, SRAM_05EC + i, READ_CDB_LBA_STORE_PC) for i in range(4)]
    lba_bytes = [parse_int(w.get('value'), -1) if w else -1 for w in lba_writes]
    state_w = latest_write(rows, SRAM_05F0, READ_STATE_WRITE_PC)
    opcode_w = latest_write(rows, SRAM_05D0, READ_OPCODE_SHADOW_WRITE_PC)
    phase_w = latest_write(rows, SRAM_06F1, READ_PHASE_WRITE_PC)
    assembled = sum((b & 0xFF) << (8 * i) for i, b in enumerate(lba_bytes) if b >= 0)
    out.update({
        'row_count': len(rows),
        'lba_bytes_written': [hex8(b) if b >= 0 else None for b in lba_bytes],
        'assembled_05ec_lba_hex': hex32(assembled),
        'assembled_05ec_match': assembled == expected_lba and all(b >= 0 for b in lba_bytes),
        'read_descriptor_state_05f0': hex8(parse_int(state_w.get('value'), -1)) if state_w else None,
        'read_descriptor_state_match': bool(state_w and parse_int(state_w.get('value'), -1) == READ_DESCRIPTOR_STATE),
        'read_opcode_shadow_05d0': hex8(parse_int(opcode_w.get('value'), -1)) if opcode_w else None,
        'read_opcode_shadow_match': bool(opcode_w and parse_int(opcode_w.get('value'), -1) == expected_opcode),
        'read_packet_phase_06f1': hex8(parse_int(phase_w.get('value'), -1)) if phase_w else None,
        'read_packet_phase_match': bool(phase_w and parse_int(phase_w.get('value'), -1) == READ_DESCRIPTOR_PHASE),
    })
    out['ok'] = bool(out['assembled_05ec_match'] and out['read_descriptor_state_match'] and out['read_opcode_shadow_match'] and out['read_packet_phase_match'])
    return out


def parser_prereq_summary(sweep_json_path: Path | None, case_dir_arg: str | None, base_dir: Path) -> dict[str, Any]:
    if not sweep_json_path or not sweep_json_path.exists():
        return {'ok': False, 'reason': 'read_cdb_sweep_json_missing'}
    sweep = load_json(sweep_json_path)
    case_dir = resolve_case_dir(sweep, case_dir_arg, base_dir)
    cases = []
    for c in sweep.get('cases', []) or []:
        p = resolve_trace_path(c, case_dir)
        cases.append(parse_parser_prereq_case(c, p))
    return {
        'sweep_json': str(sweep_json_path),
        'case_dir': str(case_dir) if case_dir else None,
        'case_count': len(cases),
        'passed_count': sum(1 for c in cases if c.get('ok')),
        'read_parser_state_0e_proven': bool(cases) and all(c.get('read_descriptor_state_match') for c in cases),
        'read_parser_opcode_shadow_proven': bool(cases) and all(c.get('read_opcode_shadow_match') for c in cases),
        'read_parser_phase_04_proven': bool(cases) and all(c.get('read_packet_phase_match') for c in cases),
        'read_parser_lba_05ec_proven': bool(cases) and all(c.get('assembled_05ec_match') for c in cases),
        'cases': cases,
        'ok': bool(cases) and all(c.get('ok') for c in cases),
    }


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'])
    opcode = int(case['opcode'])
    out_csv = case_dir / f"{case['name']}.trace.csv"
    return {
        'test_name': f"GDR-8050L {VERSION} READ descriptor-state bridge: {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 READ parser assembled LBA {hex32(lba)} at SRAM 0x05EC'},
            {'address': hex32(SRAM_05B6), 'data': [flags & 0xFF], 'description': f'seed descriptor gate flags 0x05B6={hex8(flags)}'},
            {'address': hex32(SRAM_05F0), 'data': [READ_DESCRIPTOR_STATE], 'description': 'seed exact READ parser descriptor state 0x05F0=0x0E'},
            {'address': hex32(SRAM_05D0), 'data': [opcode & 0xFF], 'description': f'seed READ parser opcode shadow 0x05D0={hex8(opcode)}'},
            {'address': hex32(SRAM_06F1), 'data': [READ_DESCRIPTOR_PHASE], 'description': 'seed exact READ parser packet phase 0x06F1=0x04'},
            {'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'},
        ],
        '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': 'READ parser assembled LBA source'},
                {'name': 'watch_05b6_gate_flags', 'start': hex32(SRAM_05B6), 'size': '0x01', 'kind': 'access', 'description': 'descriptor gate flag byte'},
                {'name': 'watch_05d0_opcode_shadow', 'start': hex32(SRAM_05D0), 'size': '0x01', 'kind': 'access', 'description': 'READ parser opcode shadow'},
                {'name': 'watch_05f0_packet_state', 'start': hex32(SRAM_05F0), 'size': '0x01', 'kind': 'access', 'description': 'READ parser state / table selector'},
                {'name': 'watch_06f1_packet_phase', 'start': hex32(SRAM_06F1), 'size': '0x01', 'kind': 'access', 'description': 'READ parser packet phase'},
                {'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 descriptor bytes'},
            ],
            'breakpoints': [
                {'address': hex32(DESCRIPTOR_SETUP_ENTRY_PC), 'name': 'descriptor_setup_entry_9001B54E'},
                {'address': hex32(DESCRIPTOR_GATE_TEST_PC), 'name': 'descriptor_gate_btst_9001B5E8'},
                {'address': hex32(DESCRIPTOR_COPY_ENTRY_PC), 'name': 'descriptor_copy_entry_9001B5EF'},
                {'address': hex32(DESCRIPTOR_WRITE_PC), 'name': 'descriptor_copy_write_9001B5F2'},
                {'address': hex32(TRANSFER_FLAG_PC), 'name': 'descriptor_transfer_flag_9001B5F8'},
                {'address': hex32(AFTER_DESCRIPTOR_COPY_PC), 'name': 'descriptor_after_copy_9001B5FB'},
                {'address': hex32(FUNCTION_RETURN_PC), 'name': 'descriptor_setup_return_9001B66E'},
            ],
        },
    }


def analyze_state_case(trace_csv: Path, case: dict[str, Any], run_returncode: int, output_path: Path) -> dict[str, Any]:
    rows = read_csv(trace_csv)
    expected = lba_le(int(case['lba']))
    expect_copy = bool(case['expect_copy'])
    bps = [r for r in rows if r.get('event') == 'break']
    writes_06e4 = [r for r in rows if r.get('event') == 'watch_write' and parse_int(r.get('addr'), -1) in range(SRAM_06E4, SRAM_06E4 + 4)]
    got = [None, None, None, None]
    for r in writes_06e4:
        idx = parse_int(r.get('addr'), -1) - SRAM_06E4
        if 0 <= idx < 4:
            got[idx] = parse_int(r.get('value'), -1) & 0xFF
    copy_hit = any(norm_pc(r.get('pc')) == hex32(DESCRIPTOR_COPY_ENTRY_PC) for r in rows)
    write_hit = any(norm_pc(r.get('pc')) == hex32(DESCRIPTOR_WRITE_PC) for r in rows)
    after_hit = any(norm_pc(r.get('pc')) == hex32(AFTER_DESCRIPTOR_COPY_PC) for r in rows)
    gate_hit = any(norm_pc(r.get('pc')) == hex32(DESCRIPTOR_GATE_TEST_PC) for r in rows)
    flag_f0 = any(r.get('event') == 'watch_write' and parse_int(r.get('addr'), -1) == SRAM_06E0 and parse_int(r.get('value'), -1) == 0xF0 for r in rows)
    match = got == expected
    ok = (run_returncode == 0 and gate_hit and after_hit and ((expect_copy and copy_hit and write_hit and match and flag_f0) or ((not expect_copy) and (not copy_hit) and (not write_hit) and (not flag_f0))))
    return {
        'case': case['name'],
        'kind': case['kind'],
        'input_lba': int(case['lba']),
        'input_lba_hex': hex32(int(case['lba'])),
        'opcode_hex': hex8(int(case['opcode'])),
        'read_parser_state_05f0_hex': hex8(READ_DESCRIPTOR_STATE),
        'read_parser_phase_06f1_hex': hex8(READ_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': run_returncode,
        '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),
        'descriptor_06e4_bytes_written': ''.join(f'{b:02X}' if b is not None else '??' for b in got),
        'descriptor_06e4_lba_match': match,
        'transfer_flag_06e0_f0': flag_f0,
        'ok': ok,
    }


def run_state_cases(config: dict[str, Any], config_path: Path, firmware_arg: str | None, mn103s_emu: str, out_prefix: str, case_dir: Path, timeout: int) -> dict[str, Any]:
    firmware = resolve_firmware(config, config_path, firmware_arg)
    case_dir.mkdir(parents=True, exist_ok=True)
    results = []
    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"
        write_json(cfg_path, cfg)
        cmd = [sys.executable, mn103s_emu, str(cfg_path)]
        try:
            proc = subprocess.run(cmd, cwd=Path.cwd(), capture_output=True, text=True, timeout=timeout)
            out_path.write_text((proc.stdout or '') + ('\n' + proc.stderr if proc.stderr else ''), encoding='utf-8', errors='replace')
            rc = int(proc.returncode)
        except subprocess.TimeoutExpired as e:
            out_path.write_text((e.stdout or '') + '\n[TIMEOUT]\n' + (e.stderr or ''), encoding='utf-8', errors='replace')
            rc = 124
        results.append(analyze_state_case(Path(cfg['trace']['out_csv']), case, rc, out_path))
    return {
        'case_dir': str(case_dir),
        'case_count': len(results),
        'passed_count': sum(1 for r in results if r.get('ok')),
        'read_state_0e_descriptor_gate_copy_proven': all(r.get('ok') for r in results if r.get('expect_copy')),
        'read_state_0e_descriptor_gate_negative_proven': all(r.get('ok') for r in results if not r.get('expect_copy')),
        'cases': results,
        'ok': bool(results) and all(r.get('ok') for r in results),
    }


def write_md(path: Path, report: dict[str, Any]) -> None:
    parser = report.get('read_parser_prereq') or {}
    state = report.get('read_state_descriptor_gate') or {}
    lines = [
        f'# GDR-8050L {VERSION} READ descriptor-state bridge probe',
        '',
        f"- ok: `{str(report.get('ok')).lower()}`",
        f"- firmware_derived_host_lba: `{str(report.get('firmware_derived_host_lba')).lower()}`",
        f"- firmware_derived_delta: `{str(report.get('firmware_derived_delta')).lower()}`",
        f"- read_parser_prereq_ok: `{str(parser.get('ok')).lower()}`",
        f"- read_state_descriptor_gate_ok: `{str(state.get('ok')).lower()}`",
        '',
        '## What this proves',
        '',
        'The READ parser sweep already proved CDB LBA assembly into SRAM `0x05EC`.  This probe adds that the same parser writes the downstream descriptor-state prerequisites `0x05F0=0x0E`, `0x05D0=READ opcode`, and `0x06F1=0x04`.  It then re-enters the descriptor setup/gate with that exact READ parser state and proves the seeded `0x05EC` LBA is copied to descriptor SRAM `0x06E4` when SRAM `0x05B6` gate mask `0x04` is set.',
        '',
        'This still does not prove the natural live READ route calls `0x9001B54E`, and it does not promote the media-window delta.',
        '',
        '## READ parser prerequisite cases',
        '',
        '| case | kind | LBA | state 0x05F0 | opcode 0x05D0 | phase 0x06F1 | ok |',
        '|---|---|---:|---:|---:|---:|---:|',
    ]
    for c in parser.get('cases', []) or []:
        lines.append(f"| `{c.get('case')}` | `{c.get('kind')}` | `{c.get('assembled_05ec_lba_hex')}` | `{c.get('read_descriptor_state_05f0')}` | `{c.get('read_opcode_shadow_05d0')}` | `{c.get('read_packet_phase_06f1')}` | `{c.get('ok')}` |")
    lines += [
        '',
        '## Descriptor setup/gate with READ state 0x0E',
        '',
        '| case | opcode | LBA | 0x05B6 | copy hit | 0x06E4 match | 0x06E0=F0 | ok |',
        '|---|---:|---:|---:|---:|---:|---:|---:|',
    ]
    for c in state.get('cases', []) or []:
        lines.append(f"| `{c.get('case')}` | `{c.get('opcode_hex')}` | `{c.get('input_lba_hex')}` | `{c.get('packet_flags_05b6_hex')}` | `{c.get('descriptor_copy_entry_hit')}` | `{c.get('descriptor_06e4_lba_match')}` | `{c.get('transfer_flag_06e0_f0')}` | `{c.get('ok')}` |")
    lines += [
        '',
        '## Remaining proof gap',
        '',
        'Connect the natural post-parser path from `0x9001E541`/`0x9001E546`/`0x9001E556` into `0x9001B54E` dynamically.  The bridge/media-profile delta remains intentionally unpromoted until that executed path or a later firmware window selector consumes/derives the unlocked offset.',
        '',
    ]
    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 READ parser descriptor-state prerequisites and state-0x0E descriptor gate')
    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('--read-cdb-sweep-json')
    ap.add_argument('--sweep-case-dir')
    ap.add_argument('--out-prefix', default='gdr8050l_auth_v256_firmware_read_descriptor_state_probe_report')
    ap.add_argument('--json', default='gdr8050l_auth_v256_firmware_read_descriptor_state_probe_report.json')
    ap.add_argument('--md', default='gdr8050l_auth_v256_firmware_read_descriptor_state_probe_report.md')
    ap.add_argument('--case-dir')
    ap.add_argument('--timeout', type=int, default=45)
    args = ap.parse_args(argv)

    base_dir = Path.cwd()
    config_path = Path(args.config)
    if not config_path.is_absolute():
        config_path = base_dir / config_path
    config = load_json(config_path) if config_path.exists() else {}

    sweep_path = Path(args.read_cdb_sweep_json) if args.read_cdb_sweep_json else None
    if sweep_path and not sweep_path.is_absolute():
        sweep_path = base_dir / sweep_path
    case_dir = Path(args.case_dir) if args.case_dir else base_dir / f'{args.out_prefix}_cases'
    if not case_dir.is_absolute():
        case_dir = base_dir / case_dir

    parser = parser_prereq_summary(sweep_path, args.sweep_case_dir, base_dir) if sweep_path else {'ok': False, 'reason': 'read_cdb_sweep_json_not_supplied'}
    state = run_state_cases(config, config_path, args.firmware, args.mn103s_emu, args.out_prefix, case_dir, args.timeout)

    firmware_derived_host_lba = bool(parser.get('read_parser_lba_05ec_proven'))
    report = {
        'version': VERSION,
        'config': str(config_path),
        'read_cdb_sweep_json': str(sweep_path) if sweep_path else None,
        'read_parser_prereq': parser,
        'read_state_descriptor_gate': state,
        'read_parser_descriptor_state_0e_proven': bool(parser.get('read_parser_state_0e_proven')),
        'read_parser_phase_04_proven': bool(parser.get('read_parser_phase_04_proven')),
        'read_state_0e_descriptor_gate_copy_proven': bool(state.get('read_state_0e_descriptor_gate_copy_proven')),
        'firmware_derived_host_lba': firmware_derived_host_lba,
        'firmware_derived_delta': False,
        'ok': bool(parser.get('ok')) and bool(state.get('ok')) and firmware_derived_host_lba,
    }
    json_path = Path(args.json)
    md_path = Path(args.md)
    if not json_path.is_absolute():
        json_path = base_dir / json_path
    if not md_path.is_absolute():
        md_path = base_dir / md_path
    write_json(json_path, report)
    write_md(md_path, report)
    print(f"[FIRMWARE-READ-DESCRIPTOR-STATE] parser_cases={parser.get('passed_count')}/{parser.get('case_count')} state_cases={state.get('passed_count')}/{state.get('case_count')} read_state_0e=true firmware_derived_delta=false ok={str(report['ok']).lower()}")
    return 0 if report['ok'] else 1


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