#!/usr/bin/env python3
"""Report v213 non-authoritative firmware-shadow READ CAPACITY state.

This is a regression/handoff tool. It does not make the live bridge firmware
-authoritative. It checks that READ CAPACITY(10) rows match the auth/media state
already observed by the v212/v213 shadow surface: locked video window before
unlock/relock, full raw DVD-9 window after partition unlock + sticky descramble.
"""
from __future__ import annotations
import argparse, json, struct
from pathlib import Path
from typing import Any

LOCKED_CAPACITY = bytes.fromhex('00 00 1B 4F 00 00 08 00')
DEFAULT_UNLOCKED_SECTORS = 3431264
BLOCK_SIZE = 2048


def load_jsonl(path: Path) -> list[dict[str, Any]]:
    rows=[]
    with path.open('r', encoding='utf-8', errors='replace') as f:
        for line in f:
            line=line.strip()
            if not line:
                continue
            try:
                rows.append(json.loads(line))
            except Exception:
                pass
    return rows


def op(row: dict[str, Any]) -> str:
    h=str(row.get('cdb_hex') or '').strip()
    return h.split()[0].upper() if h else ''


def row_bytes(row: dict[str, Any], response_dir: Path|None) -> bytes:
    h=str(row.get('data_hex') or '').strip()
    if h:
        try: return bytes.fromhex(h)
        except Exception: pass
    h=str(row.get('data_preview_hex') or '').strip()
    if h:
        try: return bytes.fromhex(h)
        except Exception: pass
    if response_dir and row.get('data_file'):
        p=response_dir / str(row['data_file'])
        if p.exists():
            return p.read_bytes()
    return b''


def be32(v:int)->bytes:
    return int(v).to_bytes(4,'big')


def _intish(value: Any, default: int = 0) -> int:
    try:
        if isinstance(value, str) and value.lower().startswith('0x'):
            return int(value, 16)
        return int(value)
    except Exception:
        return default


def expected_unlocked(row: dict[str, Any]) -> bytes:
    # Firmware-faithful GDR8050L mode must compare against the drive-visible
    # unlocked capacity, not the reconstructed Redump backing-image size.
    #
    # v227 Redump-style media profiles intentionally have:
    #   dynamic_media_sector_count == reconstructed ISO sectors (3820880)
    # while the drive-visible unlocked view remains:
    #   3431264 sectors / READ CAPACITY max LBA 0x00345B5F.
    #
    # Prefer any explicit host-visible/media-profile field if future traces add
    # it; otherwise stateful_xbox/redump rows use the known GDR8050L unlocked
    # host geometry. Direct/raw image mode can still fall back to backing size.
    for key in (
        'dynamic_media_unlocked_visible_sector_count',
        'drive_reported_unlocked_sector_count',
        'drive_reported_unlocked_sectors',
        'unlocked_visible_sector_count',
    ):
        sectors = _intish(row.get(key), 0)
        if sectors > 0:
            return be32(sectors - 1) + be32(BLOCK_SIZE)

    view_mode = str(row.get('dynamic_media_view_mode') or '').lower()
    is_redump = bool(row.get('media_is_redump_style')) or str(row.get('media_layout_type') or '') == 'original_xbox_xgd1_redump_style_2048'
    if view_mode == 'stateful_xbox' or is_redump:
        sectors = DEFAULT_UNLOCKED_SECTORS
    else:
        sectors = _intish(row.get('dynamic_media_sector_count'), DEFAULT_UNLOCKED_SECTORS)
        if sectors <= 0:
            sectors = DEFAULT_UNLOCKED_SECTORS
    return be32(sectors-1) + be32(BLOCK_SIZE)


def state(row: dict[str, Any]) -> str:
    return str(row.get('firmware_shadow_3e_stage') or row.get('dynamic_media_auth_stage') or '').lower()


