"""
ParseDump.py
============
Parses the latest binary dump folder found next to this script.

Run from the workspace root (c:\\Users\\Usuario\\Parsershowers):
    python ParseDump.py

Optional CLI arguments:
    --stream  <name>   Only print a specific stream  (e.g. shower_data_s0_mb2_sl1)
    --max     <n>      Maximum number of items to print per stream  (default: 5)
    --list             Just list available streams and their sizes, then exit
"""

import sys
import os
import argparse
from collections import Counter

_script_dir = os.path.dirname(os.path.abspath(__file__))

def _find_and_prep_dump():
    # Pick the most recent dump_* folder (lexicographic sort = chronological)
    candidates = sorted(
        [d for d in os.listdir(_script_dir)
         if d.startswith("dump_") and os.path.isdir(os.path.join(_script_dir, d))],
        reverse=True,
    )
    if not candidates:
        print("ERROR: No dump_* folder found next to ParseDump.py.")
        sys.exit(1)
    name = candidates[0]
    outer_dir = os.path.join(_script_dir, name)

    # Add outer folder to sys.path so `import dump_X` resolves to the inner package
    if outer_dir not in sys.path:
        sys.path.insert(0, outer_dir)

    # Patch __init__.py to guard against empty fetchlog entries (idempotent)
    init_path = os.path.join(outer_dir, name, "__init__.py")
    if os.path.exists(init_path):
        src = open(init_path, encoding="utf-8").read()
        guard = "# Guard: some streams have no data in this dump"
        if guard not in src:
            old = (
                "    self.current = {'file'     :{'i':0,'start':0,'end':self.contexts['file'     ][0]['end']},\n"
                "                    'timestamp':{'i':0,'start':0,'end':self.contexts['timestamp'][0]['end']},}\n"
                "    self.current_ts =      self.contexts['timestamp'][0]['value']\n"
                "    self.current_f  = open(self.contexts['file'     ][0]['value'], 'rb')\n"
                "    self.timestamp_fields = timestamp_fields[i]\n"
                "    self.decode_f = fw_data_translate.__getattribute__(DATA_SRCS[i]['decode_f'])\n"
                "    self.last_data_i = -10"
            )
            new = (
                "    # Guard: some streams have no data in this dump\n"
                "    if not self.contexts['file']:\n"
                "      self.length = 0\n"
                "      self.current = None\n"
                "      self.current_ts = None\n"
                "      self.current_f  = None\n"
                "      self.timestamp_fields = timestamp_fields[i]\n"
                "      self.decode_f = fw_data_translate.__getattribute__(DATA_SRCS[i]['decode_f'])\n"
                "      self.last_data_i = -10\n"
                "      return\n"
                "    self.current = {'file'     :{'i':0,'start':0,'end':self.contexts['file'     ][0]['end']},\n"
                "                    'timestamp':{'i':0,'start':0,'end':self.contexts['timestamp'][0]['end']},}\n"
                "    self.current_ts =      self.contexts['timestamp'][0]['value']\n"
                "    self.current_f  = open(self.contexts['file'     ][0]['value'], 'rb')\n"
                "    self.timestamp_fields = timestamp_fields[i]\n"
                "    self.decode_f = fw_data_translate.__getattribute__(DATA_SRCS[i]['decode_f'])\n"
                "    self.last_data_i = -10"
            )
            if old in src:
                open(init_path, "w", encoding="utf-8").write(src.replace(old, new))

    return name

DUMP_NAME = _find_and_prep_dump()

try:
    dump = __import__(DUMP_NAME)
except Exception as e:
    print(f"ERROR: Could not import {DUMP_NAME}: {e}")
    sys.exit(1)

print(f"Using dump: {DUMP_NAME}")


# ---------------------------------------------------------------------------
# Formatters  (one per data type)
# ---------------------------------------------------------------------------

