#!/usr/bin/env python3
"""Report v227 non-authoritative firmware-shadow LBA window map decisions.

This is a regression/handoff report only.  It does not claim the LBA delta is
firmware-derived yet.  It verifies that the current bridge-visible auth state
selects the same locked/direct and unlocked/+delta media mapping that future
firmware-in-loop code must reproduce.
"""
from __future__ import annotations
import argparse, collections, json
from pathlib import Path
from typing import Any

LOCKED_VISIBLE_MAX_LBA = 0x00001B4F
UNLOCKED_VISIBLE_MAX_LBA = 0x00345B5F
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:
                obj=json.loads(line)
            except Exception:
                continue
            if isinstance(obj, dict): rows.append(obj)
    return rows

def parse_hex(s: Any) -> bytes:
    if not s: return b''
    try: return bytes.fromhex(str(s).replace(' ','').replace(':','').replace('\n',''))
    except Exception: return b''

def cdb(row: dict[str, Any]) -> bytes:
    return parse_hex(row.get('cdb_hex'))

def op(row: dict[str, Any]) -> int | None:
    b=cdb(row)
    return b[0] if b else None

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 phase(row: dict[str, Any]) -> str:
    return str(row.get('firmware_shadow_3e_phase') or row.get('dynamic_media_auth_phase') or '')

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

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

def int_or_none(v: Any) -> int | None:
    if v is None or v == '': return None
    try: return int(v)
    except Exception: return None

def decode_read(row: dict[str, Any]) -> tuple[int|None,int|None,str]:
    b=cdb(row)
    if not b: return None,None,'unknown'
    if b[0] == 0x28 and len(b) >= 10:
        blocks=int.from_bytes(b[7:9],'big')
        if blocks == 0: blocks = 65536
        return int.from_bytes(b[2:6],'big'), blocks, '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)'
    return None,None,'unknown'

def expected_visible_max(row: dict[str, Any]) -> int:
    if state(row) == 'unlocked' and media_present(row):
        return UNLOCKED_VISIBLE_MAX_LBA
    return LOCKED_VISIBLE_MAX_LBA

def expected_delta(row: dict[str, Any], configured_delta: int) -> int:
    # v227: when the live bridge used a .media.json Redump-style layout, locked
    # video L1 tail reads are intentionally non-contiguous and carry their own
    # expected delta/segment mapping in the row.  Prefer that explicit row-level
    # map contract over the old locked=0/unlocked=configured simplification.
    v = int_or_none(row.get('firmware_shadow_lba_map_expected_delta'))
    if v is not None:
        return v
    v = int_or_none(row.get('effective_media_lba_delta'))
    if v is not None and row.get('media_is_redump_style'):
        return v
    if state(row) == 'unlocked' and media_present(row):
        return configured_delta
    return 0

