#!/usr/bin/env python3
"""Build a firmware-handoff migration report from a live GDR-8050L iSCSI JSONL trace.

v213 keeps the known-good v206/v208 live behavior, then marks stateful SEEK/READ visible-bound checks as firmware-media-window behavior to migrate into MN103S firmware-in-the-loop later.
"""
from __future__ import annotations

import argparse
import collections
import json
from pathlib import Path
from typing import Any

OPNAMES = {
    0x00: 'TEST UNIT READY',
    0x03: 'REQUEST SENSE',
    0x12: 'INQUIRY',
    0x15: 'MODE SELECT(6)',
    0x1B: 'START STOP UNIT',
    0x1C: 'RECEIVE DIAGNOSTIC RESULTS/0x1C',
    0x1D: 'SEND DIAGNOSTIC',
    0x1E: 'PREVENT/ALLOW MEDIUM REMOVAL',
    0x25: 'READ CAPACITY(10)',
    0x28: 'READ(10)',
    0x2B: 'SEEK(10)',
    0x43: 'READ TOC/PMA/ATIP',
    0x46: 'GET CONFIGURATION',
    0x4A: 'GET EVENT STATUS NOTIFICATION',
    0x51: 'READ DISC INFORMATION',
    0x55: 'MODE SELECT(10)',
    0x5A: 'MODE SENSE(10)',
    0xA0: 'REPORT LUNS',
    0xA4: 'REPORT KEY',
    0xA8: 'READ(12)',
    0xAC: 'GET PERFORMANCE',
    0xAD: 'READ DVD STRUCTURE',
    0xBB: 'SET CD SPEED',
    0xFF: 'VENDOR/PROBE 0xFF',
}


def parse_hex(s: Any) -> bytes:
    if not s:
        return b''
    if isinstance(s, bytes):
        return s
    text = str(s).replace(',', ' ').replace(':', ' ')
    parts = [p for p in text.split() if p]
    try:
        return bytes(int(p, 16) & 0xFF for p in parts)
    except ValueError:
        # tolerate compact hex strings
        t = ''.join(ch for ch in str(s) if ch in '0123456789abcdefABCDEF')
        if len(t) % 2:
            t = t[:-1]
        return bytes.fromhex(t)


def load_jsonl(path: Path) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    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 json.JSONDecodeError:
                continue
            if isinstance(obj, dict):
                rows.append(obj)
    return rows


def opcode_name(cdb: bytes) -> str:
    if not cdb:
        return '(none)'
    return OPNAMES.get(cdb[0], f'0x{cdb[0]:02X}')


def cdb_detail(cdb: bytes, row: dict[str, Any]) -> str:
    if not cdb:
        return ''
    op = cdb[0]
    if op in (0x28, 0xA8):
        return f"host_lba={row.get('host_lba')}, blocks={row.get('blocks')}, media_lba={row.get('media_lba')}, delta={row.get('effective_media_lba_delta', row.get('dynamic_media_lba_delta'))}"
    if op == 0x5A and len(cdb) > 2:
        return f"page=0x{cdb[2] & 0x3F:02X}, pc={(cdb[2] >> 6) & 3}"
    if op == 0x55:
        return f"param_len={int.from_bytes(cdb[7:9], 'big') if len(cdb) >= 9 else None}, state_event={row.get('state_event') or ''}"
    if op == 0x15:
        return f"param_len={cdb[4] if len(cdb) >= 5 else None}, state_event={row.get('state_event') or ''}"
    if op == 0xAD and len(cdb) > 9:
        return f"format=0x{cdb[7]:02X}, alloc={int.from_bytes(cdb[8:10], 'big')}"
    if op == 0xA4 and len(cdb) > 10:
        return f"key_format=0x{cdb[10] & 0x3F:02X}, alloc={int.from_bytes(cdb[8:10], 'big') if len(cdb)>=10 else None}"
    if op == 0x1B and len(cdb) > 4:
        return f"loej={(cdb[4] >> 1) & 1}, start={cdb[4] & 1}, state_event={row.get('state_event') or ''}"
    if op == 0x2B and len(cdb) >= 6:
        lba = int.from_bytes(cdb[2:6], 'big')
        return f"seek_lba={row.get('seek_lba', lba)}, visible_max={row.get('visible_max_lba')}, auth={row.get('dynamic_media_auth_stage')}, expected_boundary_failure={row.get('expected_boundary_failure')}"
    return ''