def fmt_shower_data(d: dict) -> str:
    """Pretty-print a shower_data datum."""
    arr   = d.get("arrival", {})
    ts    = d.get("store_time", "?")
    sd    = d  # also contains the decoded fields directly
    lines = [
        f"  store_time   : {ts:.3f}",
        f"  arrival      : OC={arr.get('OC','?')}  BX={arr.get('BX','?')}  CC={arr.get('CC','?')}",
        f"  minimum_wire : {sd.get('minimum_wire','?')}",
        f"  maximum_wire : {sd.get('maximum_wire','?')}",
        f"  oldest_bx    : {sd.get('oldest_bx','?')}",
    ]
    counters = sd.get("wire_counters")
    if counters is not None:
        active = [(i, c) for i, c in enumerate(counters) if c > 0]
        lines.append(f"  active wires : {len(active)}")
        if active:
            lines.append("  wire counts  : " +
                         "  ".join(f"wi{i:02d}={c}" for i, c in active))
    if "str" in sd:
        lines.append(f"  summary      : {sd['str']}")
    return "\n".join(lines)


def fmt_shower_flags(d: dict) -> str:
    """Pretty-print a shower_flags datum."""
    arr = d.get("arrival", {})
    ts  = d.get("store_time", "?")
    return (
        f"  store_time   : {ts:.3f}\n"
        f"  arrival      : OC={arr.get('OC','?')}  BX={arr.get('BX','?')}  CC={arr.get('CC','?')}\n"
        f"  shower_thr   : {d.get('shower_thr','?')}\n"
        f"  shower_thr_am: {d.get('shower_thr_am','?')}\n"
        f"  summary      : {d.get('str','')}"
    )


def fmt_shower_hits(d: dict) -> str:
    """Pretty-print a shower_hits (slhit) datum."""
    arr = d.get("arrival", {})
    ts  = d.get("store_time", "?")
    ns  = int(25 * (d.get("bctr", 0) + d.get("tdc", 0) / 32.0))
    return (
        f"  store_time   : {ts:.3f}\n"
        f"  arrival      : OC={arr.get('OC','?')}  BX={arr.get('BX','?')}  CC={arr.get('CC','?')}\n"
        f"  layer        : {d.get('ly','?')}\n"
        f"  wire         : {d.get('wi','?')}\n"
        f"  ti           : {d.get('ti','?')}\n"
        f"  bx.tdc       : {d.get('bctr','?')}.{d.get('tdc','?')}  ({ns} ns)\n"
        f"  summary      : {d.get('str','')}"
    )


def pick_formatter(stream_name: str):
    if "shower_data" in stream_name:
        return fmt_shower_data
    if "shower_flags" in stream_name:
        return fmt_shower_flags
    if "shower_hits" in stream_name:
        return fmt_shower_hits
    # fallback: just print the raw dict
    return lambda d: str(d)


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def list_streams():
    print(f"{'Stream name':<40}  {'Items':>6}")
    print("-" * 50)
    for name, stream in dump.streams.items():
        print(f"{name:<40}  {len(stream):>6}")


def print_stream(name: str, stream, max_items: int):
    fmt = pick_formatter(name)
    total = len(stream)
    n = min(max_items, total)
    print(f"\n{'=' * 60}")
    print(f"Stream: {name}   ({total} items total, showing first {n})")
    print("=" * 60)
    for i in range(n):
        datum = stream[i]
        print(f"  --- item {i} ---")
        print(fmt(datum))
    if total > n:
        print(f"  ... ({total - n} more items not shown)")


