"""
analyze_timing.py
Loads BarTimingProbe CSV results, computes per-symbol bar-synchronisation lag
statistics across all four modelling modes, generates plots, and writes a report.

Key metric: lagged_pct
  = % of primary new-bar events where the secondary bar had NOT yet advanced.
  Combines two sub-cases:
    lag > 0   : secondary caught up N ticks later (within the tracking window)
    lag = -1  : secondary never caught up within the window (window-exceeded)

  In Open Prices Only mode, MT5 fires exactly 1 tick per bar. "window-exceeded"
  therefore means the secondary bar only advances on the NEXT primary tick, i.e.
  it is permanently 1 full bar (= 15 minutes) behind for every affected bar.

Usage:
    python analyze_timing.py [results_dir]
    Default results_dir: ../results  (relative to this script)
"""

import sys
from pathlib import Path

import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

RESULTS_DIR = (
    Path(sys.argv[1]) if len(sys.argv) > 1
    else Path(__file__).parent.parent / "results"
)

# ── Model configuration ────────────────────────────────────────────────────────

# Labels match InpRunLabel in INI files and output CSV filenames.
# Tick counts confirmed from tester logs (EURUSD M15, 2025-05-08 → 2026-05-08, 24 751 bars):
#   et  Model=0  ~757 ticks/bar   Every Tick (synthesized from M1 OHLC)
#   rt  Model=4  varies/bar       Every Tick Based on Real Ticks (fallback to M=0 if unavailable)
#   m1  Model=1  ~60  ticks/bar   1-Min OHLC  (15 M1 bars × 4 OHLC points)
#   op  Model=2  1    tick/bar    Open Prices Only
MODEL_META = {
    "et": "Every Tick (Model=0)",
    "rt": "Real Ticks (Model=4)",
    "m1": "1-Min OHLC (Model=1)",
    "op": "Open Prices Only (Model=2)",
}
MODEL_SHORT = {           # compact labels for chart axes
    "et": "Every Tick\n(M=0)",
    "rt": "Real Ticks\n(M=4)",
    "m1": "1-Min OHLC\n(M=1)",
    "op": "Open Prices\nOnly (M=2)",
}
MODEL_ORDER   = ["et", "rt", "m1", "op"]
MODEL_COLOURS = {
    "et": "#1f77b4",   # blue
    "rt": "#9467bd",   # purple
    "m1": "#ff7f0e",   # orange
    "op": "#d62728",   # red
}

SECONDARY_SYMBOLS = ["GBPUSD", "USDCHF", "USDCAD", "GBPJPY", "EURJPY", "EURAUD"]
EUR_PAIRS         = {"EURJPY", "EURAUD"}

# ── Data loading ───────────────────────────────────────────────────────────────

def find_csv_files(results_dir: Path) -> dict[str, Path]:
    found = {}
    for key in MODEL_ORDER:
        matches = list(results_dir.glob(f"btp_EURUSD_PERIOD_M15_{key}.csv"))
        if matches:
            found[key] = matches[0]
    return found


def load_model(path: Path) -> pd.DataFrame:
    df = pd.read_csv(path)
    df["primary_bar_time"] = pd.to_datetime(df["primary_bar_time"], format="%Y.%m.%d %H:%M:%S")
    for sym in SECONDARY_SYMBOLS:
        if f"{sym}_bar_time" in df.columns:
            df[f"{sym}_bar_time"] = pd.to_datetime(
                df[f"{sym}_bar_time"], format="%Y.%m.%d %H:%M:%S"
            )
        if f"{sym}_lag" in df.columns:
            df[f"{sym}_lag"] = df[f"{sym}_lag"].astype(int)
    return df


def extract_bar_lags(df: pd.DataFrame) -> pd.DataFrame:
    """
    Return one row per bar event — the last row of each tracking window.
    Lag values: 0 = immediate,  N > 0 = N ticks late,  -1 = window-exceeded.
    """
    return (
        df.sort_values("window_tick")
          .groupby("primary_bar_time")
          .last()
          .reset_index()
    )


