#
#            PySceneDetect: Python-Based Video Scene Detector
#   -------------------------------------------------------------------
#     [  Site:    https://scenedetect.com                           ]
#     [  Docs:    https://scenedetect.com/docs/                     ]
#     [  Github:  https://github.com/Breakthrough/PySceneDetect/    ]
#
# Copyright (C) 2026 Brandon Castellano <http://www.bcastell.com>.
# PySceneDetect is licensed under the BSD 3-Clause License; see the
# included LICENSE file, or visit one of the above pages for details.
#
"""Consolidate per-(detector, dataset) sweep JSONs into a single markdown report.

Reads ``benchmark/results/sweep/<detector>-<dataset>.json`` for all five detectors and writes
``benchmark/SWEEP_REPORT.md``: the best cell by F1@1 per (detector, dataset), the top-5
per cell, and the cell that is best *on average* across datasets per detector (a single
recommended default).

All results come from the decode-based sweep (``benchmark/sweep.py``, driven by
``scripts/benchmark_sweep.sh``). Unlike content/adaptive, hash/hist were swept with the default
``min_scene_len`` fixed. If a ``<detector>-ext-<dataset>.json`` grid-extension file exists
alongside the main JSON, its cells are merged in.
"""

from __future__ import annotations

import json
from pathlib import Path

DETECTORS = [
    "detect-content",
    "detect-adaptive",
    "detect-hash",
    "detect-hist",
    "detect-threshold",
]
DATASETS = ["BBC", "AutoShot", "ClipShots"]
SWEEP_DIR = Path("benchmark/results/sweep")
OUT = Path("benchmark/SWEEP_REPORT.md")


def _hard(cell: dict, tol: str) -> dict:
    return cell["results"][tol]["aggregate"]["hard_cuts"]


def _params_str(p: dict) -> str:
    # :g strips float artifacts from grid generation (e.g. 0.42500000000000016 -> 0.425).
    def fmt(v):
        return f"{v:g}" if isinstance(v, float) else str(v)

    return ", ".join(f"{k}={fmt(v)}" for k, v in sorted(p.items()))


def _table(header: list[str], rows: list[list[str]]) -> str:
    widths = [max(len(header[i]), *(len(r[i]) for r in rows)) for i in range(len(header))]

    def line(cells):
        return "| " + " | ".join(c.ljust(w) for c, w in zip(cells, widths, strict=True)) + " |"

    sep = "| " + " | ".join("-" * w for w in widths) + " |"
    return "\n".join([line(header), sep, *(line(r) for r in rows)])


def _load_cells(det: str, ds: str) -> list[dict] | None:
    path = SWEEP_DIR / f"{det}-{ds}.json"
    if not path.exists():
        return None
    cells = json.loads(path.read_text())["cells"]
    ext = SWEEP_DIR / f"{det}-ext-{ds}.json"
    if ext.exists():
        cells = cells + json.loads(ext.read_text())["cells"]
    return cells


def main() -> None:
    out: list[str] = ["# Detector parameter sweep report", ""]
    out.append(
        "Generated by `benchmark/report_sweep.py` from `benchmark/sweep.py` grid results "
        "(hash/hist were swept with `min_scene_len` fixed at its default). "
        "F1/precision/recall are percentages on hard cuts; tol = frame tolerance.\n"
    )

    for det in DETECTORS:
        out.append(f"## {det}\n")
        if det == "detect-threshold":
            out.append(
                "> Note: `detect-threshold` detects **fades** (fade to/from black), not hard "
                "cuts. These datasets' ground truth is hard cuts, so the hard-cut F1 below is "
                "expectedly near zero. It is included to validate the sweep pipeline end-to-end, "
                "not as a meaningful hard-cut accuracy result.\n"
            )
        if det == "detect-hist":
            out.append(
                "> Note: thresholds >= 0.21 come from a grid-extension run "
                "(`detect-hist-ext-<dataset>.json`) after the initial grid's best cell landed "
                "on its 0.20 upper edge.\n"
            )
        # Best-per-dataset summary.
        summary_rows = []
        # Track each cell's F1@1 across datasets for an averaged recommendation.
        per_cell_f1: dict[str, list[float]] = {}
        per_cell_params: dict[str, dict] = {}
        for ds in DATASETS:
            cells = _load_cells(det, ds)
            if cells is None:
                summary_rows.append([ds, "(missing)", "", "", "", ""])
                continue
            best = max(cells, key=lambda c: _hard(c, "1")["f1"])
            h1, h0 = _hard(best, "1"), _hard(best, "0")
            summary_rows.append(
                [
                    ds,
                    f"{h1['f1']:.2f}",
                    f"{h1['precision']:.2f}",
                    f"{h1['recall']:.2f}",
                    f"{h0['f1']:.2f}",
                    _params_str(best["params"]),
                ]
            )
            for c in cells:
                key = _params_str(c["params"])
                per_cell_f1.setdefault(key, []).append(_hard(c, "1")["f1"])
                per_cell_params[key] = c["params"]
        out.append("**Best cell per dataset (by F1@1):**\n")
        out.append(
            _table(
                ["Dataset", "F1@1", "Prec@1", "Rec@1", "F1@0", "Params"],
                summary_rows,
            )
        )
        out.append("")
        # Averaged recommendation: cells scored on all datasets, ranked by mean F1@1.
        full = {k: v for k, v in per_cell_f1.items() if len(v) == len(DATASETS)}
        if full:
            ranked = sorted(full.items(), key=lambda kv: sum(kv[1]) / len(kv[1]), reverse=True)
            rec_rows = [
                [f"{sum(v) / len(v):.2f}", *(f"{x:.2f}" for x in v), k] for k, v in ranked[:5]
            ]
            out.append("**Best params averaged across all datasets (mean F1@1):**\n")
            out.append(_table(["Mean F1@1", *DATASETS, "Params"], rec_rows))
            out.append("")

    OUT.write_text("\n".join(out), encoding="utf-8")
    print(f"Wrote {OUT}")
    print("\n".join(out))


if __name__ == "__main__":
    main()