def plot_spike_plots(stream_filter=None):
    """
    For every non-empty shower_hits stream (or those matching stream_filter),
    plot a spike plot: x = BX (bunch-crossing), y = number of hits at that BX.
    Each wire gets its own colour so overlapping hits are visible.
    """
    import matplotlib.pyplot as plt

    hit_streams = {
        name: stream
        for name, stream in dump.streams.items()
        if "shower_hits" in name
        and len(stream) > 0
        and (stream_filter is None or stream_filter in name)
    }

    if not hit_streams:
        print("No non-empty shower_hits streams found for the given filter.")
        return

    n_plots = len(hit_streams)
    fig, axes = plt.subplots(n_plots, 1, figsize=(14, 3.5 * n_plots), squeeze=False)
    fig.suptitle("Shower hits — spikes per BX", fontsize=13, fontweight="bold")

    for ax, (name, stream) in zip(axes[:, 0], hit_streams.items()):
        # Deduplicate hits by (abs_bx, ly, wi, bctr, tdc)
        seen_hits = set()
        unique_hits = []
        for d in stream:
            hkey = (d["arrival"]["OC"] * 3564 + d["bctr"], d["ly"], d["wi"], d["bctr"], d["tdc"])
            if hkey not in seen_hits:
                seen_hits.add(hkey)
                unique_hits.append(d)
        # Absolute BX = OC * 3564 + local BX, then apply offset
        bx_total  = Counter(d["arrival"]["OC"] * 3564 + d["bctr"] for d in unique_hits)
        bx_sorted = sorted(bx_total)
        bx_offset = bx_sorted[0]
        bx_rel    = [b - bx_offset for b in bx_sorted]
        counts    = [bx_total[b] for b in bx_sorted]

        ax.vlines(bx_rel, 0, counts, colors="steelblue", linewidth=1.2)

        ax.set_title(name, fontsize=9)
        ax.set_xlabel(f"BX  (offset +{bx_offset:,})")
        ax.set_ylabel("Hit count")
        ax.set_xlim(min(bx_rel) - 2, max(bx_rel) + 2)
        ax.set_ylim(0)
        ax.grid(axis="x", linestyle="--", alpha=0.3)

    plt.tight_layout()
    plt.show()