def sym_stats(bar_lags: pd.DataFrame, sym: str) -> dict:
    col = f"{sym}_lag"
    if col not in bar_lags.columns:
        return {}

    s        = bar_lags[col]
    total    = len(s)
    n_zero   = int((s == 0).sum())
    n_pos    = int((s > 0).sum())
    n_miss   = int((s == -1).sum())   # window-exceeded  = secondary never caught up
    n_lagged = n_pos + n_miss          # any lag: within-window late OR window-exceeded

    pos_vals = s[s > 0]
    return {
        "total_bars":   total,
        "lag_0_pct":    n_zero   / total * 100 if total else 0,
        "lag_pos_pct":  n_pos    / total * 100 if total else 0,   # within-window lag
        "lag_miss_pct": n_miss   / total * 100 if total else 0,   # window-exceeded
        "lagged_pct":   n_lagged / total * 100 if total else 0,   # PRIMARY metric
        "mean_lag":     float(pos_vals.mean())   if len(pos_vals) else 0.0,
        "median_lag":   float(pos_vals.median()) if len(pos_vals) else 0.0,
        "max_lag":      int(pos_vals.max())       if len(pos_vals) else 0,
        "lag_series":   s,
    }


# ── Plot 1: Heatmap ────────────────────────────────────────────────────────────

def plot_heatmap(all_stats: dict, out_path: Path):
    """
    Rows = secondary symbols, columns = modelling modes.
    Cell value = lagged_pct (the unified 'any lag' metric).
    """
    models = [k for k in MODEL_ORDER if k in all_stats]
    data   = np.zeros((len(SECONDARY_SYMBOLS), len(models)))

    for ci, key in enumerate(models):
        for ri, sym in enumerate(SECONDARY_SYMBOLS):
            st = all_stats.get(key, {}).get(sym, {})
            data[ri, ci] = st.get("lagged_pct", 0.0)

    fig, ax = plt.subplots(figsize=(len(models) * 2.4 + 0.8, len(SECONDARY_SYMBOLS) * 0.95 + 2.0))
    im   = ax.imshow(data, cmap="RdYlGn_r", vmin=0, vmax=100, aspect="auto")
    cbar = plt.colorbar(im, ax=ax)
    cbar.set_label("% of M15 bars where secondary had NOT yet advanced", fontsize=9)

    ax.set_xticks(range(len(models)))
    ax.set_xticklabels([MODEL_META[k] for k in models], rotation=15, ha="right", fontsize=9)
    ax.set_yticks(range(len(SECONDARY_SYMBOLS)))
    ax.set_yticklabels(SECONDARY_SYMBOLS, fontsize=11)

    ax.set_title(
        "MT5 Bar Sync Lag — % of M15 bars where secondary iTime() had NOT yet advanced\n"
        "when the primary (EURUSD) new-bar OnTick fired\n"
        "Lag includes: within-window ticks-late (lag>0)  +  window-exceeded (≡ 1-bar late in Open Prices Only mode)",
        fontsize=9, pad=10
    )

    for ri in range(len(SECONDARY_SYMBOLS)):
        for ci in range(len(models)):
            v = data[ri, ci]
            ax.text(ci, ri, f"{v:.1f}%",
                    ha="center", va="center", fontsize=10, fontweight="bold",
                    color="white" if v > 60 else "black")

    plt.tight_layout()
    fig.savefig(out_path, dpi=150)
    plt.close(fig)
    print(f"  Heatmap saved:       {out_path.name}")


# ── Plot 2: Overview grouped bar chart ────────────────────────────────────────

