#!/usr/bin/env python3
"""scene_gap_hist.py — histogram of the gap (seconds) between an actor's consecutive scenes.

Scans scene_analyze output JSONs and, for every actor in every file, computes
next_scene_start - prev_scene_end between consecutive scenes. Prints a text
histogram plus summary percentiles — useful for picking an --anneal value
(gaps below anneal get merged into one window).

Note: each input was produced with some anneal_sec already, so gaps smaller
than that file's anneal were merged away before they reached this script. The
distribution here is therefore of the *surviving* gaps; run with --by-anneal to
see which anneal settings your files used.

Usage:
    python scripts/scene_gap_hist.py *.json
    python scripts/scene_gap_hist.py --bin 2 --max 60 *.json
    python scripts/scene_gap_hist.py --by-anneal *.json
"""

import argparse
import json
import sys
from collections import Counter
from pathlib import Path


def iter_gaps(path: Path):
    """Yield each within-actor gap (seconds) from one output JSON."""
    try:
        data = json.loads(path.read_text())
    except (OSError, ValueError) as e:
        print(f"  [skip] {path}: {e}", file=sys.stderr)
        return
    if not isinstance(data, dict) or "actors" not in data:
        return  # not a scene_analyze output (e.g. gallery.missing_images.json)
    for actor in data.get("actors", []):
        scenes = actor.get("scenes", [])
        # Scenes are [start, end]; sort defensively in case they aren't ordered.
        scenes = sorted((s for s in scenes if len(s) == 2), key=lambda s: s[0])
        for (prev, cur) in zip(scenes, scenes[1:]):
            gap = cur[0] - prev[1]
            if gap >= 0:  # negative would mean overlapping windows; ignore
                yield gap


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("files", nargs="+", help="scene_analyze output JSON files")
    ap.add_argument("--bin", type=float, default=1.0, help="histogram bin width in seconds (default: 1)")
    ap.add_argument("--max", type=float, default=60.0,
                    help="gaps >= this go in a final overflow bucket (default: 60)")
    ap.add_argument("--width", type=int, default=60, help="max bar width in chars (default: 60)")
    ap.add_argument("--by-anneal", action="store_true",
                    help="also report how many files used each anneal_sec value")
    args = ap.parse_args()

    paths = [Path(f) for f in args.files]

    if args.by_anneal:
        anneal = Counter()
        for p in paths:
            try:
                d = json.loads(p.read_text())
            except (OSError, ValueError):
                continue
            if isinstance(d, dict) and "actors" in d:
                anneal[d.get("anneal_sec")] += 1
        print("anneal_sec used across files:")
        for val, n in sorted(anneal.items(), key=lambda kv: (kv[0] is None, kv[0])):
            print(f"  {val}: {n} file(s)")
        print()

    gaps = [g for p in paths for g in iter_gaps(p)]
    if not gaps:
        sys.exit("No gaps found (need actors with 2+ scenes).")

    bins = Counter()
    for g in gaps:
        if g >= args.max:
            bins["overflow"] += 1
        else:
            bins[int(g // args.bin)] += 1

    peak = max(bins.values())
    n = len(gaps)
    print(f"{n} gaps across {len(paths)} file(s), bin={args.bin}s\n")
    n_bins = int(args.max // args.bin)
    for b in range(n_bins):
        lo = b * args.bin
        hi = lo + args.bin
        c = bins.get(b, 0)
        bar = "#" * round(c / peak * args.width)
        print(f"{lo:6.1f}–{hi:<6.1f} {c:6d} {c/n*100:5.1f}% {bar}")
    ov = bins.get("overflow", 0)
    if ov:
        bar = "#" * round(ov / peak * args.width)
        print(f">={args.max:<11.1f} {ov:6d} {ov/n*100:5.1f}% {bar}")

    gaps.sort()
    def pct(p):
        return gaps[min(len(gaps) - 1, int(p / 100 * len(gaps)))]
    print(f"\nmin={gaps[0]:.1f}  p50={pct(50):.1f}  p75={pct(75):.1f}  "
          f"p90={pct(90):.1f}  p95={pct(95):.1f}  p99={pct(99):.1f}  max={gaps[-1]:.1f}")


if __name__ == "__main__":
    main()