def plot_interactive_by_station(stream_filter=None, out_dir="."):
    """
    Build one interactive Plotly HTML figure per station (mb1–mb4).
    Each figure has one subplot per sector×SL combo, showing:
      - Blue spikes : hits per absolute BX  (shower_hits, left y-axis)
      - Orange line : active wire count per shower event (shower_data, right y-axis)
      - Green bands : wire range [minimum_wire, maximum_wire] per shower event
    Files are saved as <out_dir>/spikes_mb<N>.html
    """
    import re
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots

    # Build per-station dict keyed by (sector, SL) base name
    # e.g. "s0_mb2_sl1"  →  {hits: stream, data: stream}
    # Outer key is (sector, mb), e.g. ("s0", "mb2")
    station_groups: dict = {}
    for name, stream in dump.streams.items():
        m = re.search(r"(shower_hits|shower_data|shower_flags)_((s\d+)_mb(\d+)_sl\d+)$", name)
        if not m:
            continue
        kind   = m.group(1)          # shower_hits / shower_data / shower_flags
        key    = m.group(2)          # e.g. s0_mb2_sl1
        sector = m.group(3)          # e.g. s0
        mb     = f"mb{m.group(4)}"   # e.g. mb2
        if len(stream) == 0:
            continue
        if stream_filter and stream_filter not in name:
            continue
        station_groups.setdefault((sector, mb), {}).setdefault(key, {})[kind] = stream

    if not station_groups:
        print("No non-empty streams found.")
        return

    os.makedirs(out_dir, exist_ok=True)

    for (sector, mb), groups in sorted(station_groups.items()):
        # Only keep groups that have hits data
        groups = {k: v for k, v in groups.items() if "shower_hits" in v}
        if not groups:
            continue

        n = len(groups)
        fig = make_subplots(
            rows=n, cols=1,
            shared_xaxes=True,
            subplot_titles=list(sorted(groups.keys())),
            specs=[[{"secondary_y": True}]] * n,
            vertical_spacing=0.04,
        )

        for row, (key, streams) in enumerate(sorted(groups.items()), start=1):
            # ── Hits spikes (left y-axis) ──────────────────────────────────
            # Deduplicate hits by (abs_bx, ly, wi, bctr, tdc) before counting
            seen_hits = set()
            unique_hits = []
            for d in streams["shower_hits"]:
                hkey = (d["arrival"]["OC"] * 3564 + d["bctr"], d["ly"], d["wi"], d["bctr"], d["tdc"])
                if hkey not in seen_hits:
                    seen_hits.add(hkey)
                    unique_hits.append(d)
            hits = unique_hits
            # Group hits by abs_bx so hover shows all hits at that BX
            from collections import defaultdict
            hits_by_bx = defaultdict(list)
            for d in hits:
                hits_by_bx[d["arrival"]["OC"] * 3564 + d["bctr"]].append(d)
            bx_sorted = sorted(hits_by_bx)

            bx_offset = bx_sorted[0]
            x_spike, y_spike, hit_text = [], [], []
            for bx in bx_sorted:
                group = hits_by_bx[bx]
                cnt = len(group)
                xv = bx - bx_offset
                lines = [f"<b>abs_BX {bx}  (BX+{bx - bx_offset})</b>  — {cnt} hit(s)"]
                for h in group:
                    lines.append(f"  ly={h['ly']}  wi={h['wi']}  bctr={h['bctr']}  tdc={h['tdc']}")
                tip = "<br>".join(lines)
                x_spike += [xv, xv, None]
                y_spike += [0,  cnt, None]
                hit_text += [tip, tip, ""]

            fig.add_trace(
                go.Scatter(
                    x=x_spike, y=y_spike,
                    text=hit_text,
                    mode="lines",
                    line=dict(color="steelblue", width=1.5),
                    name="hits",
                    hovertemplate="%{text}<extra></extra>",
                    legendgroup="hits", showlegend=(row == 1),
                ),
                row=row, col=1, secondary_y=False,
            )

            # Tight x range from data
            x_min = 0
            x_max = bx_sorted[-1] - bx_offset
            pad = max(1, (x_max - x_min) * 0.02)

            # ── Shower data spikes (right y-axis) ──────────────────────────
            if "shower_data" in streams:
                seen_showers = set()
                unique_showers = []
                for d in streams["shower_data"]:
                    abs_bx = d["arrival"]["OC"] * 3564 + d["oldest_bx"]
                    wc = tuple(d["wire_counters"])
                    key_s = (abs_bx, d["minimum_wire"], d["maximum_wire"], wc)
                    if key_s not in seen_showers:
                        seen_showers.add(key_s)
                        unique_showers.append((abs_bx, d))

                x_sw, y_sw, sw_text = [], [], []
                for abs_bx, d in unique_showers:
                    active = sum(d["wire_counters"])
                    xv = abs_bx - bx_offset
                    wc_str = ", ".join(str(c) for c in d["wire_counters"])
                    tip = (
                        f"<b>Shower  abs_oldest_BX={abs_bx}</b><br>"
                        f"min_wire={d['minimum_wire']}  max_wire={d['maximum_wire']}<br>"
                        f"active_wires={active}<br>"
                        f"wire_counters=[{wc_str}]"
                    )
                    x_sw += [xv, xv, None]
                    y_sw += [0, active, None]
                    sw_text += [tip, tip, ""]

                fig.add_trace(
                    go.Scatter(
                        x=x_sw, y=y_sw,
                        text=sw_text,
                        mode="lines",
                        line=dict(color="darkorange", width=2),
                        name="active wires",
                        hovertemplate="%{text}<extra></extra>",
                        legendgroup="shower", showlegend=(row == 1),
                    ),
                    row=row, col=1, secondary_y=True,
                )

            fig.update_yaxes(title_text="Hits / BX", row=row, col=1, secondary_y=False,
                             rangemode="tozero")
            fig.update_yaxes(title_text="Total wire hits", row=row, col=1, secondary_y=True,
                             showgrid=False, rangemode="tozero")
            fig.update_xaxes(range=[x_min - pad, x_max + pad], row=row, col=1)
            # Only label the bottom x-axis
            if row == n:
                fig.update_xaxes(title_text=f"BX  (offset +{bx_offset:,})", row=row, col=1)

            # ── Rate annotation ────────────────────────────────────────────
            total_hits  = len(hits)
            bx_span     = bx_sorted[-1] - bx_sorted[0] + 1
            time_s      = bx_span * 25e-9
            rate_hz     = total_hits / time_s if time_s > 0 else 0
            if rate_hz >= 1e6:
                rate_str = f"{rate_hz/1e6:.2f} MHz"
            elif rate_hz >= 1e3:
                rate_str = f"{rate_hz/1e3:.2f} kHz"
            else:
                rate_str = f"{rate_hz:.1f} Hz"
            rate_label = (
                f"{total_hits} hits / {bx_span} BX "
                f"({bx_span*25:.0f} ns)  →  hit rate = {rate_str}"
            )
            # Place annotation at top-right of this subplot
            fig.add_annotation(
                text=rate_label,
                xref=f"x{row if row > 1 else ''}", yref=f"y{row if row > 1 else ''}",
                x=x_max, y=max(len(v) for v in hits_by_bx.values()),
                xanchor="right", yanchor="top",
                showarrow=False,
                font=dict(size=11, color="steelblue"),
                bgcolor="rgba(255,255,255,0.7)",
                row=row, col=1,
            )

            # ── Shower rate annotation ─────────────────────────────────────
            if "shower_data" in streams and unique_showers:
                n_showers   = len(unique_showers)
                shower_bxs  = [abs_bx for abs_bx, _ in unique_showers]
                sh_span     = max(shower_bxs) - min(shower_bxs) + 1
                sh_time_s   = sh_span * 25e-9
                sh_rate_hz  = n_showers / sh_time_s if sh_time_s > 0 else 0
                if sh_rate_hz >= 1e6:
                    sh_rate_str = f"{sh_rate_hz/1e6:.2f} MHz"
                elif sh_rate_hz >= 1e3:
                    sh_rate_str = f"{sh_rate_hz/1e3:.2f} kHz"
                else:
                    sh_rate_str = f"{sh_rate_hz:.1f} Hz"
                fig.add_annotation(
                    text=f"{n_showers} showers / {sh_span} BX ({sh_span*25:.0f} ns)  →  shower rate = {sh_rate_str}",
                    xref=f"x{row if row > 1 else ''}", yref=f"y{row if row > 1 else ''}",
                    x=x_min, y=max(len(v) for v in hits_by_bx.values()),
                    xanchor="left", yanchor="top",
                    showarrow=False,
                    font=dict(size=11, color="darkorange"),
                    bgcolor="rgba(255,255,255,0.7)",
                    row=row, col=1,
                )

        fig.update_layout(
            title=f"Shower hits + shower data — {sector.upper()} {mb.upper()}",
            height=220 * n + 80,
            margin=dict(l=60, r=60, t=60, b=50),
            template="plotly_white",
            legend=dict(orientation="h", y=1.04, x=0, xanchor="left"),
        )

        out_path = os.path.join(out_dir, f"spikes_{sector}_{mb}.html")
        fig.write_html(out_path)
        print(f"Saved: {out_path}")