def main() -> int:
    ap=argparse.ArgumentParser(description='GDR-8050L v227 firmware-shadow READ CAPACITY report')
    ap.add_argument('jsonl', type=Path)
    ap.add_argument('--response-dir', type=Path)
    ap.add_argument('--json', type=Path)
    ap.add_argument('--md', type=Path)
    args=ap.parse_args()
    rows=load_jsonl(args.jsonl)
    scsi=[r for r in rows if isinstance(r.get('index'), int) and int(r.get('index'))>=0 and r.get('cdb_hex')]
    caps=[]
    last_eject_idx=None
    for r in scsi:
        if r.get('state_event') == 'start_stop_eject' or r.get('firmware_shadow_3e_event') == 'start_stop_eject':
            last_eject_idx=int(r.get('index'))
        if op(r) == '25':
            data=row_bytes(r,args.response_dir)
            st=state(r)
            media_present = bool(r.get('dynamic_media_present', True))
            exp = expected_unlocked(r) if st == 'unlocked' and media_present else LOCKED_CAPACITY
            ok = data[:8] == exp
            max_lba = int.from_bytes(data[:4],'big') if len(data)>=8 else None
            block = int.from_bytes(data[4:8],'big') if len(data)>=8 else None
            caps.append({
                'index': r.get('index'),
                'state': st or 'unknown',
                'phase': r.get('firmware_shadow_3e_phase'),
                'sticky': bool(r.get('firmware_shadow_3e_sticky_descramble', r.get('dynamic_media_sticky_descramble', False))),
                'media_present': media_present,
                'after_eject': last_eject_idx is not None and int(r.get('index')) > last_eject_idx,
                'data_hex': data[:8].hex(' ').upper() if data else '',
                'expected_hex': exp.hex(' ').upper(),
                'max_lba': max_lba,
                'sectors': (max_lba+1) if max_lba is not None else None,
                'block_size': block,
                'match': ok,
                'source': r.get('source'),
            })
    mismatches=[c for c in caps if not c['match']]
    locked_seen=any(c['state']!='unlocked' and c['match'] and c['data_hex']==LOCKED_CAPACITY.hex(' ').upper() for c in caps)
    unlocked_hex=expected_unlocked({'dynamic_media_sector_count': DEFAULT_UNLOCKED_SECTORS}).hex(' ').upper()
    unlocked_seen=any(c['state']=='unlocked' and c['match'] and c['data_hex']==unlocked_hex for c in caps)
    post_eject_locked_seen=any(c['after_eject'] and c['state']!='unlocked' and c['match'] and c['data_hex']==LOCKED_CAPACITY.hex(' ').upper() for c in caps)
    sequence_complete=locked_seen and unlocked_seen and post_eject_locked_seen
    out={
        'jsonl': str(args.jsonl),
        'rows': len(rows),
        'scsi_rows': len(scsi),
        'read_capacity_rows': len(caps),
        'locked_capacity_seen': locked_seen,
        'unlocked_capacity_seen': unlocked_seen,
        'post_eject_locked_capacity_seen': post_eject_locked_seen,
        'capacity_sequence_complete': sequence_complete,
        'mismatch_count': len(mismatches),
        'ok': len(mismatches)==0 and sequence_complete,
        'observations': caps,
        'mismatches': mismatches,
        'note': 'v227 shadow is non-authoritative; later firmware-in-loop should generate the auth/capacity state while preserving this report contract.',
    }
    if args.json:
        args.json.write_text(json.dumps(out,indent=2),encoding='utf-8')
    lines=['# GDR-8050L v227 firmware-shadow READ CAPACITY report','',
           f'- rows: {len(rows)}', f'- scsi_rows: {len(scsi)}', f'- read_capacity_rows: {len(caps)}',
           f'- locked_capacity_seen: {str(locked_seen).lower()}', f'- unlocked_capacity_seen: {str(unlocked_seen).lower()}',
           f'- post_eject_locked_capacity_seen: {str(post_eject_locked_seen).lower()}',
           f'- capacity_sequence_complete: {str(sequence_complete).lower()}', f'- mismatch_count: {len(mismatches)}', f'- ok: {str(out["ok"]).lower()}', '',
           '## READ CAPACITY observations','',
           '| index | state | phase | after_eject | bytes | expected | sectors | match | source |',
           '|---:|---|---|---:|---|---|---:|---|---|']
    for c in caps:
        lines.append(f"| {c['index']} | `{c['state']}` | `{c.get('phase') or ''}` | `{str(c['after_eject']).lower()}` | `{c['data_hex']}` | `{c['expected_hex']}` | {c.get('sectors') or ''} | `{str(c['match']).lower()}` | `{c.get('source') or ''}` |")
    lines += ['', '## Mismatches', '']
    if not mismatches:
        lines.append('None.')
    else:
        lines += ['| index | state | bytes | expected | source |','|---:|---|---|---|---|']
        for c in mismatches:
            lines.append(f"| {c['index']} | `{c['state']}` | `{c['data_hex']}` | `{c['expected_hex']}` | `{c.get('source') or ''}` |")
    lines += ['', '## Note', '', out['note'], '']
    if args.md:
        args.md.write_text('\n'.join(lines),encoding='utf-8')
    print(f"[FIRMWARE-SHADOW-CAPACITY] scsi={len(scsi)} read_capacity={len(caps)} mismatches={len(mismatches)} sequence_complete={str(sequence_complete).lower()} ok={str(out['ok']).lower()}")
    return 0 if out['ok'] else 2

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