"""Clip-cutting utilities and match derivation.

The product never cuts clips automatically — GSI events exist to timestamp
the VOD timeline (markers, chapters, match bands). Cutting happens when the
user asks for a range: `cut_range` does a fast stream-copy with a re-encode
fallback, and every cut is verified with a full null-decode before it's
accepted (stream-copy starts can land on a broken reference frame).
"""
import json
import os
import re
import subprocess
import time

VOD_SETTLE_S = 300          # vod file must be untouched this long — still-recording guard
MATCH_GAP_MS = 12 * 60000   # same-map events this far apart start a new match


def sanitize(s):
    return re.sub(r"[^a-z0-9_-]", "", str(s or "unknown").lower()) or "unknown"


def event_map(row):
    try:
        return sanitize(json.loads(row["detail"] or "{}").get("map"))
    except Exception:
        return "unknown"


def night_date(vod_start_epoch):
    return time.strftime("%Y-%m-%d", time.localtime(vod_start_epoch))


def vod_is_settled(vod_path):
    try:
        return time.time() - os.path.getmtime(vod_path) > VOD_SETTLE_S
    except OSError:
        return False


PROXY_V_NVENC = ["-c:v", "h264_nvenc", "-preset", "p4", "-rc", "vbr",
                 "-b:v", "2500k", "-maxrate", "4M", "-bufsize", "8M"]
PROXY_V_X264 = ["-c:v", "libx264", "-preset", "veryfast", "-crf", "26",
                "-maxrate", "4M", "-bufsize", "8M"]


def make_proxy(path):
    """720p ~2.5 Mbps `<name>_proxy.mp4` beside the original. Preview players
    stream these — 27 Mbps OBS-bitrate originals starve remote playback. All
    library listers key on strict filename patterns, so proxies never show up
    as content."""
    out = re.sub(r"\.mp4$", "_proxy.mp4", path)
    if os.path.exists(out):
        return out
    tmp = out + f".tmp-{os.getpid()}.mp4"
    for codec in (PROXY_V_NVENC, PROXY_V_X264):
        try:
            subprocess.run(
                ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", path,
                 "-map", "0:v:0", "-map", "0:a:0", "-vf", "scale=-2:720",
                 *codec, "-c:a", "aac", "-b:a", "96k", "-ac", "2",
                 "-movflags", "+faststart", tmp],
                check=True, timeout=300, capture_output=True)
            os.replace(tmp, out)
            return out
        except Exception:
            if os.path.exists(tmp):
                os.remove(tmp)
    return None


def decodes_clean(path, timeout=90):
    """Full decode with zero errors — catches the deterministic mid-GOP
    stream-copy cuts that start on a broken reference frame."""
    try:
        r = subprocess.run(
            ["ffmpeg", "-hide_banner", "-v", "error",
             "-i", path, "-f", "null", "-"],
            capture_output=True, text=True, timeout=timeout, encoding="utf-8", errors="replace")
        return r.returncode == 0 and not r.stderr.strip()
    except Exception:
        return False


def cut_range(vod_path, start_s, end_s, out_path) -> bool:
    """Cut [start_s, end_s) of a VOD to out_path.

    Attempt 1 is a stream copy (fast, keyframe-snapped: the start can land
    ~a GOP early, which just means a hair more context). Attempt 2 re-encodes,
    which rescues both a glitched writeback and the deterministic mid-GOP cuts
    where -c copy starts on a broken reference frame."""
    if end_s <= start_s or not os.path.exists(vod_path):
        return False
    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    tmp = out_path + ".tmp.mp4"
    for vcodec in (["-c", "copy"],
                   ["-c:v", "libx264", "-preset", "veryfast", "-crf", "17",
                    "-c:a", "copy"]):
        try:
            subprocess.run(
                ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
                 "-ss", f"{start_s:.3f}", "-i", vod_path, "-t", f"{end_s - start_s:.3f}",
                 *vcodec, "-movflags", "+faststart", tmp],
                check=True, timeout=600)
            if not decodes_clean(tmp):
                raise RuntimeError("cut does not decode cleanly")
            os.replace(tmp, out_path)
            return True
        except Exception as e:
            print(f"[!] cut ({vcodec[1]}) failed for {os.path.basename(out_path)}: {e}", flush=True)
            if os.path.exists(tmp):
                os.remove(tmp)
    return False


def derive_matches(conn, game):
    """Split a game's GSI event stream into matches: same VOD + same map,
    with a new match on map change or a >MATCH_GAP_MS lull. These become the
    timeline's match bands. Returns newest first:
    {vod_id, map, date, from_ms, to_ms, start_epoch, kills, deaths}."""
    rows = conn.execute(
        """SELECT ge.event_type, ge.detail, ge.epoch_ms, ge.vod_time_ms,
                  v.id AS vod_id, v.start_epoch
           FROM gsi_events ge JOIN vods v ON v.id = ge.vod_id
           WHERE ge.game = ? AND ge.vod_time_ms IS NOT NULL AND v.duration_ms > 0
           ORDER BY v.id, ge.vod_time_ms""", (game,)).fetchall()
    matches = []
    cur = None
    for r in rows:
        if r["event_type"] == "match_end":
            # vision scanners emit real boundaries (scoreboard animating
            # in/out) — split here even though the "map" never changes
            cur = None
            continue
        m = event_map(r)
        if (cur is None or r["vod_id"] != cur["vod_id"]
                or (m != "unknown" and cur["map"] != "unknown" and m != cur["map"])
                or r["vod_time_ms"] - cur["to_ms"] > MATCH_GAP_MS):
            cur = {"vod_id": r["vod_id"], "game": game, "map": m,
                   "date": night_date(r["start_epoch"]),
                   "from_ms": r["vod_time_ms"], "to_ms": r["vod_time_ms"],
                   "start_epoch": r["epoch_ms"] // 1000,
                   "kills": 0, "deaths": 0}
            matches.append(cur)
        cur["to_ms"] = r["vod_time_ms"]
        if cur["map"] == "unknown" and m != "unknown":
            cur["map"] = m
        if r["event_type"] == "kill":
            cur["kills"] += 1
        elif r["event_type"] == "death":
            cur["deaths"] += 1
    return matches[::-1]
