#!/usr/bin/env python3
"""Report v227 non-authoritative firmware-shadow READ/SEEK bounds and LBA-window decisions."""
from __future__ import annotations
import argparse, json
from pathlib import Path
from typing import Any

LOCKED_VISIBLE_MAX_LBA = 6991
DEFAULT_UNLOCKED_SECTORS = 3431264
DEFAULT_DELTA = 306080

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 cdb_bytes(row: dict[str, Any]) -> bytes:
    h=str(row.get('cdb_hex') or '').strip()
    try: return bytes.fromhex(h)
    except Exception: return b''

def op(row: dict[str, Any]) -> str:
    b=cdb_bytes(row)
    return f'{b[0]:02X}' if b else ''

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

def media_present(row: dict[str, Any]) -> bool:
    return bool(row.get('dynamic_media_present', True))

def unlocked_sectors(row: dict[str, Any]) -> int:
    try: v=int(row.get('dynamic_media_sector_count') or DEFAULT_UNLOCKED_SECTORS)
    except Exception: v=DEFAULT_UNLOCKED_SECTORS
    return v if v > 0 else DEFAULT_UNLOCKED_SECTORS

def expected_visible_max(row: dict[str, Any]) -> int:
    try:
        v = row.get('firmware_shadow_lba_map_visible_max_lba')
        if v is not None and v != '':
            return int(v)
    except Exception:
        pass
    if state(row) == 'unlocked' and media_present(row):
        return unlocked_sectors(row) - 1
    return LOCKED_VISIBLE_MAX_LBA

def expected_delta(row: dict[str, Any]) -> int:
    try:
        v = row.get('firmware_shadow_lba_map_expected_delta')
        if v is not None and v != '':
            return int(v)
    except Exception:
        pass
    try:
        if row.get('media_is_redump_style') and row.get('effective_media_lba_delta') not in (None, ''):
            return int(row.get('effective_media_lba_delta'))
    except Exception:
        pass
    if state(row) == 'unlocked' and media_present(row):
        try: return int(row.get('dynamic_media_lba_delta') or DEFAULT_DELTA)
        except Exception: return DEFAULT_DELTA
    return 0

def decode_lba_blocks(row: dict[str, Any]) -> tuple[int|None, int|None, str]:
    b=cdb_bytes(row)
    if not b: return None,None,'unknown'
    if b[0] == 0x28 and len(b) >= 10:
        return int.from_bytes(b[2:6],'big'), int.from_bytes(b[7:9],'big') or 65536, 'READ(10)'
    if b[0] == 0xA8 and len(b) >= 12:
        return int.from_bytes(b[2:6],'big'), int.from_bytes(b[6:10],'big'), 'READ(12)'
    if b[0] == 0x2B and len(b) >= 10:
        return int.from_bytes(b[2:6],'big'), 1, 'SEEK(10)'
    return None,None,'unknown'

def status(row: dict[str, Any]) -> str:
    return str(row.get('scsi_status') or row.get('status') or '').upper() or 'GOOD'

