#!/usr/bin/env python3
"""GDR-8050L firmware-window delta audit (v234).

This is a proof hygiene tool.  It keeps the live bridge behavior unchanged, but
separates three things that were easy to conflate in v227/v228 traces:

* the legacy configured contiguous-layout fallback delta (commonly 306080),
* the effective Redump media-profile READ delta actually used for unlocked rows
  (198144 for the current Red Faction II profile), and
* raw firmware byte matches for that delta.

A raw ROM match is useful evidence, but not firmware-derived behavior unless the
match is consumed by the executed READ/window path.  This report therefore
classifies the ROM context and looks for direct address xrefs before deciding.
"""
from __future__ import annotations

import argparse, collections, json, re, sys
from pathlib import Path
from typing import Any

from hlds_gdr_firmware_lba_cpu_smoke import resolve_firmware

ROM_BASE_DEFAULT = 0x90000000


def load_json(path: str | Path | None, default: Any = None) -> Any:
    if not path:
        return default
    p = Path(path)
    if not p.exists():
        return default
    try:
        return json.loads(p.read_text(encoding='utf-8'))
    except Exception:
        return default


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


def intish(v: Any, default: int = 0) -> int:
    try:
        if v is None or v == '':
            return default
        if isinstance(v, str):
            return int(v, 0)
        return int(v)
    except Exception:
        return default


def cdb_opcode(row: dict[str, Any]) -> int | None:
    h = str(row.get('cdb_hex') or '').strip()
    if not h:
        return None
    try:
        return int(h.split()[0], 16)
    except Exception:
        return None


def calc_effective_delta(row: dict[str, Any]) -> int | None:
    if row.get('effective_media_lba_delta') is not None:
        return intish(row.get('effective_media_lba_delta'))
    if row.get('dynamic_media_effective_lba_delta') is not None:
        return intish(row.get('dynamic_media_effective_lba_delta'))
    if row.get('media_lba') is not None and row.get('host_lba') is not None:
        return intish(row.get('media_lba')) - intish(row.get('host_lba'))
    return None


def scan_all(data: bytes, pat: bytes) -> list[int]:
    out: list[int] = []
    i = 0
    while True:
        j = data.find(pat, i)
        if j < 0:
            return out
        out.append(j)
        i = j + 1


def u32le_at(data: bytes, off: int) -> int | None:
    if off < 0 or off + 4 > len(data):
        return None
    return int.from_bytes(data[off:off+4], 'little')


def table_context(data: bytes, off: int, base: int, radius: int = 6) -> dict[str, Any]:
    align = off & ~3
    entries = []
    for idx in range(-radius, radius + 1):
        pos = align + idx * 4
        val = u32le_at(data, pos)
        if val is None:
            continue
        entries.append({'index': idx, 'offset': pos, 'rom_address': f'0x{base+pos:08X}', 'u32le': val, 'u32le_hex': f'0x{val:08X}'})
    vals = [e['u32le'] for e in entries]
    diffs = [b - a for a, b in zip(vals, vals[1:])]
    diff_counts = collections.Counter(diffs)
    dominant_diff, dominant_count = (None, 0)
    if diff_counts:
        dominant_diff, dominant_count = diff_counts.most_common(1)[0]
    # A local arithmetic table is suspicious as a proof because any one value in
    # a stride table can equal a desired delta without being a semantic constant.
    progression = bool(dominant_diff is not None and dominant_count >= max(3, len(diffs) - 2) and dominant_diff != 0)
    classification = 'arithmetic_progression_table' if progression else 'isolated_or_mixed_context'
    sequence_start = align
    sequence_end = align
    if progression and dominant_diff is not None:
        by_off = {int(e['offset']): int(e['u32le']) for e in entries}
        while (sequence_start - 4) in by_off and by_off[sequence_start] - by_off[sequence_start - 4] == dominant_diff:
            sequence_start -= 4
        while (sequence_end + 4) in by_off and by_off[sequence_end + 4] - by_off[sequence_end] == dominant_diff:
            sequence_end += 4
    return {
        'match_aligned_offset': align,
        'match_aligned_rom_address': f'0x{base+align:08X}',
        'sequence_start_offset': sequence_start,
        'sequence_start_rom_address': f'0x{base+sequence_start:08X}',
        'sequence_end_offset': sequence_end,
        'sequence_end_rom_address': f'0x{base+sequence_end:08X}',
        'entries': entries,
        'dominant_step': dominant_diff,
        'dominant_step_hex': f'0x{dominant_diff & 0xFFFFFFFF:08X}' if dominant_diff is not None else '',
        'dominant_step_count': dominant_count,
        'classification': classification,
        'constant_is_unique_standalone': not progression,
    }