def plot_overview(all_stats: dict, out_path: Path):
    """
    One bar group per secondary symbol; bars within each group are coloured by model.
    Y-axis = lagged_pct.  Non-EUR / EUR zones are shaded.
    """
    models = [k for k in MODEL_ORDER if k in all_stats]
    n_sym  = len(SECONDARY_SYMBOLS)
    n_mod  = len(models)

    fig, ax = plt.subplots(figsize=(13, 5))
    x     = np.arange(n_sym)
    width = 0.72 / n_mod

    for i, key in enumerate(models):
        vals   = [all_stats.get(key, {}).get(sym, {}).get("lagged_pct", 0.0)
                  for sym in SECONDARY_SYMBOLS]
        offset = (i - (n_mod - 1) / 2) * width
        bars   = ax.bar(x + offset, vals, width * 0.90,
                        label=MODEL_META[key], color=MODEL_COLOURS[key],
                        alpha=0.85, edgecolor="white", linewidth=0.5)
        for bar, v in zip(bars, vals):
            if v > 3:
                ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 1.2,
                        f"{v:.0f}%", ha="center", va="bottom",
                        fontsize=6.5, rotation=90, color=MODEL_COLOURS[key])

    # Background zones
    ax.axvspan(-0.5, 3.5, alpha=0.04, color="#cc0000")
    ax.axvspan(3.5,  5.5, alpha=0.04, color="#006600")
    ax.text(1.5, 108, "non-EUR pairs", ha="center", fontsize=9,
            color="#990000", fontstyle="italic")
    ax.text(4.5, 108, "EUR pairs",    ha="center", fontsize=9,
            color="#004400", fontstyle="italic")

    ax.set_xticks(x)
    ax.set_xticklabels(SECONDARY_SYMBOLS, fontsize=11)
    ax.set_ylabel("% of M15 bars where secondary had NOT yet advanced", fontsize=9)
    ax.set_ylim(0, 118)
    ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f"{v:.0f}%"))
    ax.axhline(100, color="gray", linestyle="--", alpha=0.35, linewidth=0.9)
    ax.legend(fontsize=9, loc="upper right")
    ax.set_title(
        "MT5 Bar Sync Lag by Symbol and Modelling Mode\n"
        "100% = secondary bar always lagging behind primary  |  0% = always in sync",
        fontsize=10
    )

    plt.tight_layout()
    fig.savefig(out_path, dpi=150)
    plt.close(fig)
    print(f"  Overview chart saved: {out_path.name}")


# ── Plot 3: Within-window tick distributions (tick models only) ───────────────

def plot_distributions(all_stats: dict, out_path: Path):
    """
    Grouped histograms showing how many ticks it takes for each secondary to catch up.
    Only tick-based models are shown (et, rt, m1).
    Open Prices Only is excluded — non-EUR pairs always exceed the window (1-bar late),
    EUR pairs always sync immediately; there is no meaningful within-window distribution.
    """
    tick_models = [k for k in ["et", "rt", "m1"] if k in all_stats]
    if not tick_models:
        print("  No tick-based models found; skipping distribution plot.")
        return

    MAX_SHOW = 6   # show buckets 0, 1, 2, 3, 4, 5, then "6+"
    xtick_labels = [str(v) for v in range(MAX_SHOW)] + [f"{MAX_SHOW}+"]
    xtick_pos    = list(range(MAX_SHOW + 1))

    ncols = 3
    nrows = (len(SECONDARY_SYMBOLS) + ncols - 1) // ncols   # = 2 for 6 symbols
    n_mod = len(tick_models)
    bw    = 0.72 / n_mod

    fig, axes = plt.subplots(nrows, ncols, figsize=(14, 4.8 * nrows), sharey=False)
    axes = axes.flatten()

    for ax, sym in zip(axes, SECONDARY_SYMBOLS):
        total = None
        for i, key in enumerate(tick_models):
            st = all_stats.get(key, {}).get(sym, {})
            if not st:
                continue
            s = st["lag_series"]
            if total is None:
                total = st["total_bars"]

            counts = [float((s == v).sum()) for v in range(MAX_SHOW)]
            counts.append(float((s >= MAX_SHOW).sum()))        # 6+ bucket
            pcts   = [c / total * 100 for c in counts]

            offset = (i - (n_mod - 1) / 2) * bw
            ax.bar([x + offset for x in xtick_pos], pcts, bw * 0.88,
                   color=MODEL_COLOURS[key], label=MODEL_META[key],
                   alpha=0.82, edgecolor="white", linewidth=0.4)

        ax.set_title(sym, fontsize=11, fontweight="bold")
        ax.set_xticks(xtick_pos)
        ax.set_xticklabels(xtick_labels, fontsize=8)
        ax.set_ylabel("% of all M15 bars", fontsize=8)
        ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda v, _: f"{v:.0f}%"))
        ax.legend(fontsize=7, loc="upper right")

    for ax in axes[len(SECONDARY_SYMBOLS):]:
        ax.set_visible(False)

    fig.text(
        0.5, -0.01,
        "Ticks after primary new-bar event until secondary iTime() advanced   "
        "(0 = same tick as primary = no lag)",
        ha="center", fontsize=9
    )
    fig.suptitle(
        "Within-window tick lag distribution — Every Tick / Real Ticks / 1-Min OHLC\n"
        "Note: Open Prices Only not shown — non-EUR pairs always miss the single-tick window "
        "(= 1 bar late);  EUR pairs always sync immediately (lag = 0)",
        fontsize=9, y=1.01
    )

    plt.tight_layout()
    fig.savefig(out_path, dpi=150, bbox_inches="tight")
    plt.close(fig)
    print(f"  Distribution plot saved: {out_path.name}")