def main() -> int:
    ap=argparse.ArgumentParser(description='GDR-8050L v227 firmware-shadow LBA window map report')
    ap.add_argument('jsonl', type=Path)
    ap.add_argument('--response-dir', type=Path, help='accepted for symmetry; not required')
    ap.add_argument('--configured-delta', type=int, default=None)
    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 op(r) is not None]
    configured_delta=args.configured_delta
    if configured_delta is None:
        # v227: Redump-style media profiles derive the current unlocked bridge
        # delta from layout (game_output_start_lba), not the old global 306080
        # config field.  Prefer unlocked row-level effective deltas when present.
        redump_unlocked=[]
        for r in scsi:
            if state(r) == 'unlocked' and r.get('media_is_redump_style'):
                v=int_or_none(r.get('effective_media_lba_delta'))
                if v not in (None,):
                    redump_unlocked.append(v)
        if redump_unlocked:
            configured_delta=collections.Counter(redump_unlocked).most_common(1)[0][0]
    if configured_delta is None:
        for r in scsi:
            v=int_or_none(r.get('dynamic_media_lba_delta'))
            if v not in (None,0):
                configured_delta=v; break
    if configured_delta is None:
        configured_delta=DEFAULT_DELTA

    read_rows=[]
    failures=[]
    observed_deltas=[]
    states=collections.Counter()
    for r in scsi:
        if op(r) not in (0x28,0xA8): continue
        host_lba, blocks, kind = decode_read(r)
        if host_lba is None or blocks is None: continue
        last_lba=host_lba + max(0,blocks-1)
        st=state(r); states[st]+=1
        actual=status(r)
        actual_ok=(actual=='GOOD')
        vis=expected_visible_max(r)
        exp_ok=media_present(r) and last_lba <= vis
        exp_delta=expected_delta(r, configured_delta)
        media_lba=int_or_none(r.get('media_lba'))
        eff_delta=int_or_none(r.get('effective_media_lba_delta'))
        actual_delta=(media_lba - host_lba) if media_lba is not None else None
        expected_media_lba=host_lba + exp_delta if exp_ok else None
        if actual_ok and actual_delta is not None:
            observed_deltas.append(actual_delta)
        status_match=(actual_ok==exp_ok)
        map_match=True
        if exp_ok and actual_ok:
            map_match=(media_lba==expected_media_lba and actual_delta==exp_delta and (eff_delta is None or eff_delta==exp_delta))
        rec={
            'index': r.get('index'), 'kind': kind, 'cdb_hex': r.get('cdb_hex'),
            'state': st, 'phase': phase(r), 'media_present': media_present(r),
            'host_lba': host_lba, 'blocks': blocks, 'last_lba': last_lba,
            'visible_max_lba': vis, 'expected_ok': exp_ok, 'actual_status': actual,
            'expected_delta': exp_delta, 'effective_delta': eff_delta,
            'media_lba': media_lba, 'actual_delta': actual_delta,
            'expected_media_lba': expected_media_lba,
            'status_match': status_match, 'map_match': map_match,
            'source': str(r.get('source') or ''),
            'read_map_note': str(r.get('read_map_note') or ''),
        }
        read_rows.append(rec)
        if not status_match or not map_match:
            failures.append(rec)
    delta_counts=collections.Counter(observed_deltas)
    unlocked_deltas=collections.Counter(r['actual_delta'] for r in read_rows if r['state']=='unlocked' and r['actual_status']=='GOOD' and r['actual_delta'] is not None)
    locked_deltas=collections.Counter(r['actual_delta'] for r in read_rows if r['state']!='unlocked' and r['actual_status']=='GOOD' and r['actual_delta'] is not None)
    candidate_delta = unlocked_deltas.most_common(1)[0][0] if unlocked_deltas else None
    locked_direct_ok = all((r['state']=='unlocked' or r['actual_status']!='GOOD' or r['actual_delta']==r['expected_delta']) for r in read_rows)
    unlocked_delta_ok = all((r['state']!='unlocked' or r['actual_status']!='GOOD' or r['actual_delta']==r['expected_delta']) for r in read_rows)
    post_eject_direct_ok = any(r['state']!='unlocked' and str(r['phase']).startswith('ejected') and r['actual_delta']==r['expected_delta'] for r in read_rows)
    has_unlocked_delta_read = any(r['state']=='unlocked' and r['actual_status']=='GOOD' and r['actual_delta']==r['expected_delta'] for r in read_rows)
    has_locked_direct_read = any(r['state']!='unlocked' and r['actual_status']=='GOOD' and r['actual_delta']==r['expected_delta'] for r in read_rows)
    ok = (len(failures)==0 and has_locked_direct_read and has_unlocked_delta_read and locked_direct_ok and unlocked_delta_ok)
    report={
        'jsonl': str(args.jsonl), 'rows': len(rows), 'scsi_rows': len(scsi), 'read_rows': len(read_rows),
        'configured_media_lba_delta': configured_delta,
        'candidate_delta_from_unlocked_bridge_rows': candidate_delta,
        'candidate_delta_equals_configured': candidate_delta == configured_delta,
        'firmware_derived': False,
        'source_note': 'This is a v227 non-authoritative bridge/shadow LBA map report. It proves the live trace is internally consistent and creates the handoff contract. It does not yet prove the delta from MN103S firmware state/MMIO/SRAM.',
        'locked_direct_read_seen': has_locked_direct_read,
        'unlocked_delta_read_seen': has_unlocked_delta_read,
        'post_eject_direct_read_seen': post_eject_direct_ok,
        'locked_direct_ok': locked_direct_ok,
        'unlocked_delta_ok': unlocked_delta_ok,
        'state_counts': dict(states),
        'observed_delta_counts': dict(sorted(delta_counts.items(), key=lambda kv: kv[0])),
        'unlocked_delta_counts': dict(sorted(unlocked_deltas.items(), key=lambda kv: kv[0])),
        'locked_delta_counts': dict(sorted(locked_deltas.items(), key=lambda kv: kv[0])),
        'mismatch_count': len(failures), 'ok': ok,
        'observations': read_rows, 'mismatches': failures,
        'next_handoff_requirement': 'Replace candidate_delta_from_unlocked_bridge_rows with firmware-derived delta evidence from the packet/FIFO/MMIO/SRAM execution path, then require equality with configured_media_lba_delta before making firmware authoritative.',
    }
    if args.json:
        args.json.write_text(json.dumps(report, indent=2), encoding='utf-8')
    lines=[
        '# GDR-8050L v227 firmware-shadow LBA window map report','',
        f'- rows: {len(rows)}', f'- scsi_rows: {len(scsi)}', f'- read_rows: {len(read_rows)}',
        f'- configured_media_lba_delta: {configured_delta}',
        f'- candidate_delta_from_unlocked_bridge_rows: {candidate_delta}',
        f'- candidate_delta_equals_configured: {str(candidate_delta == configured_delta).lower()}',
        '- firmware_derived: false',
        f'- locked_direct_read_seen: {str(has_locked_direct_read).lower()}',
        f'- unlocked_delta_read_seen: {str(has_unlocked_delta_read).lower()}',
        f'- post_eject_direct_read_seen: {str(post_eject_direct_ok).lower()}',
        f'- locked_direct_ok: {str(locked_direct_ok).lower()}',
        f'- unlocked_delta_ok: {str(unlocked_delta_ok).lower()}',
        f'- mismatch_count: {len(failures)}', f'- ok: {str(ok).lower()}', '',
        '## Delta counts','',
        f'- locked_delta_counts: `{dict(sorted(locked_deltas.items(), key=lambda kv: kv[0]))}`',
        f'- unlocked_delta_counts: `{dict(sorted(unlocked_deltas.items(), key=lambda kv: kv[0]))}`',
        f'- all_observed_delta_counts: `{dict(sorted(delta_counts.items(), key=lambda kv: kv[0]))}`', '',
        '## High-value READ map observations','',
        '| index | state | phase | host_lba | blocks | expected_delta | actual_delta | media_lba | status | source |',
        '|---:|---|---|---:|---:|---:|---:|---:|---|---|'
    ]
    sample=[]
    # include first few locked/direct, first few unlocked, post-eject, and high LBAs
    for pred in [
        lambda r: r['state']!='unlocked' and r['actual_delta']==0,
        lambda r: r['state']=='unlocked' and r['actual_delta']==configured_delta,
        lambda r: r['host_lba'] > LOCKED_VISIBLE_MAX_LBA,
        lambda r: str(r['phase']).startswith('ejected'),
    ]:
        for r in read_rows:
            if pred(r) and r not in sample:
                sample.append(r)
            if len(sample) >= 40: break
    for r in sample[:80]:
        lines.append(f"| {r['index']} | `{r['state']}` | `{r['phase']}` | {r['host_lba']} | {r['blocks']} | {r['expected_delta']} | {'' if r['actual_delta'] is None else r['actual_delta']} | {'' if r['media_lba'] is None else r['media_lba']} | `{r['actual_status']}` | `{r['source']}` |")
    lines += ['', '## Mismatches', '']
    if not failures:
        lines.append('None.')
    else:
        lines += ['| index | state | host_lba | expected_delta | actual_delta | expected_ok | actual | map_match | source |','|---:|---|---:|---:|---:|---|---|---|---|']
        for r in failures[:80]:
            lines.append(f"| {r['index']} | `{r['state']}` | {r['host_lba']} | {r['expected_delta']} | {'' if r['actual_delta'] is None else r['actual_delta']} | `{str(r['expected_ok']).lower()}` | `{r['actual_status']}` | `{str(r['map_match']).lower()}` | `{r['source']}` |")
    lines += ['', '## Handoff note', '', report['source_note'], '', report['next_handoff_requirement'], '']
    if args.md:
        args.md.write_text('\n'.join(lines), encoding='utf-8')
    print(f"[FIRMWARE-SHADOW-LBA-MAP] reads={len(read_rows)} configured_delta={configured_delta} candidate_delta={candidate_delta} mismatches={len(failures)} firmware_derived=false ok={str(ok).lower()}")
    return 0 if ok else 2

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