"""Vision-scan driver — runs game-adapter `scan()` implementations over the
chapter windows of settled VODs and lands the events in gsi_events, exactly
like live GSI ingest (one event contract, one timeline).

An adapter opts in by defining:

    def scan(path, t0, t1) -> iterable[(event_type, t_seconds, detail_dict)]
    SCAN_VERSION = 1   # bump to force a rescan of already-scanned VODs

`sample()` below is the shared decode helper scanners are expected to use:
1fps raw frames of a filtered crop, normalized to 1080p first (crops assume
1920×1080). Rescans are idempotent: a VOD's previous rows for (vod, game)
are replaced in one write. Event types are stored as emitted — the timeline
renders unknown types with default styling, and "match_end" splits match
bands (see cutting.derive_matches).
"""
import json
import os
import subprocess
import threading
import time

from clipshelf import problems
from clipshelf import config
from clipshelf import db
from clipshelf.config import DATA
from clipshelf.cutting import vod_is_settled
from clipshelf.plugin_host import registry

SCAN_DIR = DATA / "scan"
SCANNED = SCAN_DIR / "scanned.json"
MIN_WINDOW_S = 120          # chapters shorter than this aren't worth a decode
SCAN_LOOP_S = 30 * 60


def sample(path, t0, dur, vf, w, h, ch=1):
    """1fps raw frames of a filtered crop as (n, h, w[, ch]) uint8.
    Everything is normalized to 1080p first — crops assume 1920×1080."""
    import numpy as np
    fmt = "gray" if ch == 1 else "rgb24"
    full_vf = f"fps=1,scale=1920:1080,{vf},format={fmt}"
    px = w * h * ch
    r = subprocess.run(
        ["ffmpeg", "-v", "error", "-ss", f"{t0:.0f}", "-t", f"{dur:.0f}", "-i", path,
         "-vf", full_vf, "-f", "rawvideo", "-"],
        capture_output=True, timeout=int(120 + dur))
    n = len(r.stdout) // px
    a = np.frombuffer(r.stdout, dtype=np.uint8)[:n * px]
    return a.reshape((n, h, w) if ch == 1 else (n, h, w, ch))


def windows_for(conn, game, vod_id=None):
    """(vod_id, filename, start_s, end_s) chapter spans tagged with the game.
    A chapter runs to the next chapter mark or the end of the VOD."""
    q = """SELECT c.vod_id, c.start_time_ms, v.filename, v.duration_ms,
                  (SELECT MIN(c2.start_time_ms) FROM vod_chapters c2
                   WHERE c2.vod_id = c.vod_id AND c2.start_time_ms > c.start_time_ms) nxt
           FROM vod_chapters c JOIN vods v ON v.id = c.vod_id
           WHERE c.game_tag = ? AND v.duration_ms > 0 AND c.start_time_ms IS NOT NULL"""
    args = [game]
    if vod_id:
        q += " AND c.vod_id = ?"
        args.append(vod_id)
    for r in conn.execute(q + " ORDER BY c.vod_id DESC, c.start_time_ms", args):
        end = (r["nxt"] if r["nxt"] else r["duration_ms"]) / 1000
        yield r["vod_id"], r["filename"], r["start_time_ms"] / 1000, end


def _ledger() -> dict:
    try:
        return json.loads(SCANNED.read_text(encoding="utf-8"))
    except Exception:
        return {}


def _ledger_write(seen: dict):
    SCAN_DIR.mkdir(parents=True, exist_ok=True)
    tmp = SCANNED.with_suffix(".json.tmp")
    tmp.write_text(json.dumps(seen), encoding="utf-8")
    tmp.replace(SCANNED)


def scan_vod(conn, game, adapter, vod_id, fname, wins) -> int:
    """Scan every window of one VOD, then replace that VOD's rows in one
    write — a multi-chapter VOD must never have a later window's DELETE eat
    an earlier window's events."""
    path = os.path.join(config.VOD_DIR, fname)
    if not os.path.exists(path):
        print(f"[scan] [{vod_id}] missing file {fname}", flush=True)
        return 0
    row = conn.execute("SELECT start_epoch FROM vods WHERE id=?", (vod_id,)).fetchone()
    start_epoch = (row[0] if row else 0) or 0
    events = []
    for t0, t1 in wins:
        try:
            got = list(adapter.scan(path, t0, t1))
        except Exception as e:  # a broken scanner must not stop the pass
            problems.record(f"{game} scanner", e,
                            f"vod {vod_id} {t0:.0f}-{t1:.0f}s — the game pack may need "
                            "templates for your account")
            continue
        print(f"[scan] [{vod_id}] {fname} {t0/60:.0f}-{t1/60:.0f}min → {len(got)} events",
              flush=True)
        events += got
    # rescans must clear old rows even when the new pass keeps nothing
    conn.execute("DELETE FROM gsi_events WHERE vod_id=? AND game=?", (vod_id, game))
    for et, t, det in events:
        det = det if isinstance(det, dict) else {}
        etype = str(et).strip()[:40] or "event"
        title = str(det.get("title") or etype)[:200]
        conn.execute(
            """INSERT INTO gsi_events (vod_id, game, event_type, title, detail,
               epoch_ms, vod_time_ms) VALUES (?,?,?,?,?,?,?)""",
            (vod_id, game, etype, title,
             json.dumps(det), int((start_epoch + t) * 1000), int(t * 1000)))
    conn.commit()
    return len(events)


def scan_pass(game: str = None, vod_id: int = None, limit: int = None,
              force: bool = False) -> dict:
    """One pass over every scan-capable adapter (or one game / one VOD).
    Skips VODs already scanned at the adapter's current SCAN_VERSION and
    VODs still being recorded."""
    out = {"scanned": 0, "events": 0}
    seen = _ledger()
    conn = db.connect()
    try:
        for slug, adapter in list(registry.game_adapters.items()):
            if game and slug != game:
                continue
            if not callable(getattr(adapter, "scan", None)):
                continue
            version = int(getattr(adapter, "SCAN_VERSION", 1))
            by_vod = {}
            for vid, fname, t0, t1 in windows_for(conn, slug, vod_id):
                if t1 - t0 >= MIN_WINDOW_S:
                    by_vod.setdefault((vid, fname), []).append((t0, t1))
            for (vid, fname), wins in by_vod.items():
                key = f"{slug}:{vid}"
                if not force and seen.get(key) == version:
                    continue
                path = os.path.join(config.VOD_DIR, fname)
                if not vod_is_settled(path):
                    continue    # still recording — next pass gets it
                if limit and out["scanned"] >= limit:
                    return out
                out["events"] += scan_vod(conn, slug, adapter, vid, fname, wins)
                out["scanned"] += 1
                seen[key] = version
                _ledger_write(seen)
    finally:
        conn.close()
    return out


def scan_loop():
    time.sleep(120)
    while True:
        try:
            r = scan_pass()
            if r["scanned"]:
                print(f"[scan] pass done — {r['events']} events / {r['scanned']} vods",
                      flush=True)
        except Exception as e:
            problems.record("vision scan", e)
        time.sleep(SCAN_LOOP_S)


def start():
    threading.Thread(target=scan_loop, daemon=True).start()