def _build_persistence_curve(hit_bxs, persist=16):
    """
    Given a list of absolute BX values where hits occur,
    return (x_rel, y_counts, bx_offset) where each hit contributes
    +1 to the `persist` consecutive BX bins starting at its position.
    Uses a sparse dict; inserts None breaks between clusters separated
    by more than `persist` BX so Plotly does not bridge the gaps.
    x_rel is offset-corrected (starts at 0).
    """
    if not hit_bxs:
        return [], [], 0
    bx_offset = min(hit_bxs)
    counts: dict = {}
    for bx in hit_bxs:
        rel = bx - bx_offset
        for i in range(persist):
            counts[rel + i] = counts.get(rel + i, 0) + 1
    xs = sorted(counts)
    # Build output with None gaps between disconnected segments
    x_out, y_out = [], []
    for i, x in enumerate(xs):
        if i > 0 and x - xs[i - 1] > 1:
            x_out.append(None)
            y_out.append(None)
        x_out.append(x)
        y_out.append(counts[x])
    return x_out, y_out, bx_offset


def plot_persist_by_station(stream_filter=None, out_dir=".", persist=16):
    """
    Like plot_interactive_by_station but each hit is spread over `persist` BX bins,
    showing the cumulative occupancy a downstream trigger would see.
    Output files: persist_{sector}_{mb}.html
    """
    import re
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots

    station_groups: dict = {}
    for name, stream in dump.streams.items():
        m = re.search(r"(shower_hits|shower_data|shower_flags)_((s\d+)_mb(\d+)_sl\d+)$", name)
        if not m:
            continue
        kind   = m.group(1)
        key    = m.group(2)
        sector = m.group(3)
        mb     = f"mb{m.group(4)}"
        if len(stream) == 0:
            continue
        if stream_filter and stream_filter not in name:
            continue
        station_groups.setdefault((sector, mb), {}).setdefault(key, {})[kind] = stream

    if not station_groups:
        print("No non-empty streams found.")
        return

    os.makedirs(out_dir, exist_ok=True)

    for (sector, mb), groups in sorted(station_groups.items()):
        groups = {k: v for k, v in groups.items() if "shower_hits" in v}
        if not groups:
            continue

        n = len(groups)
        fig = make_subplots(
            rows=n, cols=1,
            shared_xaxes=True,
            subplot_titles=list(sorted(groups.keys())),
            specs=[[{"secondary_y": True}]] * n,
            vertical_spacing=0.04,
        )

        for row, (key, streams) in enumerate(sorted(groups.items()), start=1):
            # ── Deduplicate hits ───────────────────────────────────────────
            seen_hits = set()
            hit_bxs = []
            for d in streams["shower_hits"]:
                abs_bx = d["arrival"]["OC"] * 3564 + d["bctr"]
                hkey = (abs_bx, d["ly"], d["wi"], d["tdc"])
                if hkey not in seen_hits:
                    seen_hits.add(hkey)
                    hit_bxs.append(abs_bx)

            x_rel, counts, bx_offset = _build_persistence_curve(hit_bxs, persist)

            fig.add_trace(
                go.Scatter(
                    x=x_rel, y=counts,
                    mode="lines",
                    line=dict(color="steelblue", width=1.5),
                    name=f"hits (×{persist} BX)",
                    hovertemplate=f"BX+{bx_offset:,}=%{{x}}<br>Occupancy=%{{y}}<extra>hits</extra>",
                    legendgroup="hits", showlegend=(row == 1),
                ),
                row=row, col=1, secondary_y=False,
            )

            x_numeric = [v for v in x_rel if v is not None]
            x_min, x_max = 0, max(x_numeric) if x_numeric else 0
            pad = max(1, (x_max - x_min) * 0.02)

            # ── Shower data spikes (right y-axis) ──────────────────────────
            if "shower_data" in streams:
                seen_showers = set()
                unique_showers = []
                for d in streams["shower_data"]:
                    abs_bx = d["arrival"]["OC"] * 3564 + d["oldest_bx"]
                    wc = tuple(d["wire_counters"])
                    key_s = (abs_bx, d["minimum_wire"], d["maximum_wire"], wc)
                    if key_s not in seen_showers:
                        seen_showers.add(key_s)
                        unique_showers.append((abs_bx, d))

                x_sw, y_sw, sw_text = [], [], []
                for abs_bx, d in unique_showers:
                    active = sum(d["wire_counters"])
                    xv = abs_bx - bx_offset
                    wc_str = ", ".join(str(c) for c in d["wire_counters"])
                    tip = (
                        f"<b>Shower  abs_oldest_BX={abs_bx}</b><br>"
                        f"min_wire={d['minimum_wire']}  max_wire={d['maximum_wire']}<br>"
                        f"active_wires={active}<br>"
                        f"wire_counters=[{wc_str}]"
                    )
                    x_sw += [xv, xv, None]
                    y_sw += [0, active, None]
                    sw_text += [tip, tip, ""]

                fig.add_trace(
                    go.Scatter(
                        x=x_sw, y=y_sw,
                        text=sw_text,
                        mode="lines",
                        line=dict(color="darkorange", width=2),
                        name="active wires",
                        hovertemplate="%{text}<extra></extra>",
                        legendgroup="shower", showlegend=(row == 1),
                    ),
                    row=row, col=1, secondary_y=True,
                )

            fig.update_yaxes(title_text=f"Occupancy ({persist}-BX window)", row=row, col=1,
                             secondary_y=False, rangemode="tozero")
            fig.update_yaxes(title_text="Total wire hits", row=row, col=1,
                             secondary_y=True, showgrid=False, rangemode="tozero")
            fig.update_xaxes(range=[x_min - pad, x_max + pad], row=row, col=1)
            if row == n:
                fig.update_xaxes(title_text=f"BX  (offset +{bx_offset:,})", row=row, col=1)

            # ── Rate annotation ────────────────────────────────────────────
            total_hits = len(hit_bxs)
            bx_span    = max(hit_bxs) - min(hit_bxs) + 1 if hit_bxs else 1
            time_s     = bx_span * 25e-9
            rate_hz    = total_hits / time_s if time_s > 0 else 0
            if rate_hz >= 1e6:
                rate_str = f"{rate_hz/1e6:.2f} MHz"
            elif rate_hz >= 1e3:
                rate_str = f"{rate_hz/1e3:.2f} kHz"
            else:
                rate_str = f"{rate_hz:.1f} Hz"
            rate_label = (
                f"{total_hits} hits / {bx_span} BX "
                f"({bx_span*25:.0f} ns)  →  hit rate = {rate_str}"
            )
            y_numeric = [v for v in counts if v is not None]
            y_top = max(y_numeric) if y_numeric else 1
            fig.add_annotation(
                text=rate_label,
                x=x_max, y=y_top,
                xanchor="right", yanchor="top",
                showarrow=False,
                font=dict(size=11, color="steelblue"),
                bgcolor="rgba(255,255,255,0.7)",
                row=row, col=1,
            )

            # ── Shower rate annotation ─────────────────────────────────────
            if "shower_data" in streams and unique_showers:
                n_showers   = len(unique_showers)
                shower_bxs  = [abs_bx for abs_bx, _ in unique_showers]
                sh_span     = max(shower_bxs) - min(shower_bxs) + 1
                sh_time_s   = sh_span * 25e-9
                sh_rate_hz  = n_showers / sh_time_s if sh_time_s > 0 else 0
                if sh_rate_hz >= 1e6:
                    sh_rate_str = f"{sh_rate_hz/1e6:.2f} MHz"
                elif sh_rate_hz >= 1e3:
                    sh_rate_str = f"{sh_rate_hz/1e3:.2f} kHz"
                else:
                    sh_rate_str = f"{sh_rate_hz:.1f} Hz"
                fig.add_annotation(
                    text=f"{n_showers} showers / {sh_span} BX ({sh_span*25:.0f} ns)  →  shower rate = {sh_rate_str}",
                    x=x_min, y=y_top,
                    xanchor="left", yanchor="top",
                    showarrow=False,
                    font=dict(size=11, color="darkorange"),
                    bgcolor="rgba(255,255,255,0.7)",
                    row=row, col=1,
                )

        fig.update_layout(
            title=f"Hit occupancy ({persist}-BX persistence) — {sector.upper()} {mb.upper()}",
            height=220 * n + 80,
            margin=dict(l=60, r=60, t=60, b=50),
            template="plotly_white",
            legend=dict(orientation="h", y=1.04, x=0, xanchor="left"),
        )

        out_path = os.path.join(out_dir, f"persist_{sector}_{mb}.html")
        fig.write_html(out_path)
        print(f"Saved: {out_path}")