def classify_handoff(cdb: bytes, source: str, row: dict[str, Any]) -> tuple[str, str, str]:
    """Return priority, category, note."""
    if not cdb:
        return ('ignore', 'transport/non-cdb', 'iSCSI/session row, not a SCSI command')
    op = cdb[0]
    event = str(row.get('state_event') or '')
    # Firmware-critical auth/media-window behavior.
    if op in (0x15, 0x55) and ('auth' in source or event):
        return ('P0', 'firmware-auth-state', 'replace bridge-side auth recognizer with firmware packet/FIFO/MMIO execution')
    if op == 0x5A and len(cdb) > 2 and (cdb[2] & 0x3F) == 0x3E:
        return ('P0', 'firmware-auth-state', 'MODE SENSE 3E should come directly from firmware auth state')
    if op == 0x25:
        return ('P0', 'firmware-media-window', 'READ CAPACITY is locked/unlocked state-dependent')
    if op in (0x28, 0xA8):
        return ('P0', 'firmware-media-window', 'READ LBA/window mapping should be selected by firmware state')
    if op == 0x2B:
        return ('P0', 'firmware-media-window', 'SEEK visible-LBA bounds should follow locked/unlocked firmware capacity state')
    if op == 0x1B:
        return ('P0', 'firmware-media-change', 'START STOP UNIT should relock/reset firmware media state')
    if op == 0xAD and len(cdb) > 7 and cdb[7] == 0xC0:
        return ('P0', 'firmware-auth-state', 'Xbox auth READ DVD STRUCTURE C0 should remain firmware/catalog backed')

    # Standard MMC/transport commands that are safe to synthesize until firmware path is available.
    if op in (0x12, 0xA0, 0x00, 0x1C, 0x1D, 0x1E, 0x43, 0x46, 0x4A, 0x51, 0x5A, 0xA4, 0xAC, 0xAD, 0xBB, 0xFF):
        return ('P2', 'profile/mmc-synthetic', 'safe current bridge response; later compare against firmware/hardware profile')

    return ('P1', 'unknown-or-low-frequency', 'inspect before deciding bridge vs firmware ownership')


def summarize(rows: list[dict[str, Any]]) -> dict[str, Any]:
    scsi = []
    for row in rows:
        cdb = parse_hex(row.get('cdb_hex') or row.get('cdb'))
        if cdb:
            scsi.append((row, cdb))

    groups: dict[tuple[str, str, str, str], dict[str, Any]] = {}
    for row, cdb in scsi:
        source = str(row.get('source') or '')
        priority, category, note = classify_handoff(cdb, source, row)
        opname = opcode_name(cdb)
        key = (priority, category, opname, source)
        g = groups.setdefault(key, {
            'priority': priority,
            'category': category,
            'opcode': opname,
            'source': source,
            'count': 0,
            'good': 0,
            'check_condition': 0,
            'sample_index': row.get('index'),
            'sample_cdb_hex': row.get('cdb_hex'),
            'sample_detail': cdb_detail(cdb, row),
            'note': note,
        })
        g['count'] += 1
        if row.get('scsi_status') == 'GOOD' or row.get('ok') is True:
            g['good'] += 1
        if row.get('scsi_status') == 'CHECK CONDITION':
            g['check_condition'] += 1

    priority_order = {'P0': 0, 'P1': 1, 'P2': 2, 'ignore': 9}
    handoff = sorted(groups.values(), key=lambda g: (priority_order.get(g['priority'], 8), g['category'], -g['count'], g['opcode'], g['source']))
    counts_by_priority = collections.Counter(g['priority'] for g in handoff for _ in range(g['count']))
    counts_by_category = collections.Counter(g['category'] for g in handoff for _ in range(g['count']))

    status_counts = collections.Counter(str(r.get('scsi_status') or ('GOOD' if r.get('ok') else '')) for r, _ in scsi)
    source_counts = collections.Counter(str(r.get('source') or '') for r, _ in scsi)
    op_counts = collections.Counter(opcode_name(cdb) for _, cdb in scsi)

    return {
        'total_rows': len(rows),
        'scsi_command_rows': len(scsi),
        'status_counts': dict(status_counts),
        'check_condition_count': status_counts.get('CHECK CONDITION', 0),
        'opcode_counts': dict(op_counts),
        'source_counts': dict(source_counts),
        'handoff_counts_by_priority': dict(counts_by_priority),
        'handoff_counts_by_category': dict(counts_by_category),
        'handoff_groups': handoff,
    }


