"""The ingest tray: captures waiting to become clips.

Pressing the clip hotkey mid-game should cost nothing and decide nothing.
So a press captures a buffer — OBS's replay buffer when it is running, or a
deferred cut from the recording once it settles — and drops it here, tagged
with whatever game was on screen at the time. It stays a raw buffer until
you sit down, scrub it, set in and out, give it a name and keep it. Then it
is trimmed into the archive like any other clip.

Nothing in the tray is published, shared or cut until you say so, and
discarding is one click — which is the point: the hotkey is for "something
happened", not for "this is a clip".
"""
import json
import os
import re
import time
from pathlib import Path

from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import FileResponse

from clipshelf import config, db, problems, fileops
from clipshelf.config import HIGHLIGHTS_DIR, INGEST_DIR
from clipshelf.cutting import cut_range, make_proxy, night_date

router = APIRouter(tags=["ingest"])

KEEP_DAYS = 14          # untouched captures are swept, with a warning first
CAP_GB_DEFAULT = 20     # and the tray is capped, because a buffer is not small


def cap_bytes() -> int:
    gb = config.CFG.get("ingest_cap_gb", CAP_GB_DEFAULT)
    try:
        gb = float(gb)
    except (TypeError, ValueError):
        gb = CAP_GB_DEFAULT
    return int(max(0, gb) * (1000 ** 3))     # 0 disables the cap


def enabled() -> bool:
    """Off means hotkey captures land straight in the archive, as they did
    before the tray existed."""
    return bool(config.CFG.get("clip_ingest", True))


def _probe_ms(path: Path) -> int:
    import subprocess
    try:
        out = subprocess.run(
            ["ffprobe", "-v", "error", "-show_entries", "format=duration",
             "-of", "csv=p=0", str(path)],
            capture_output=True, text=True, timeout=60, encoding="utf-8", errors="replace").stdout.strip()
        return int(float(out) * 1000)
    except Exception:
        return 0