def resolve_expected_delta(args: argparse.Namespace, rows: list[dict[str, Any]], lba_map: dict[str, Any], media_layout: dict[str, Any], const_report: dict[str, Any]) -> tuple[int, str]:
    if args.expected_delta is not None:
        return intish(args.expected_delta), 'cli'
    for doc, key, source in (
        (lba_map, 'configured_media_lba_delta', 'lba_map_json.configured_media_lba_delta'),
        (lba_map, 'candidate_delta_from_unlocked_bridge_rows', 'lba_map_json.candidate_delta_from_unlocked_bridge_rows'),
        (media_layout, 'redump_unlocked_candidate_delta', 'media_layout_json.redump_unlocked_candidate_delta'),
        (const_report, 'expected_delta', 'constant_report.expected_delta'),
    ):
        if isinstance(doc, dict) and doc.get(key) is not None:
            v = intish(doc.get(key), 0)
            if v:
                return v, source
    unlocked = [r for r in rows if cdb_opcode(r) in (0x28, 0xA8) and str(r.get('dynamic_media_auth_stage')) == 'unlocked']
    ctr = collections.Counter(d for r in unlocked for d in [calc_effective_delta(r)] if d is not None)
    if ctr:
        return int(ctr.most_common(1)[0][0]), 'jsonl.unlocked_read_effective_delta_mode'
    return 0, 'not_found'