def md_table(rows: list[list[Any]]) -> str:
    if not rows:
        return ''
    widths = [max(len(str(r[i])) for r in rows) for i in range(len(rows[0]))]
    out = []
    out.append('| ' + ' | '.join(str(rows[0][i]) for i in range(len(widths))) + ' |')
    out.append('| ' + ' | '.join('---' for _ in widths) + ' |')
    for r in rows[1:]:
        out.append('| ' + ' | '.join(str(r[i]).replace('|', '\\|') for i in range(len(widths))) + ' |')
    return '\n'.join(out)


def write_md(path: Path, report: dict[str, Any], trace_report: dict[str, Any] | None) -> None:
    lines: list[str] = []
    lines.append('# GDR-8050L stateful SEEK bounds + firmware handoff map')
    lines.append('')
    lines.append(f"rows: `{report['total_rows']}` total / `{report['scsi_command_rows']}` SCSI commands")
    lines.append(f"check_condition_count: `{report['check_condition_count']}`")
    if trace_report:
        lines.append(f"known_good_v206_complete: `{str(trace_report.get('known_good_v206_complete')).lower()}`")
        lines.append(f"known_good_v206_eject_cycle_complete: `{str(trace_report.get('known_good_v206_eject_cycle_complete')).lower()}`")
    lines.append('')
    lines.append('## Handoff priority summary')
    lines.append('')
    rows = [['priority', 'meaning', 'SCSI rows']]
    meanings = {
        'P0': 'migrate first: auth, capacity, READ/window, media-change state',
        'P1': 'inspect next: uncommon/unknown command ownership',
        'P2': 'safe bridge/profile response for now; compare to firmware/hardware later',
        'ignore': 'transport/non-CDB rows',
    }
    for p in ['P0', 'P1', 'P2', 'ignore']:
        if p in report['handoff_counts_by_priority']:
            rows.append([p, meanings.get(p, ''), report['handoff_counts_by_priority'][p]])
    lines.append(md_table(rows))
    lines.append('')
    lines.append('## Firmware handoff groups')
    lines.append('')
    rows = [['priority', 'category', 'opcode', 'source', 'count', 'sample index', 'sample detail', 'note']]
    for g in report['handoff_groups']:
        rows.append([g['priority'], g['category'], g['opcode'], g['source'], g['count'], g['sample_index'], g.get('sample_detail') or g['sample_cdb_hex'], g['note']])
    lines.append(md_table(rows))
    lines.append('')
    lines.append('## Command counts')
    lines.append('')
    rows = [['opcode', 'count']]
    for k, v in sorted(report['opcode_counts'].items(), key=lambda kv: (-kv[1], kv[0])):
        rows.append([k, v])
    lines.append(md_table(rows))
    lines.append('')
    path.write_text('\n'.join(lines) + '\n', encoding='utf-8')


def main() -> int:
    ap = argparse.ArgumentParser(description='Build firmware-handoff migration report from live GDR iSCSI JSONL trace')
    ap.add_argument('trace_jsonl', type=Path)
    ap.add_argument('--json', dest='json_out', type=Path)
    ap.add_argument('--md', dest='md_out', type=Path)
    ap.add_argument('--trace-report-json', type=Path)
    args = ap.parse_args()
    rows = load_jsonl(args.trace_jsonl)
    report = summarize(rows)
    report['trace'] = str(args.trace_jsonl)
    trace_report = None
    if args.trace_report_json and args.trace_report_json.exists():
        try:
            trace_report = json.loads(args.trace_report_json.read_text(encoding='utf-8'))
            report['known_good_v206_complete'] = bool(trace_report.get('known_good_v206_complete'))
            report['known_good_v206_eject_cycle_complete'] = bool(trace_report.get('known_good_v206_eject_cycle_complete'))
        except Exception as exc:
            report['trace_report_load_error'] = str(exc)
    if args.json_out:
        args.json_out.write_text(json.dumps(report, indent=2), encoding='utf-8')
    if args.md_out:
        write_md(args.md_out, report, trace_report)
    print(f"[FIRMWARE-HANDOFF] scsi={report['scsi_command_rows']} p0={report['handoff_counts_by_priority'].get('P0',0)} p1={report['handoff_counts_by_priority'].get('P1',0)} p2={report['handoff_counts_by_priority'].get('P2',0)} check_condition={report['check_condition_count']}")
    return 0

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