# ── Report ─────────────────────────────────────────────────────────────────────

def verdict(all_stats: dict) -> str:
    models_present = [k for k in MODEL_ORDER if k in all_stats]
    EUR = {"EURJPY", "EURAUD"}

    noneur_max_lagged  = 0.0
    eur_max_lagged     = 0.0
    op_miss: dict[str, float] = {}

    for key, sym_dict in all_stats.items():
        for sym, st in sym_dict.items():
            if not st:
                continue
            p = st["lagged_pct"]
            if sym not in EUR:
                noneur_max_lagged = max(noneur_max_lagged, p)
                if key == "op":
                    op_miss[sym] = st["lag_miss_pct"]
            else:
                eur_max_lagged = max(eur_max_lagged, p)

    if noneur_max_lagged < 5:
        return (
            "No significant lag detected. The reported entry delays were caused by "
            "something other than `iTime()` staleness."
        )

    paras = []

    paras.append(
        f"**Confirmed:** non-EUR off-chart symbols (GBPUSD, USDCHF, USDCAD, GBPJPY) "
        f"show bar sync lag on virtually every M15 bar — up to **{noneur_max_lagged:.1f}%** of bars "
        "across all tested modelling modes."
    )

    paras.append(
        "In **Every Tick, Real Ticks, and 1-Min OHLC** modes the lag is almost always "
        "**exactly 1 tick**. The secondary bar becomes visible one tick after the primary "
        "fires. Signal logic reading `iTime(secondary, tf, 0)` inside `OnTick` will see "
        "the previous bar's timestamp on that one tick, causing a brief 1-tick data "
        "misalignment. In high-resolution models this resolves within the same M15 bar "
        "and has a minor practical impact."
    )

    if op_miss:
        avg_miss = sum(op_miss.values()) / len(op_miss)
        paras.append(
            f"In **Open Prices Only** mode the impact is fundamentally different. "
            f"MT5 generates exactly **1 tick per M15 bar**. When the primary (EURUSD) "
            f"bar fires, non-EUR secondary bars have not yet advanced — and since there "
            f"are no further ticks in this bar period, they **never** catch up within "
            f"the tracking window ({avg_miss:.0f}% of bars show window-exceeded). "
            "The secondary bar only becomes current on the **next** primary tick "
            "— i.e. the next M15 bar, 15 minutes later. "
            "Any signal that reads off-chart `iTime()` or `iOpen/iClose` on the "
            "primary new-bar event will silently operate on stale data, producing "
            "a systematic **+1-bar (15-minute) entry delay** for all non-EUR pairs."
        )

    if eur_max_lagged < 5:
        paras.append(
            f"**EUR cross pairs** (EURJPY, EURAUD) are nearly unaffected "
            f"(max {eur_max_lagged:.1f}% of bars). MT5 appears to advance EUR-denominated "
            "bar times concurrently with EURUSD in most cases, likely because they share "
            "the same exchange-session clock."
        )

    paras.append(
        "**Recommended architecture:** run each symbol in its own independent MT5 "
        "backtest — where it is the primary symbol — and merge the resulting trade "
        "schedules externally. This eliminates the off-chart bar sync dependency entirely. "
        "A gate of the form `if (iTime(sym, tf, 0) == expected_bar) { ... }` reduces "
        "stale reads in tick modes but does not help in Open Prices Only mode where the "
        "secondary bar never advances within the bar period."
    )

    return "\n\n".join(paras)