def add(path: str, game: str, source: str = "replay", event_id: int = None,
        vod_id: int = None, vod_time_ms: int = None, label: str = "") -> int:
    """Move a captured buffer into the tray. Returns the ingest id."""
    import shutil
    src = Path(path)
    INGEST_DIR.mkdir(parents=True, exist_ok=True)
    stamp = time.strftime("%Y-%m-%d_%H-%M-%S")
    dest = INGEST_DIR / f"cap_{stamp}{src.suffix or '.mp4'}"
    n = 2
    while dest.exists():
        dest = INGEST_DIR / f"cap_{stamp}-{n}{src.suffix or '.mp4'}"
        n += 1
    shutil.move(str(src), dest)
    dur = _probe_ms(dest)
    conn = db.connect()
    try:
        cur = conn.execute(
            """INSERT INTO ingest_clips
               (file, game, source, event_id, vod_id, vod_time_ms, epoch_ms,
                duration_ms, out_ms, label)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (dest.name, game or "", source, event_id, vod_id, vod_time_ms,
             int(time.time() * 1000), dur, dur or None, label or ""))
        conn.commit()
        cid = cur.lastrowid
    finally:
        conn.close()
    try:
        sweep()             # evict oldest-first if this one put us over
    except Exception as e:
        print(f"[ingest] sweep after capture failed ({e})", flush=True)
    return cid


def waiting() -> int:
    conn = db.connect()
    try:
        return conn.execute(
            "SELECT COUNT(*) FROM ingest_clips WHERE status = 'new'").fetchone()[0]
    finally:
        conn.close()


def _row(conn, cid: int):
    r = conn.execute("SELECT * FROM ingest_clips WHERE id = ?", (cid,)).fetchone()
    if not r or r["status"] != "new":
        raise HTTPException(404, "that capture is not in the tray")
    return r


def _path(row) -> Path:
    p = (INGEST_DIR / row["file"]).resolve()
    if not str(p).startswith(str(INGEST_DIR.resolve())):
        raise HTTPException(400, "bad capture path")
    return p


def _public(row) -> dict:
    from clipshelf import game_detect
    p = INGEST_DIR / row["file"]
    return {
        "id": row["id"],
        "game": row["game"],
        "game_label": game_detect.display_name(row["game"]) if row["game"] else "",
        "source": row["source"],
        "label": row["label"],
        "epoch_ms": row["epoch_ms"],
        "duration_ms": row["duration_ms"],
        "in_ms": row["in_ms"],
        "out_ms": row["out_ms"] if row["out_ms"] is not None else row["duration_ms"],
        "vod_id": row["vod_id"],
        "vod_time_ms": row["vod_time_ms"],
        "size": p.stat().st_size if p.exists() else 0,
        "missing": not p.exists(),
        "media": f"/api/ingest/{row['id']}/media",
        "age_days": max(0, (time.time() * 1000 - row["epoch_ms"]) / 86400000),
        "expires_in_days": max(0, KEEP_DAYS - (time.time() * 1000 - row["epoch_ms"]) / 86400000),
    }


def usage() -> dict:
    """What the tray is costing. Shown wherever the count is, because the
    number of captures says nothing about the gigabytes behind them."""
    conn = db.connect()
    try:
        rows = conn.execute(
            "SELECT file FROM ingest_clips WHERE status='new'").fetchall()
    finally:
        conn.close()
    total = 0
    for r in rows:
        try:
            total += (INGEST_DIR / r["file"]).stat().st_size
        except OSError:
            pass
    cap = cap_bytes()
    return {"bytes": total, "gb": round(total / 1000 ** 3, 2),
            "cap_gb": round(cap / 1000 ** 3, 2) if cap else 0,
            "full": bool(cap and total >= cap),
            "fraction": round(total / cap, 3) if cap else 0.0}


def _drop(conn, row, why: str):
    fileops.remove(_path(row))
    conn.execute("UPDATE ingest_clips SET status='discarded' WHERE id=?", (row["id"],))
    print(f"[ingest] dropped capture {row['id']} ({why})", flush=True)


def sweep() -> int:
    """Drop captures nobody came back for, then evict oldest-first until the
    tray is under its size cap.

    The age sweep alone is not enough: a heavy session can put tens of
    gigabytes of replay buffers on disk in one night, and nothing else in
    this app grows that fast without the user asking for it."""
    cutoff = int((time.time() - KEEP_DAYS * 86400) * 1000)
    conn = db.connect()
    dropped = 0
    try:
        for row in conn.execute(
                "SELECT * FROM ingest_clips WHERE status='new' AND epoch_ms < ?",
                (cutoff,)).fetchall():
            _drop(conn, row, f"older than {KEEP_DAYS} days")
            dropped += 1
        conn.commit()

        cap = cap_bytes()
        if cap:
            rows = conn.execute(
                "SELECT * FROM ingest_clips WHERE status='new' ORDER BY epoch_ms DESC"
            ).fetchall()
            total, over = 0, []
            for i, row in enumerate(rows):        # newest first: those survive
                try:
                    total += (INGEST_DIR / row["file"]).stat().st_size
                except OSError:
                    continue
                # the newest capture is never evicted, even if it alone
                # blows the cap — deleting what someone just pressed the key
                # for is the worst thing this could do
                if total > cap and i > 0:
                    over.append(row)
            for row in over:
                _drop(conn, row, "tray is over its size cap")
                dropped += 1
            conn.commit()
    finally:
        conn.close()
    if dropped:
        print(f"[ingest] swept {dropped} capture(s)", flush=True)
    return dropped


# ------------------------------------------------------------------ routes
@router.get("/api/ingest")
def ingest_list():
    conn = db.connect()
    try:
        rows = conn.execute(
            "SELECT * FROM ingest_clips WHERE status='new' ORDER BY epoch_ms DESC"
        ).fetchall()
    finally:
        conn.close()
    return {"status": "success", "enabled": enabled(), "keep_days": KEEP_DAYS,
            "usage": usage(), "captures": [_public(r) for r in rows]}


@router.get("/api/ingest/{cid}/media")
def ingest_media(cid: int):
    conn = db.connect()
    try:
        row = _row(conn, cid)
    finally:
        conn.close()
    p = _path(row)
    if not p.exists():
        raise HTTPException(404, "the captured file is gone")
    return FileResponse(p, media_type="video/mp4")


@router.post("/api/ingest/{cid}/keep")
async def ingest_keep(cid: int, request: Request):
    """Trim the capture to [in, out], name it, and file it in the archive."""
    body = await request.json()
    conn = db.connect()
    try:
        row = _row(conn, cid)
        src = _path(row)
        if not src.exists():
            raise HTTPException(404, "the captured file is gone")
        dur = row["duration_ms"] or _probe_ms(src)
        in_ms = max(0, int(body.get("in_ms") or 0))
        out_ms = int(body.get("out_ms") if body.get("out_ms") is not None else dur)
        out_ms = min(out_ms, dur) if dur else out_ms
        if out_ms - in_ms < 400:
            raise HTTPException(400, "that range is too short to cut")
        label = re.sub(r"[^\w-]+", "-", str(body.get("label") or "")).strip("-")[:40]
        game = re.sub(r"[^a-z0-9_-]", "",
                      str(body.get("game") or row["game"]).lower().strip().replace(" ", "-")) \
            or "clips"

        date = night_date(row["epoch_ms"] / 1000)
        hms = time.strftime("%H-%M-%S", time.localtime(row["epoch_ms"] / 1000))
        fname = f"c_{hms}" + (f"_{label}" if label else "") + ".mp4"
        dest = Path(HIGHLIGHTS_DIR) / game / f"marked_{date}" / fname
        n = 2
        while dest.exists():
            dest = dest.with_name(f"c_{hms}" + (f"_{label}" if label else "")
                                  + f"-{n}.mp4")
            n += 1
        dest.parent.mkdir(parents=True, exist_ok=True)
        if not cut_range(str(src), in_ms / 1000, out_ms / 1000, str(dest)):
            raise HTTPException(500, "the trim failed — see Problems in settings")
        make_proxy(str(dest))
        rel = os.path.relpath(dest, HIGHLIGHTS_DIR).replace(os.sep, "/")
        conn.execute(
            """UPDATE ingest_clips SET status='kept', in_ms=?, out_ms=?, label=?,
               game=?, rel=? WHERE id=?""",
            (in_ms, out_ms, label.replace("-", " "), game, rel, cid))
        conn.commit()
    finally:
        conn.close()
    fileops.remove(src)            # the buffer has served its purpose
    print(f"[{time.strftime('%X')}] [ingest] ✂ kept {rel}", flush=True)
    return {"status": "success", "rel": rel,
            "src": "/api/editor/highlights/file/" + rel, "waiting": waiting()}


@router.post("/api/ingest/bulk")
async def ingest_bulk(request: Request):
    """Clear the tray in one move: keep everything untrimmed, or drop it all.

    After a long session the honest answer is often "none of these" or "all
    of them, whole" — making that a per-capture chore is how a tray stops
    being used at all."""
    body = await request.json()
    action = str(body.get("action") or "")
    ids = body.get("ids")
    conn = db.connect()
    try:
        rows = conn.execute(
            "SELECT * FROM ingest_clips WHERE status='new' ORDER BY epoch_ms").fetchall()
    finally:
        conn.close()
    if ids:
        want = {int(i) for i in ids}
        rows = [r for r in rows if r["id"] in want]
    if action == "discard":
        for r in rows:
            ingest_discard(r["id"])
        return {"status": "success", "discarded": len(rows), "waiting": waiting()}
    if action == "keep":
        kept, failed = [], []
        for r in rows:
            try:
                res = await ingest_keep(r["id"], _Body({
                    "in_ms": r["in_ms"],
                    "out_ms": r["out_ms"] if r["out_ms"] is not None else r["duration_ms"],
                    "label": r["label"], "game": r["game"]}))
                kept.append(res["rel"])
            except Exception as e:
                failed.append({"id": r["id"], "error": str(e)[:120]})
        return {"status": "success", "kept": len(kept), "failed": failed,
                "waiting": waiting()}
    raise HTTPException(400, "action must be 'keep' or 'discard'")


class _Body:
    """Lets the bulk route reuse the single-capture handler rather than
    duplicating the trim logic."""

    def __init__(self, data):
        self._data = data

    async def json(self):
        return self._data


@router.delete("/api/ingest/{cid}")
def ingest_discard(cid: int):
    conn = db.connect()
    try:
        row = _row(conn, cid)
        fileops.remove(_path(row))
        conn.execute("UPDATE ingest_clips SET status='discarded' WHERE id=?", (cid,))
        conn.commit()
    finally:
        conn.close()
    return {"status": "success", "waiting": waiting()}


@router.post("/api/ingest/{cid}/label")
async def ingest_label(cid: int, request: Request):
    """Save in/out/label without cutting — so a long tray can be triaged in
    one pass and cut later."""
    body = await request.json()
    conn = db.connect()
    try:
        row = _row(conn, cid)
        conn.execute(
            "UPDATE ingest_clips SET in_ms=?, out_ms=?, label=?, game=? WHERE id=?",
            (max(0, int(body.get("in_ms") or 0)),
             int(body.get("out_ms") if body.get("out_ms") is not None
                 else (row["out_ms"] or row["duration_ms"])),
             str(body.get("label") or "")[:60],
             str(body.get("game") or row["game"])[:40], cid))
        conn.commit()
    finally:
        conn.close()
    return {"status": "success"}
