#!/usr/bin/env python3
"""Reduce GDR-8050L route-scan traces to the packet/FIFO evidence that matters.

This is intentionally narrower than hlds_lba_trace_validator.py.  v189 is about
answering one question quickly: did the natural route consume D954 and DF38 like
real host-interface status/FIFO surfaces, and did that move execution from the
CA1A/E4C5 wait into the READ/LBA assembler at 0x9001E541?
"""
from __future__ import annotations

import argparse
import csv
import json
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any

INTERESTING_PCS = {
    0x9000606B: "main_loop_0606b",
    0x90006608: "main_loop_6608",
    0x9001D56D: "dispatcher_wrapper_d56d",
    0x9001E303: "read_dispatch_func_e303",
    0x9001E3D6: "read_compare_block_e3d6",
    0x9001E3E0: "read10_compare_e3e0",
    0x9001E3E7: "read12_compare_e3e7",
    0x9001E4BE: "read_service_wait_entry_e4be",
    0x9001E4C5: "ca1a_poll_call_e4c5",
    0x9001E4CF: "after_ca1a_wait_e4cf",
    0x9001E4E5: "lba_assembler_entry_e4e5",
    0x9001E4E8: "read10_compare_e4e8",
    0x9001E500: "read12_compare_e500",
    0x9001E522: "packet_state_bset_05b6",
    0x9001E527: "dfa2_lba_b0_read",
    0x9001E52D: "dfa3_lba_b1_read",
    0x9001E533: "dfa4_lba_b2_read",
    0x9001E539: "dfa5_lba_b3_read",
    0x9001E541: "assembled_lba_write_05ec",
    0x9001E551: "opcode_shadow_05d0_write",
    0x9001E556: "phase_06f1_write",
    0x9001CA1A: "d954_read_ca1a",
    0x9001CA20: "d954_branch_ca20",
    0x9001CA25: "d954_ack_value_ca25",
    0x9001CA27: "d954_ack_write_ca27",
    0x9001CA2A: "df38_copy_call_ca2a",
    0x9001CA2F: "ca1a_success_ret_ca2f",
    0x9001CA42: "df38_copy_0",
    0x9001CA48: "df38_copy_1",
    0x9001CA4E: "df38_copy_2",
    0x9001CA54: "df38_copy_3",
    0x9001CA5A: "df38_copy_4",
    0x9001CA60: "df38_copy_5",
    0x9001CA66: "df38_copy_6",
    0x9001CA6C: "df38_copy_7",
    0x9001CA77: "df38_copy_return",
}

INTERESTING_ADDRS = {
    0x0000002C: "irq_status_002c",
    0x00000536: "scheduler_0536",
    0x000005D0: "cdb_shadow_05d0",
    0x000005EC: "assembled_lba_05ec",
    0x000005F0: "packet_state_05f0",
    0x000006F1: "phase_06f1",
    0x00000704: "df38_copy_dst_0704",
    0x00000728: "dispatcher_slot_0728",
    0x00000774: "cdb_mirror_0774",
    0x00000A8D: "service_gate_0a8d",
    0x0000D108: "data_phase_d108",
    0x0000D950: "host_status_d950",
    0x0000D954: "service_ready_d954",
    0x0000DF30: "packet_frame_df30",
    0x0000DF38: "service_fifo_df38",
    0x0000DFA0: "packet_latch_dfa0",
}

EVENTS_OF_INTEREST = {
    "gdr_session_command_arm",
    "gdr_session_command_complete",
    "gdr_session_dfa0_latch",
    "gdr_session_seed_memory",
    "gdr_session_d954_ready_read",
    "gdr_session_d954_ack_write",
    "gdr_session_d954_ack_clear",
    "gdr_session_df38_fifo_read",
    "gdr_session_firmware_ingress_call",
    "gdr_session_firmware_lba_source",
    "gdr_session_data_phase_request",
    "gdr_session_d108_hw_data_phase_complete",
    "gdr_session_complete",
}


def parse_int(v: Any, default: int | None = None) -> int:
    if v is None or v == "":
        if default is None:
            raise ValueError("empty integer")
        return default
    if isinstance(v, int):
        return v
    s = str(v).strip()
    if not s:
        if default is None:
            raise ValueError("empty integer")
        return default
    return int(s, 0)


def norm_int(v: Any) -> int | None:
    try:
        return parse_int(v)
    except Exception:
        return None


def read_rows(path: Path) -> list[dict[str, str]]:
    with path.open(newline="", encoding="utf-8", errors="ignore") as f:
        return [dict(r) for r in csv.DictReader(f)]


def interesting_addr_name(addr: int | None) -> str | None:
    if addr is None:
        return None
    for base, name in INTERESTING_ADDRS.items():
        if base <= addr < base + (0x10 if base in (0x5D0, 0x704, 0x774, 0xDF30, 0xDF38, 0xDFA0) else 4):
            return f"{name}+0x{addr-base:X}"
    return None