def generate_report(all_stats: dict, csv_files: dict) -> str:
    models_present = [k for k in MODEL_ORDER if k in all_stats]

    lines = [
        "# MT5 Multi-Symbol Bar Timing Probe — Results Report",
        "",
        "**Study:** Cross-symbol bar synchronisation in the MT5 Strategy Tester  ",
        "**Primary symbol:** EURUSD M15  ",
        "**Secondary symbols:** GBPUSD, USDCHF, USDCAD, GBPJPY, EURJPY, EURAUD  ",
        "**Period:** 2025-05-08 to 2026-05-08  ",
        "",
        "---",
        "",
        "## Background",
        "",
        "A portfolio EA running multiple symbols inside a single MT5 backtest showed",
        "systematic +15-minute entry delays on GBPUSD, USDCHF, USDCAD and GBPJPY,",
        "while EURUSD, EURJPY and EURAUD entered on time. When each symbol was run in",
        "its own independent backtest the timing was correct.",
        "",
        "This probe quantifies whether the MT5 tester delivers a stale `iTime()` value",
        "for off-chart symbols at the moment the primary-symbol `OnTick` fires.",
        "",
        "---",
        "",
        "## Measurement method",
        "",
        "A minimal EA (`BarTimingProbe.mq5`) runs on EURUSD M15 as the primary symbol.",
        "On each primary new-bar event it opens a **tracking window** (up to 50 ticks).",
        "Each tick it polls `iTime(secondary, PERIOD_M15, 0)` for all six secondary",
        "symbols and records on which tick — if any — each one reports the new bar time.",
        "",
        "| Lag value | Meaning |",
        "|-----------|---------|",
        "| `0` | Secondary bar visible on the **same tick** as primary (no lag) |",
        "| `N > 0` | Secondary bar appeared **N ticks** after the primary new-bar tick |",
        "| `-1` (window-exceeded) | Secondary bar did **not** advance within 50 ticks |",
        "",
        "In **Open Prices Only** mode (1 tick per bar), `-1` means the secondary bar",
        "will only advance on the **next** primary bar tick — a full **+1 bar = +15 min** delay.",
        "",
        "## Key metric — Lagged (%)",
        "",
        "**Lagged** = % of primary new-bar events where the secondary bar had NOT yet",
        "advanced. This combines within-window lag (`lag > 0`) and window-exceeded (`lag = -1`),",
        "giving a single number that answers: *how often does signal logic see stale bar data?*",
        "",
        "---",
        "",
        "## Summary table — Lagged (%) per symbol per model",
        "",
        "| Symbol | " + " | ".join(MODEL_META[k] for k in models_present) + " |",
        "|" + "|".join(["---------"] * (1 + len(models_present))) + "|",
    ]

    for sym in SECONDARY_SYMBOLS:
        row = f"| {sym} |"
        for k in models_present:
            st  = all_stats.get(k, {}).get(sym, {})
            pct = st.get("lagged_pct")
            row += f" {'N/A' if pct is None else f'**{pct:.1f}%**'} |"
        lines.append(row)

    lines += [
        "",
        "*Lagged = secondary bar had not yet advanced (lag > 0, or window-exceeded).*  ",
        "*In Open Prices Only, window-exceeded ≡ 1 full bar (15 min) behind the primary.*",
        "",
        "---",
        "",
        "## Per-model detail",
        "",
    ]

    for k in models_present:
        lines.append(f"### {MODEL_META[k]}  (`{k}`)")
        lines.append("")
        if k == "op":
            lines.append(
                "> **Open Prices Only note:** MT5 generates exactly 1 tick per bar. "
                "`window-exceeded` (lag = -1) means the secondary bar did **not** advance "
                "on the same tick as the primary. Since there are no further ticks in this "
                "bar period the secondary will only update on the next M15 bar — "
                "**+15 minutes of signal delay**."
            )
            lines.append("")
        lines.append(
            "| Symbol | Bars | Immediate (lag=0) | **Lagged total** | "
            "↳ within-window (lag>0) | ↳ window-exceeded (lag=−1) | "
            "Mean ticks¹ | Max ticks¹ |"
        )
        lines.append("|--------|------|-------------------|------------------|------------------------|---------------------------|-------------|------------|")
        for sym in SECONDARY_SYMBOLS:
            st = all_stats.get(k, {}).get(sym, {})
            if not st:
                lines.append(f"| {sym} | — | — | — | — | — | — | — |")
                continue
            lines.append(
                f"| {sym} "
                f"| {st['total_bars']:,} "
                f"| {st['lag_0_pct']:.1f}% "
                f"| **{st['lagged_pct']:.1f}%** "
                f"| {st['lag_pos_pct']:.1f}% "
                f"| {st['lag_miss_pct']:.1f}% "
                f"| {st['mean_lag']:.2f} "
                f"| {st['max_lag']} |"
            )
        lines.append("")
        lines.append("*¹ Mean/max ticks counted only among bars where lag > 0 (within-window). "
                     "Window-exceeded bars are excluded from tick statistics.*")
        lines.append("")

    lines += [
        "---",
        "",
        "## Verdict",
        "",
        verdict(all_stats),
        "",
        "---",
        "",
        "## Implications for portfolio EA design",
        "",
        "| Approach | Assessment |",
        "|----------|------------|",
        "| Multi-symbol live signals in one EA, reading `iTime()` off-chart | **Use with caution** — secondary bars lag by 1 tick (~99.7% of M15 bars for non-EUR pairs); in Open Prices Only this becomes a full +15-minute delay |",
        "| Gate off-chart reads on `iTime(sym, tf, 0) == expected_bar` | **Partial mitigation** — eliminates stale reads in tick models, but unhelpful in Open Prices Only where the secondary bar never advances mid-bar |",
        "| Independent per-symbol MT5 runs + externally merged schedule | **Recommended** — each backtest uses its own symbol as primary; zero off-chart dependency |",
        "",
        "---",
        "",
        "## Charts",
        "",
        "| File | Description |",
        "|------|-------------|",
        "| `lag_heatmap.png` | Colour-coded grid: lagged % at a glance (symbols × models) |",
        "| `lag_overview.png` | Grouped bar chart comparing all symbols and models |",
        "| `lag_distributions.png` | Tick-level histogram for tick-based models (et / rt / m1) |",
        "",
        "---",
        "",
        "## Data files",
        "",
    ]
    for k, p in csv_files.items():
        rows = sum(1 for _ in open(p, encoding="utf-8", errors="ignore")) - 1
        lines.append(f"- `{p.name}` — {rows:,} rows — {MODEL_META[k]}")

    return "\n".join(lines)