def main() -> int:
    ap=argparse.ArgumentParser(description='GDR-8050L v227 firmware-shadow READ/SEEK bounds report')
    ap.add_argument('jsonl', type=Path)
    ap.add_argument('--response-dir', type=Path, help='accepted for symmetry; not required by this report')
    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')]
    obs=[]
    for r in scsi:
        if op(r) not in ('28','A8','2B'): continue
        lba,blocks,kind=decode_lba_blocks(r)
        if lba is None or blocks is None: continue
        last_lba = lba + max(blocks,1) - 1
        vis=expected_visible_max(r)
        exp_ok = last_lba <= vis and media_present(r)
        actual=status(r)
        actual_ok = (actual == 'GOOD')
        exp_delta = expected_delta(r)
        try: media_lba_i = int(r.get('media_lba')) if r.get('media_lba') is not None else None
        except Exception: media_lba_i = None
        try: eff_delta_i = int(r.get('effective_media_lba_delta')) if r.get('effective_media_lba_delta') is not None else None
        except Exception: eff_delta_i = None
        expected_media_lba = lba + exp_delta if kind.startswith('READ') and exp_ok else None
        map_match = True
        if kind.startswith('READ') and exp_ok:
            map_match = (media_lba_i == expected_media_lba and (eff_delta_i is None or eff_delta_i == exp_delta))
        status_match = (actual_ok == exp_ok)
        obs.append({
            'index': r.get('index'), 'kind': kind, 'cdb_hex': r.get('cdb_hex'),
            'state': state(r), '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(r), 'host_lba': lba, 'blocks': blocks, 'last_lba': last_lba,
            'visible_max_lba': vis, 'expected_ok': exp_ok, 'actual_status': actual,
            'status_match': status_match, 'expected_delta': exp_delta, 'effective_delta': eff_delta_i,
            'media_lba': media_lba_i, 'expected_media_lba': expected_media_lba,
            'map_match': map_match, 'expected_boundary_failure': (not exp_ok and (bool(r.get('expected_boundary_failure')) or state(r) != 'unlocked')),
            'source': str(r.get('source') or ''),
        })
    mismatches=[o for o in obs if not o['status_match'] or not o['map_match']]
    read_obs=[o for o in obs if o['kind'].startswith('READ')]
    seek_obs=[o for o in obs if o['kind']=='SEEK(10)']
    locked_direct=any(o['kind'].startswith('READ') and o['state']!='unlocked' and o['expected_ok'] and o['map_match'] and o['expected_delta']==0 for o in obs)
    unlocked_delta=any(o['kind'].startswith('READ') and o['state']=='unlocked' and o['expected_ok'] and o['map_match'] and o['expected_delta']!=0 for o in obs)
    locked_boundary_fail=any(o['state']!='unlocked' and not o['expected_ok'] and o['actual_status']=='CHECK CONDITION' for o in obs)
    unlocked_boundary_pass=any(o['state']=='unlocked' and o['host_lba'] > LOCKED_VISIBLE_MAX_LBA and o['expected_ok'] and o['actual_status']=='GOOD' for o in obs)
    post_eject_relocked_fail=any(o['state']!='unlocked' and str(o.get('phase') or '').startswith('ejected') and not o['expected_ok'] and o['actual_status']=='CHECK CONDITION' for o in obs)
    ok = len(mismatches)==0 and locked_direct and unlocked_delta and locked_boundary_fail and unlocked_boundary_pass
    report={
        'jsonl': str(args.jsonl), 'rows': len(rows), 'scsi_rows': len(scsi),
        'observed_bounds_rows': len(obs), 'read_rows': len(read_obs), 'seek_rows': len(seek_obs),
        'locked_direct_read_seen': locked_direct, 'unlocked_delta_read_seen': unlocked_delta,
        'locked_boundary_failure_seen': locked_boundary_fail, 'unlocked_boundary_pass_seen': unlocked_boundary_pass,
        'post_eject_relocked_boundary_failure_seen': post_eject_relocked_fail,
        'mismatch_count': len(mismatches), 'ok': ok, 'observations': obs, 'mismatches': mismatches,
        'note': 'v227 shadow is non-authoritative; later firmware-in-loop should generate the READ/SEEK capacity and LBA-window decisions while preserving this report contract.',
    }
    if args.json: args.json.write_text(json.dumps(report,indent=2),encoding='utf-8')
    lines=['# GDR-8050L v227 firmware-shadow READ/SEEK bounds report','',
           f'- rows: {len(rows)}', f'- scsi_rows: {len(scsi)}', f'- observed_bounds_rows: {len(obs)}',
           f'- read_rows: {len(read_obs)}', f'- seek_rows: {len(seek_obs)}',
           f'- locked_direct_read_seen: {str(locked_direct).lower()}',
           f'- unlocked_delta_read_seen: {str(unlocked_delta).lower()}',
           f'- locked_boundary_failure_seen: {str(locked_boundary_fail).lower()}',
           f'- unlocked_boundary_pass_seen: {str(unlocked_boundary_pass).lower()}',
           f'- post_eject_relocked_boundary_failure_seen: {str(post_eject_relocked_fail).lower()}',
           f'- mismatch_count: {len(mismatches)}', f'- ok: {str(ok).lower()}', '',
           '## Boundary / high-LBA observations','',
           '| index | kind | state | phase | host_lba | blocks | visible_max | expected_ok | actual | expected_delta | media_lba | source |',
           '|---:|---|---|---|---:|---:|---:|---|---|---:|---:|---|']
    interesting=[o for o in obs if o['host_lba'] > LOCKED_VISIBLE_MAX_LBA or not o['expected_ok'] or o['kind']=='SEEK(10)'][:80]
    for o in interesting:
        lines.append(f"| {o['index']} | `{o['kind']}` | `{o['state']}` | `{o.get('phase') or ''}` | {o['host_lba']} | {o['blocks']} | {o['visible_max_lba']} | `{str(o['expected_ok']).lower()}` | `{o['actual_status']}` | {o['expected_delta']} | {'' if o['media_lba'] is None else o['media_lba']} | `{o['source']}` |")
    lines += ['', '## READ map sample', '',
              '| index | state | host_lba | blocks | expected_delta | effective_delta | media_lba | expected_media_lba | map_match |',
              '|---:|---|---:|---:|---:|---:|---:|---:|---|']
    for o in [x for x in obs if x['kind'].startswith('READ')][:40]:
        lines.append(f"| {o['index']} | `{o['state']}` | {o['host_lba']} | {o['blocks']} | {o['expected_delta']} | {'' if o['effective_delta'] is None else o['effective_delta']} | {'' if o['media_lba'] is None else o['media_lba']} | {'' if o['expected_media_lba'] is None else o['expected_media_lba']} | `{str(o['map_match']).lower()}` |")
    lines += ['', '## Mismatches', '']
    if not mismatches:
        lines.append('None.')
    else:
        lines += ['| index | kind | state | host_lba | expected_ok | actual | map_match | source |','|---:|---|---|---:|---|---|---|---|']
        for o in mismatches:
            lines.append(f"| {o['index']} | `{o['kind']}` | `{o['state']}` | {o['host_lba']} | `{str(o['expected_ok']).lower()}` | `{o['actual_status']}` | `{str(o['map_match']).lower()}` | `{o['source']}` |")
    lines += ['', '## Note', '', report['note'], '']
    if args.md: args.md.write_text('\n'.join(lines),encoding='utf-8')
    print(f"[FIRMWARE-SHADOW-BOUNDS] scsi={len(scsi)} observed={len(obs)} read={len(read_obs)} seek={len(seek_obs)} mismatches={len(mismatches)} ok={str(ok).lower()}")
    return 0 if ok else 2
if __name__ == '__main__':
    raise SystemExit(main())