def build_report(args: argparse.Namespace) -> dict[str, Any]:
    jsonl = Path(args.jsonl)
    rows = iter_jsonl(jsonl)
    lba_map = load_json(args.lba_map_json, {})
    media_layout = load_json(args.media_layout_json, {})
    const_report = load_json(args.window_constant_json, {})
    cfg = load_json(args.config, {})

    read_rows = [r for r in rows if cdb_opcode(r) in (0x28, 0xA8)]
    unlocked = [r for r in read_rows if str(r.get('dynamic_media_auth_stage')) == 'unlocked']
    locked = [r for r in read_rows if str(r.get('dynamic_media_auth_stage')) != 'unlocked']
    eff_counts = collections.Counter(calc_effective_delta(r) for r in read_rows if calc_effective_delta(r) is not None)
    unlocked_eff_counts = collections.Counter(calc_effective_delta(r) for r in unlocked if calc_effective_delta(r) is not None)
    locked_eff_counts = collections.Counter(calc_effective_delta(r) for r in locked if calc_effective_delta(r) is not None)
    legacy_counts = collections.Counter(intish(r.get('dynamic_media_legacy_configured_lba_delta', r.get('dynamic_media_configured_lba_delta', r.get('dynamic_media_lba_delta'))), 0) for r in rows if r.get('cdb_hex'))
    shadow_counts = collections.Counter(intish(r.get('firmware_shadow_lba_map_configured_delta'), 0) for r in rows if r.get('firmware_shadow_lba_map_configured_delta') is not None)

    expected_delta, expected_source = resolve_expected_delta(args, rows, lba_map, media_layout, const_report)

    resolved_fw = resolve_firmware(cfg, Path(args.config).resolve() if args.config else jsonl.resolve(), args.firmware)
    fw = Path(str(resolved_fw)) if resolved_fw else Path(str(args.firmware or ''))
    fw_data = fw.read_bytes() if fw.exists() else b''
    rom_base = intish(args.rom_base, ROM_BASE_DEFAULT)
    matches: list[dict[str, Any]] = []
    xref_summary: dict[str, Any] = {}
    if fw_data and expected_delta:
        patterns = {
            'u32le': int(expected_delta).to_bytes(4, 'little', signed=False),
            'u24le': int(expected_delta).to_bytes(4, 'little', signed=False)[:3],
        }
        seen = set()
        for enc, pat in patterns.items():
            for off in scan_all(fw_data, pat):
                key = (off, enc)
                if key in seen:
                    continue
                seen.add(key)
                ctx = bytes(fw_data[max(0, off-16): min(len(fw_data), off+24)]).hex(' ').upper()
                m = {'encoding': enc, 'offset': off, 'rom_address': f'0x{rom_base+off:08X}', 'pattern_hex': pat.hex(' ').upper(), 'context_hex': ctx}
                if enc == 'u32le':
                    m['table_context'] = table_context(fw_data, off, rom_base)
                matches.append(m)
        full_address_hits = {}
        for off in sorted({m['offset'] for m in matches}):
            addr = rom_base + off
            full_address_hits[f'0x{addr:08X}'] = [f'0x{rom_base+x:08X}' for x in scan_all(fw_data, addr.to_bytes(4, 'little'))]
        table_bases = set()
        for m in matches:
            tc = m.get('table_context') or {}
            table_bases.add(int(tc.get('sequence_start_offset', m['offset'] & ~3)))
        table_base_hits = {}
        for off in sorted(table_bases):
            addr = rom_base + off
            table_base_hits[f'0x{addr:08X}'] = [f'0x{rom_base+x:08X}' for x in scan_all(fw_data, addr.to_bytes(4, 'little'))]
        xref_summary = {
            'full_matched_address_reference_count': sum(len(v) for v in full_address_hits.values()),
            'full_matched_address_references': full_address_hits,
            'full_table_base_reference_count': sum(len(v) for v in table_base_hits.values()),
            'full_table_base_references': table_base_hits,
            'low16_reference_warning': 'low16 matches are intentionally not treated as proof because MN103S short absolute I/O/SRAM operands alias many unrelated addresses',
        }

    u32_matches = [m for m in matches if m.get('encoding') == 'u32le']
    table_classifications = collections.Counter(((m.get('table_context') or {}).get('classification') or 'none') for m in u32_matches)
    constant_present = bool(matches)
    standalone = any((m.get('table_context') or {}).get('constant_is_unique_standalone') for m in u32_matches)
    full_xrefs = int((xref_summary or {}).get('full_matched_address_reference_count') or 0) + int((xref_summary or {}).get('full_table_base_reference_count') or 0)

    live_effective_ok = (not unlocked) or (len(unlocked_eff_counts) == 1 and expected_delta in unlocked_eff_counts)

    # Earlier audit versions assumed every locked READ should be a direct host_lba
    # -> media_lba mapping with delta 0.  The v233 live trace shows one valid
    # locked visible-window tail read (host_lba == visible max_lba 6991) backed by
    # a nonzero Redump physical media location.  That is not an unlocked game
    # window delta and it is already validated by the shadow LBA-map report as a
    # map_match.  Treat the LBA-map report as the authority for locked visible
    # window consistency; keep the raw nonzero count in this audit so it remains
    # visible and cannot be mistaken for firmware-derived game-window proof.
    locked_zero_delta_only_ok = (not locked) or (set(locked_eff_counts.keys()) <= {0})
    locked_lba_map_report_ok = bool(lba_map.get('locked_direct_ok')) if isinstance(lba_map, dict) and lba_map else locked_zero_delta_only_ok
    locked_nonzero_delta_rows = []
    for r in locked:
        d = calc_effective_delta(r)
        if d not in (None, 0):
            locked_nonzero_delta_rows.append({
                'index': r.get('index'),
                'cdb_hex': r.get('cdb_hex'),
                'host_lba': intish(r.get('host_lba')),
                'media_lba': intish(r.get('media_lba')),
                'effective_delta': d,
                'source': r.get('source'),
                'auth_stage': r.get('dynamic_media_auth_stage'),
                'data_len': r.get('data_len'),
            })
    locked_direct_ok = bool(locked_lba_map_report_ok)
    legacy_delta = legacy_counts.most_common(1)[0][0] if legacy_counts else None
    legacy_mismatch_is_expected = legacy_delta not in (None, 0, expected_delta) and bool(media_layout.get('redump_unlocked_candidate_delta') == expected_delta or lba_map.get('configured_media_lba_delta') == expected_delta)

    firmware_derived_delta = False
    if constant_present and standalone and full_xrefs > 0:
        # Still conservative: a real proof requires an executed path, so even this
        # would be only candidate evidence.  Leave false until execution trace is available.
        firmware_derived_delta = False

    ok = bool(live_effective_ok and locked_direct_ok and expected_delta and constant_present)
    return {
        'version': 'v234-firmware-window-delta-audit',
        'jsonl': str(jsonl),
        'config': str(args.config) if args.config else '',
        'firmware': str(fw),
        'firmware_resolved': bool(fw_data),
        'rows': len(rows),
        'scsi_rows': sum(1 for r in rows if r.get('cdb_hex')),
        'read_rows': len(read_rows),
        'locked_read_rows': len(locked),
        'unlocked_read_rows': len(unlocked),
        'expected_effective_delta': expected_delta,
        'expected_effective_delta_hex': f'0x{expected_delta:08X}' if expected_delta else '',
        'expected_effective_delta_source': expected_source,
        'effective_delta_counts': dict((str(k), v) for k, v in sorted(eff_counts.items(), key=lambda kv: kv[0])),
        'unlocked_effective_delta_counts': dict((str(k), v) for k, v in sorted(unlocked_eff_counts.items(), key=lambda kv: kv[0])),
        'locked_effective_delta_counts': dict((str(k), v) for k, v in sorted(locked_eff_counts.items(), key=lambda kv: kv[0])),
        'legacy_configured_delta_counts': dict((str(k), v) for k, v in sorted(legacy_counts.items(), key=lambda kv: kv[0])),
        'shadow_configured_delta_counts': dict((str(k), v) for k, v in sorted(shadow_counts.items(), key=lambda kv: kv[0])),
        'legacy_delta_mismatch_is_expected_media_profile_override': bool(legacy_mismatch_is_expected),
        'live_effective_delta_ok': bool(live_effective_ok),
        'locked_direct_delta_ok': bool(locked_direct_ok),
        'locked_zero_delta_only_ok': bool(locked_zero_delta_only_ok),
        'locked_lba_map_report_ok': bool(locked_lba_map_report_ok),
        'locked_nonzero_delta_rows': locked_nonzero_delta_rows,
        'locked_nonzero_delta_interpretation': 'Allowed when the shadow LBA-map report validates the locked visible window; this is not an unlocked game-window delta proof.',
        'firmware_delta_constant_present': constant_present,
        'firmware_delta_match_count': len(matches),
        'firmware_delta_unique_offsets': sorted({m['offset'] for m in matches}),
        'firmware_delta_unique_rom_addresses': [f'0x{ROM_BASE_DEFAULT+o:08X}' for o in sorted({m['offset'] for m in matches})],
        'firmware_delta_context_classifications': dict(table_classifications),
        'firmware_delta_constant_standalone': bool(standalone),
        'firmware_xref_summary': xref_summary,
        'matches': matches,
        'firmware_derived_delta': firmware_derived_delta,
        'next_handoff_requirement': 'Trace executed firmware READ/window decision path from CDB LBA SRAM 0x05EC through the state/MMIO/SRAM value that selects the unlocked Redump game_output_start_lba delta.',
        'ok': ok,
    }