# ── Main ───────────────────────────────────────────────────────────────────────

def main():
    print(f"Results dir: {RESULTS_DIR}")
    csv_files = find_csv_files(RESULTS_DIR)

    if not csv_files:
        print("No CSV files found. Run run_backtests.py first.")
        print(f"Looking for: btp_EURUSD_PERIOD_M15_<label>.csv  in  {RESULTS_DIR}")
        sys.exit(1)

    found_keys = [k for k in MODEL_ORDER if k in csv_files]
    missing    = [k for k in MODEL_ORDER if k not in csv_files]
    print(f"Found:   {found_keys}")
    if missing:
        print(f"Missing: {missing}  (run run_backtests.py to generate)")

    all_stats: dict[str, dict[str, dict]] = {}

    for key in found_keys:
        path = csv_files[key]
        print(f"\n[{MODEL_META[key]}]  {path.name}")
        df       = load_model(path)
        bar_lags = extract_bar_lags(df)
        print(f"  {len(bar_lags):,} bar events")

        sym_dict = {}
        for sym in SECONDARY_SYMBOLS:
            st = sym_stats(bar_lags, sym)
            if not st:
                continue
            sym_dict[sym] = st
            # Annotate op mode window-exceeded clearly
            note = ""
            if key == "op" and st["lag_miss_pct"] > 50:
                note = "  ← always 1-bar late (window has only 1 tick in this mode)"
            print(
                f"  {sym}: lagged={st['lagged_pct']:.1f}%"
                f"  (within-window={st['lag_pos_pct']:.1f}%"
                f"  mean={st['mean_lag']:.2f} ticks"
                f"  window-exceeded={st['lag_miss_pct']:.1f}%)"
                + note
            )
        all_stats[key] = sym_dict

    print()
    plot_heatmap(all_stats,       RESULTS_DIR / "lag_heatmap.png")
    plot_overview(all_stats,      RESULTS_DIR / "lag_overview.png")
    plot_distributions(all_stats, RESULTS_DIR / "lag_distributions.png")

    report_text = generate_report(all_stats, csv_files)
    report_path = RESULTS_DIR / "report.md"
    report_path.write_text(report_text, encoding="utf-8")
    print(f"  Report saved:        {report_path.name}")
    print("\nDone.")


if __name__ == "__main__":
    main()