def dump_raw_to_file(stream_filter=None, out_dir="."):
    """
    Write raw field values for every non-empty stream to a TSV file.
    One file per stream: <out_dir>/<stream_name>.tsv

    Fields kept per stream type:
      shower_data  : store_time, minimum_wire, maximum_wire, oldest_bx, wire_counters
      shower_flags : store_time, shower_thr, shower_thr_am
      shower_hits  : store_time, ly, wi, bctr, tdc
    """
    import csv

    # Fields to keep, in order, keyed by substring present in the stream name.
    # 'raw_data', 'str', and arrival_* are always dropped.
    KEEP = {
        "shower_data":  ["store_time", "minimum_wire", "maximum_wire", "abs_oldest_bx", "oldest_bx", "wire_counters"],
        "shower_flags": ["store_time", "shower_thr", "shower_thr_am"],
        "shower_hits":  ["store_time", "abs_bx", "ly", "wi", "bctr", "tdc"],
    }

    matched = {
        name: stream
        for name, stream in dump.streams.items()
        if len(stream) > 0 and (stream_filter is None or stream_filter in name)
    }

    if not matched:
        print("No non-empty streams found for the given filter.")
        return

    os.makedirs(out_dir, exist_ok=True)

    for name, stream in matched.items():
        # Determine which fields to keep for this stream type
        fieldnames = next((v for k, v in KEEP.items() if k in name), None)

        out_path = os.path.join(out_dir, f"{name}.tsv")
        rows = []
        for datum in stream:
            if fieldnames:
                row = {k: datum[k] for k in fieldnames if k in datum}
                # Computed fields
                if "abs_bx" in fieldnames:
                    row["abs_bx"] = datum["arrival"]["OC"] * 3564 + datum["bctr"]
                if "abs_oldest_bx" in fieldnames:
                    row["abs_oldest_bx"] = datum["arrival"]["OC"] * 3564 + datum["oldest_bx"]
                if "wire_counters" in row and isinstance(row["wire_counters"], list):
                    row["wire_counters"] = ",".join(str(x) for x in row["wire_counters"])
            else:
                # Unknown type: keep everything except raw_data and str
                row = {k: v for k, v in datum.items()
                       if k not in ("raw_data", "str", "arrival")}
            rows.append(row)

        if not rows:
            continue

        # Deduplicate rows ignoring store_time
        if "shower_data" in name or "shower_flags" in name or "shower_hits" in name:
            seen = set()
            deduped = []
            for row in rows:
                key = tuple(str(row[k]) for k in row if k != "store_time")
                if key not in seen:
                    seen.add(key)
                    deduped.append(row)
            rows = deduped

        cols = fieldnames if fieldnames else list(rows[0].keys())
        with open(out_path, "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=cols, delimiter="\t")
            writer.writeheader()
            writer.writerows(rows)

        print(f"Saved: {out_path}  ({len(rows)} rows)")


def main():
    parser = argparse.ArgumentParser(description="Parse dump_20260518_170835 binary data.")
    parser.add_argument("--stream", default=None,
                        help="Name of a specific stream to print (substring match allowed).")
    parser.add_argument("--max", type=int, default=5,
                        help="Maximum number of items to print per stream (default: 5).")
    parser.add_argument("--list", action="store_true",
                        help="List available streams and sizes, then exit.")
    parser.add_argument("--spikes", action="store_true",
                        help="Plot spike plots (hits per BX) for all shower_hits streams.")
    parser.add_argument("--interactive", action="store_true",
                        help="Save interactive Plotly HTML spike plots, one per station.")
    parser.add_argument("--persist", action="store_true",
                        help="Save Plotly HTML plots with 16-BX hit persistence (cumulative occupancy).")
    parser.add_argument("--persist-bx", type=int, default=16, metavar="N",
                        help="Persistence window in BX for --persist (default: 16).")
    parser.add_argument("--raw", action="store_true",
                        help="Dump raw field values for all streams to TSV files.")
    parser.add_argument("--outdir", default=".",
                        help="Output directory for --interactive / --raw files (default: current dir).")
    args = parser.parse_args()

    if args.list:
        list_streams()
        return

    if args.spikes:
        plot_spike_plots(args.stream)
        return

    if args.interactive:
        plot_interactive_by_station(args.stream, out_dir=args.outdir)
        return

    if args.persist:
        plot_interactive_by_station(args.stream, out_dir=args.outdir)
        plot_persist_by_station(args.stream, out_dir=args.outdir, persist=args.persist_bx)
        return

    if args.raw:
        dump_raw_to_file(args.stream, out_dir=args.outdir)
        return

    # Determine which streams to iterate
    if args.stream:
        matched = {k: v for k, v in dump.streams.items() if args.stream in k}
        if not matched:
            print(f"No stream matching '{args.stream}' found.")
            print("Available streams:")
            list_streams()
            sys.exit(1)
    else:
        matched = dump.streams

    for name, stream in matched.items():
        if len(stream) == 0:
            print(f"\n[{name}] — empty, skipping.")
            continue
        print_stream(name, stream, args.max)


if __name__ == "__main__":
    main()
