#!/usr/bin/env python3
"""Summarize compact GDR/XDVD READ trace JSONL files.

v165 adds layer-break and raw-vs-XISO mapping diagnostics.  A physical/raw
option-1 dump should use physical_raw/raw_full/direct mapping and should read
through the sidecar/ISO layer_break_lba.  The XISO path uses xiso_alias/dual_alias
and may cross the physical ISO layer in the backing image without the dumper
reporting a logical XISO layer transition.

Usage:
    python gdr_read_trace_summary.py gdr8050l_auth_v165_windows_iscsi_read_trace.jsonl
"""
from __future__ import annotations
import argparse, json
from collections import Counter
from pathlib import Path
from typing import Any


def load_rows(path: Path) -> list[dict[str, Any]]:
    rows=[]
    with path.open('r', encoding='utf-8', errors='replace') as fh:
        for line in fh:
            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 as_int(v: Any, default: int = 0) -> int:
    try:
        return int(v)
    except Exception:
        return default


def main() -> int:
    ap=argparse.ArgumentParser(description='Summarize GDR/XDVD compact READ trace JSONL')
    ap.add_argument('trace_jsonl')
    ap.add_argument('--top', type=int, default=20)
    args=ap.parse_args()
    path=Path(args.trace_jsonl)
    rows=load_rows(path)
    print(f'File: {path}')
    print(f'Rows: {len(rows)}')
    if not rows:
        return 0
    sig=Counter(str(r.get('signature','')) for r in rows)
    notes=Counter(str(r.get('read_map_note','')) for r in rows)
    modes=Counter(str(r.get('read_map_mode','')) for r in rows)
    stages=Counter(str(r.get('auth_stage','')) for r in rows)
    host_layers=Counter(str(r.get('host_layer','')) for r in rows if 'host_layer' in r)
    iso_layers=Counter(str(r.get('iso_layer','')) for r in rows if 'iso_layer' in r)
    layer_break=next((as_int(r.get('layer_break_lba')) for r in rows if as_int(r.get('layer_break_lba'))), 0)
    physical=next((as_int(r.get('xbox_physical_sectors')) for r in rows if as_int(r.get('xbox_physical_sectors'))), 0)
    iso_count=next((as_int(r.get('iso_sector_count')) for r in rows if as_int(r.get('iso_sector_count'))), 0)
    print('\nAuth stages:')
    for k,v in stages.most_common(): print(f'  {k}: {v}')
    print('\nRead-map modes:')
    for k,v in modes.most_common(): print(f'  {k}: {v}')
    print('\nRead-map notes:')
    for k,v in notes.most_common(): print(f'  {k}: {v}')
    if layer_break or physical or iso_count:
        print('\nPhysical layout:')
        if iso_count: print(f'  iso_sector_count: {iso_count}')
        if physical: print(f'  xbox_physical_sectors: {physical}')
        if layer_break: print(f'  layer_break_lba: {layer_break}')
        if physical:
            print(f'  full_raw_bytes: {physical * 2048}')
    if host_layers or iso_layers:
        print('\nLayer observations:')
        if host_layers:
            print('  host layers:')
            for k,v in host_layers.most_common(): print(f'    {k}: {v}')
        if iso_layers:
            print('  backing ISO layers:')
            for k,v in iso_layers.most_common(): print(f'    {k}: {v}')
        host_cross=[r for r in rows if r.get('host_crosses_layer_break')]
        iso_cross=[r for r in rows if r.get('iso_crosses_layer_break')]
        print(f'  host reads crossing layer_break: {len(host_cross)}')
        print(f'  backing ISO reads crossing layer_break: {len(iso_cross)}')
        if iso_cross:
            print('  first backing-ISO layer-crossing reads:')
            for r in iso_cross[:min(args.top, len(iso_cross))]:
                print(f"    host_lba={r.get('host_lba')}..{r.get('host_end_lba')} iso_lba={r.get('iso_lba')}..{r.get('iso_end_lba')} blocks={r.get('blocks')} map={r.get('read_map_note')}")
    print('\nSignatures:')
    for k,v in sig.most_common(args.top): print(f'  {k}: {v}')
    print('\nFirst reads:')
    for r in rows[:min(args.top, len(rows))]:
        extra=''
        if 'iso_layer' in r: extra=f" host_layer={r.get('host_layer')} iso_layer={r.get('iso_layer')}"
        print(f"  host_lba={r.get('host_lba')} iso_lba={r.get('iso_lba')} blocks={r.get('blocks')} sig={r.get('signature')} map={r.get('read_map_note')}{extra}")
    interesting=[]
    for r in rows:
        s=str(r.get('signature','')).lower()
        if any(x in s for x in ('xdfs','xbe','udf','iso9660')):
            interesting.append(r)
    if interesting:
        print('\nInteresting signature reads:')
        for r in interesting[:args.top]:
            print(f"  host_lba={r.get('host_lba')} iso_lba={r.get('iso_lba')} blocks={r.get('blocks')} sig={r.get('signature')} map={r.get('read_map_note')}")
    return 0

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