def write_md(rep: dict[str, Any], path: Path) -> None:
    lines: list[str] = []
    lines += ['# GDR-8050L v234 firmware-window delta audit', '']
    for k in ['ok','read_rows','locked_read_rows','unlocked_read_rows','expected_effective_delta','expected_effective_delta_hex','expected_effective_delta_source','live_effective_delta_ok','locked_direct_delta_ok','locked_zero_delta_only_ok','locked_lba_map_report_ok','legacy_delta_mismatch_is_expected_media_profile_override','firmware_delta_constant_present','firmware_delta_match_count','firmware_delta_context_classifications','firmware_delta_constant_standalone','firmware_derived_delta']:
        lines.append(f'- {k}: `{rep.get(k)}`')
    lines += ['', '## Delta counts', '', '| category | counts |', '|---|---|']
    for k in ['effective_delta_counts','unlocked_effective_delta_counts','locked_effective_delta_counts','legacy_configured_delta_counts','shadow_configured_delta_counts']:
        lines.append(f'| `{k}` | `{rep.get(k)}` |')
    lines += ['', '## Locked nonzero delta rows', '', '| index | host LBA | media LBA | delta | source |', '|---:|---:|---:|---:|---|']
    for r in rep.get('locked_nonzero_delta_rows') or []:
        lines.append(f"| `{r.get('index')}` | `{r.get('host_lba')}` | `{r.get('media_lba')}` | `{r.get('effective_delta')}` | `{r.get('source')}` |")
    if not rep.get('locked_nonzero_delta_rows'):
        lines.append('|  |  |  |  |  |')
    lines += ['', rep.get('locked_nonzero_delta_interpretation','')]

    lines += ['', '## Firmware-match interpretation', '']
    if rep.get('firmware_delta_constant_present'):
        lines.append('The expected effective Redump delta is present in the firmware bytes, but v234 does **not** promote it to firmware-derived behavior. The match is audited for table context and full-address xrefs so we do not mistake a table member for an executed READ-window constant.')
    else:
        lines.append('No firmware byte match was found for the expected effective delta.')
    x = rep.get('firmware_xref_summary') or {}
    lines += ['', f"- full matched-address reference count: `{x.get('full_matched_address_reference_count', 0)}`", f"- full table-base reference count: `{x.get('full_table_base_reference_count', 0)}`", f"- note: `{x.get('low16_reference_warning','')}`"]
    lines += ['', '## Matches', '', '| encoding | ROM address | pattern | context classification | dominant step | standalone |', '|---|---:|---|---|---|---|']
    for m in rep.get('matches') or []:
        tc = m.get('table_context') or {}
        lines.append(f"| `{m.get('encoding')}` | `{m.get('rom_address')}` | `{m.get('pattern_hex')}` | `{tc.get('classification','')}` | `{tc.get('dominant_step_hex','')}` | `{tc.get('constant_is_unique_standalone','')}` |")
    lines += ['', '## Next handoff requirement', '', rep.get('next_handoff_requirement','')]
    path.write_text('\n'.join(lines) + '\n', encoding='utf-8')


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument('jsonl', help='live iSCSI JSONL trace')
    ap.add_argument('--config', default='')
    ap.add_argument('--firmware', default='GDR-8050L_0L23_5af3ae9b_firmware_4c61eb12_plaintext_7cab0f5c.bin')
    ap.add_argument('--rom-base', default='0x90000000')
    ap.add_argument('--lba-map-json', default='')
    ap.add_argument('--media-layout-json', default='')
    ap.add_argument('--window-constant-json', default='')
    ap.add_argument('--expected-delta', default=None)
    ap.add_argument('--json', default='')
    ap.add_argument('--md', default='')
    args = ap.parse_args(argv)
    rep = build_report(args)
    if args.json:
        Path(args.json).write_text(json.dumps(rep, indent=2) + '\n', encoding='utf-8')
    if args.md:
        write_md(rep, Path(args.md))
    print(f"[FIRMWARE-WINDOW-DELTA-AUDIT] ok={str(rep['ok']).lower()} expected_delta={rep.get('expected_effective_delta')} live_effective_ok={str(rep.get('live_effective_delta_ok')).lower()} locked_lba_map_ok={str(rep.get('locked_lba_map_report_ok')).lower()} locked_nonzero_rows={len(rep.get('locked_nonzero_delta_rows') or [])} constant_present={str(rep.get('firmware_delta_constant_present')).lower()} table_class={rep.get('firmware_delta_context_classifications')} firmware_derived_delta={str(rep.get('firmware_derived_delta')).lower()}")
    return 0 if rep.get('ok') else 2


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