def reduce_trace(path: Path, *, max_samples: int = 40) -> dict[str, Any]:
    rows = read_rows(path)
    events = Counter((r.get("event") or "") for r in rows)
    pc_hits = Counter()
    event_samples: list[dict[str, Any]] = []
    addr_samples: list[dict[str, Any]] = []
    command_names: list[str] = []
    per_command = defaultdict(Counter)
    current = "<pre>"

    for i, r in enumerate(rows):
        ev = r.get("event") or ""
        pc = norm_int(r.get("pc"))
        addr = norm_int(r.get("addr"))
        phys = norm_int(r.get("addr_physical"))
        if ev == "gdr_session_command_arm":
            current = r.get("name") or f"cmd{len(command_names):02d}"
            command_names.append(current)
        if pc in INTERESTING_PCS:
            pc_hits[INTERESTING_PCS[pc]] += 1
            per_command[current][INTERESTING_PCS[pc]] += 1
        if ev in EVENTS_OF_INTEREST and len(event_samples) < max_samples:
            event_samples.append({
                "row": i, "event": ev, "name": r.get("name"), "pc": r.get("pc"),
                "addr": r.get("addr"), "value": r.get("value"),
                "bytes_hex": r.get("bytes_hex"), "notes": r.get("notes"),
            })
        an = interesting_addr_name(addr) or interesting_addr_name(phys)
        if an and len(addr_samples) < max_samples:
            addr_samples.append({
                "row": i, "addr_name": an, "event": ev, "name": r.get("name"),
                "pc": r.get("pc"), "addr": r.get("addr"), "addr_physical": r.get("addr_physical"),
                "value": r.get("value"), "bytes_hex": r.get("bytes_hex"), "notes": r.get("notes"),
            })

    outcome_hint = "BLOCKED"
    if pc_hits.get("assembled_lba_write_05ec", 0) or events.get("gdr_session_firmware_lba_source", 0):
        outcome_hint = "PROVEN_OR_CONTROLLED"
    elif pc_hits.get("read10_compare_e3e0", 0) or pc_hits.get("read10_compare_e4e8", 0) or events.get("gdr_session_d954_ready_read", 0):
        outcome_hint = "PARTIAL_FIFO_PROGRESS"

    return {
        "trace_csv": str(path),
        "rows": len(rows),
        "outcome_hint": outcome_hint,
        "command_count": len(command_names),
        "commands": command_names,
        "event_counts": dict(events),
        "interesting_pc_hits": dict(pc_hits),
        "per_command_interesting_pc_hits": {k: dict(v) for k, v in per_command.items()},
        "event_samples": event_samples,
        "addr_samples": addr_samples,
    }


def write_md(path: Path, reduced: dict[str, Any]) -> None:
    lines: list[str] = []
    lines.append("# GDR-8050L v189 FIFO route reducer")
    lines.append("")
    lines.append(f"trace_csv={reduced.get('trace_csv')}")
    lines.append(f"rows={reduced.get('rows')}")
    lines.append(f"outcome_hint={reduced.get('outcome_hint')}")
    lines.append(f"command_count={reduced.get('command_count')}")
    lines.append("")
    lines.append("## Key event counts")
    lines.append("")
    for key in sorted(EVENTS_OF_INTEREST):
        n = (reduced.get("event_counts") or {}).get(key, 0)
        if n:
            lines.append(f"- `{key}`: {n}")
    lines.append("")
    lines.append("## Interesting PC hits")
    lines.append("")
    pcs = reduced.get("interesting_pc_hits") or {}
    if pcs:
        for key, n in sorted(pcs.items()):
            lines.append(f"- `{key}`: {n}")
    else:
        lines.append("- none")
    lines.append("")
    lines.append("## First interesting events")
    lines.append("")
    for s in reduced.get("event_samples") or []:
        lines.append(f"- row {s.get('row')}: `{s.get('event')}` `{s.get('name')}` pc={s.get('pc')} addr={s.get('addr')} value={s.get('value')} bytes={s.get('bytes_hex')} notes={s.get('notes')}")
    lines.append("")
    lines.append("## First interesting address touches")
    lines.append("")
    for s in reduced.get("addr_samples") or []:
        lines.append(f"- row {s.get('row')}: `{s.get('addr_name')}` via `{s.get('event')}` pc={s.get('pc')} addr={s.get('addr')} value={s.get('value')} bytes={s.get('bytes_hex')} notes={s.get('notes')}")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def main() -> int:
    ap = argparse.ArgumentParser(description="Reduce GDR-8050L v189 packet/FIFO route traces")
    ap.add_argument("trace_csv", nargs="+", help="Trace CSV(s) from mn103s_emu.py or hlds_lba_route_scan_builder.py")
    ap.add_argument("--out-json", default=None)
    ap.add_argument("--out-md", default=None)
    ap.add_argument("--max-samples", type=int, default=40)
    args = ap.parse_args()

    reduced = [reduce_trace(Path(p), max_samples=args.max_samples) for p in args.trace_csv]
    obj: Any = reduced[0] if len(reduced) == 1 else {"traces": reduced}
    if args.out_json:
        out = Path(args.out_json)
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(json.dumps(obj, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    if args.out_md:
        out = Path(args.out_md)
        if len(reduced) == 1:
            write_md(out, reduced[0])
        else:
            lines = ["# GDR-8050L v189 FIFO route reducer", ""]
            for r in reduced:
                lines.append(f"- `{r.get('trace_csv')}`: rows={r.get('rows')} outcome_hint={r.get('outcome_hint')} commands={r.get('command_count')}")
            out.parent.mkdir(parents=True, exist_ok=True)
            out.write_text("\n".join(lines) + "\n", encoding="utf-8")
    if not args.out_json and not args.out_md:
        print(json.dumps(obj, indent=2, sort_keys=True))
    return 0


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