#!/usr/bin/env python3
"""Scan an entire video and rank short windows using FFmpeg and Pillow only."""
from __future__ import annotations

import argparse
import json
import math
import subprocess
import tempfile
from pathlib import Path

from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageStat


def probe(video: Path) -> tuple[float, float]:
    data = json.loads(subprocess.check_output([
        "ffprobe", "-v", "error", "-select_streams", "v:0",
        "-show_entries", "stream=avg_frame_rate:format=duration", "-of", "json", str(video),
    ]))
    duration = float(data["format"]["duration"])
    numerator, denominator = data["streams"][0]["avg_frame_rate"].split("/")
    return duration, float(numerator) / max(float(denominator), 1.0)


def extract(video: Path, second: float, destination: Path) -> Image.Image:
    subprocess.run([
        "ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-ss", f"{second:.4f}",
        "-i", str(video), "-frames:v", "1", "-vf", "scale=480:-2", str(destination),
    ], check=True)
    return Image.open(destination).convert("RGB")


def score_pair(first: Image.Image, second: Image.Image) -> tuple[float, float, float]:
    gray_first = first.convert("L")
    gray_second = second.convert("L")
    motion = float(ImageStat.Stat(ImageChops.difference(gray_first, gray_second)).mean[0])
    edges = gray_second.filter(ImageFilter.FIND_EDGES)
    sharpness = float(ImageStat.Stat(edges).var[0])
    luminance = float(ImageStat.Stat(gray_second).mean[0])
    exposure = max(0.0, 1.0 - abs(luminance - 118.0) / 118.0)
    return motion, sharpness, exposure


def normalize(values: list[float]) -> list[float]:
    if not values:
        return []
    low, high = min(values), max(values)
    if high - low < 1e-9:
        return [0.5] * len(values)
    return [(value - low) / (high - low) for value in values]


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("video")
    parser.add_argument("out_dir")
    parser.add_argument("--windows", type=int, default=48)
    parser.add_argument("--window-seconds", type=float, default=2.0)
    parser.add_argument("--top", type=int, default=12)
    args = parser.parse_args()
    video = Path(args.video)
    out = Path(args.out_dir)
    out.mkdir(parents=True, exist_ok=True)
    duration, fps = probe(video)
    first_center = args.window_seconds / 2
    last_center = max(first_center, duration - args.window_seconds / 2)
    centers = [first_center + (last_center - first_center) * index / max(args.windows - 1, 1) for index in range(args.windows)]
    rows = []
    thumbs = []
    with tempfile.TemporaryDirectory() as temp_dir:
        temp = Path(temp_dir)
        for index, center in enumerate(centers):
            first = extract(video, max(0.0, center - args.window_seconds / 2), temp / f"{index:03d}_a.jpg")
            middle = extract(video, center, temp / f"{index:03d}_m.jpg")
            last = extract(video, min(duration - 0.04, center + args.window_seconds / 2), temp / f"{index:03d}_b.jpg")
            motion, sharpness, exposure = score_pair(first, last)
            rows.append({
                "center": center, "start": max(0.0, center - args.window_seconds / 2),
                "end": min(duration, center + args.window_seconds / 2),
                "motion_raw": motion, "sharpness_raw": sharpness, "exposure": exposure,
            })
            thumbs.append(middle.copy())
    motions = normalize([row["motion_raw"] for row in rows])
    sharpness = normalize([row["sharpness_raw"] for row in rows])
    for row, motion, sharp in zip(rows, motions, sharpness):
        row["score"] = round(0.52 * motion + 0.33 * sharp + 0.15 * row["exposure"], 4)
        for key in ("center", "start", "end", "motion_raw", "sharpness_raw", "exposure"):
            row[key] = round(float(row[key]), 4)
    ranked = sorted(rows, key=lambda row: row["score"], reverse=True)
    payload = {
        "video": str(video.resolve()), "duration": round(duration, 4), "fps": round(fps, 4),
        "sampling_windows": len(rows), "top_windows": ranked[:args.top], "all_windows": rows,
        "note": "Heuristic only: visual review of the global sheet and every chosen window remains mandatory.",
    }
    analysis_path = out / "window_analysis.json"
    analysis_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    cols, cell_w, cell_h = 4, 320, 210
    sheet = Image.new("RGB", (cols * cell_w, math.ceil(len(thumbs) / cols) * cell_h), (13, 13, 17))
    draw = ImageDraw.Draw(sheet)
    for index, (image, row) in enumerate(zip(thumbs, rows)):
        image.thumbnail((cell_w, 180))
        x = (index % cols) * cell_w
        y = (index // cols) * cell_h
        sheet.paste(image, (x, y))
        draw.text((x + 8, y + 184), f"{row['center']:.1f}s  score {row['score']:.2f}", fill="white")
    sheet_path = out / "global_contact_sheet.jpg"
    sheet.save(sheet_path, quality=92)
    print(json.dumps({
        "analysis": str(analysis_path), "sheet": str(sheet_path),
        "top": [{"start": row["start"], "end": row["end"], "score": row["score"]} for row in ranked[:args.top]],
    }))


if __name__ == "__main__":
    main()
