"""Getting ffmpeg, so the installer doesn't have to ship it.

ffmpeg is the one hard dependency this app cannot fake: no proxies, no
cuts, no renders, no transcription without it. Until now the answer was
"install it yourself and put it on PATH", which is a reasonable ask of a
developer and an unreasonable one of someone who just wants their clips.

We fetch it instead of bundling it, and that is a licensing decision as
much as a size one. The builds that carry libx264 — which the encode paths
use — are GPL, and redistributing them inside a proprietary installer means
taking on GPL obligations for that binary. Downloading the upstream build
onto the user's machine, at their request, is the same thing every other
app in this position does and keeps the question from arising.

The download is verified against the size the server advertises and the
archive must actually contain a working ffmpeg.exe/ffprobe — a truncated
zip that unpacks to nothing would otherwise look like success.
"""
import os
import shutil
import subprocess
import threading
import time
import urllib.request
import zipfile
from pathlib import Path

from fastapi import APIRouter

from clipshelf import net, config
from clipshelf.config import DATA

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

TOOLS = DATA / "tools"
# BtbN's builds: predictable "latest" URLs, no scraping, both binaries inside
SOURCES = {
    "win32": "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/"
             "ffmpeg-master-latest-win64-gpl.zip",
    "linux": "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/"
             "ffmpeg-master-latest-linux64-gpl.tar.xz",
}
state = {"running": False, "ok": None, "detail": "", "progress": 0.0}


def _exe(name: str) -> str:
    return name + (".exe" if os.name == "nt" else "")


def bundled() -> str:
    """Path to an ffmpeg we installed, or ''."""
    for p in TOOLS.rglob(_exe("ffmpeg")):
        if p.is_file():
            return str(p)
    return ""


def find() -> str:
    """Whatever ffmpeg this machine should use: configured, bundled, PATH."""
    cfg = str(config.CFG.get("ffmpeg_path") or "")
    if cfg and Path(cfg).is_file():
        return cfg
    b = bundled()
    if b:
        return b
    return shutil.which("ffmpeg") or ""


def available() -> bool:
    return bool(find())


def _verify(path: str) -> bool:
    try:
        r = subprocess.run([path, "-version"], capture_output=True, text=True, timeout=60, encoding="utf-8", errors="replace")
        return r.returncode == 0 and "ffmpeg version" in (r.stdout or "")
    except Exception:
        return False


def install() -> dict:
    """Fetch ffmpeg into the app's data dir and remember where it went."""
    key = "win32" if os.name == "nt" else "linux"
    url = SOURCES[key]
    state.update(running=True, ok=None, detail="downloading", progress=0.0)
    try:
        TOOLS.mkdir(parents=True, exist_ok=True)
        tmp = TOOLS / ("dl" + (".zip" if key == "win32" else ".tar.xz"))
        req = urllib.request.Request(url, headers={"User-Agent": "Clipshelf"})
        with urllib.request.urlopen(req, timeout=120, context=net.context()) as r:
            total = int(r.headers.get("Content-Length") or 0)
            got = 0
            with open(tmp, "wb") as fh:
                while True:
                    chunk = r.read(1 << 20)
                    if not chunk:
                        break
                    fh.write(chunk)
                    got += len(chunk)
                    if total:
                        state["progress"] = round(got / total, 3)
        if total and got < total:
            raise ValueError("download ended early")

        state.update(detail="unpacking", progress=1.0)
        dest = TOOLS / "ffmpeg"
        shutil.rmtree(dest, ignore_errors=True)
        dest.mkdir(parents=True, exist_ok=True)
        if key == "win32":
            with zipfile.ZipFile(tmp) as z:
                for n in z.namelist():
                    base = os.path.basename(n)
                    if base in (_exe("ffmpeg"), _exe("ffprobe")):
                        (dest / base).write_bytes(z.read(n))
        else:
            import tarfile
            with tarfile.open(tmp) as t:
                for m in t.getmembers():
                    if os.path.basename(m.name) in ("ffmpeg", "ffprobe") and m.isfile():
                        f = t.extractfile(m)
                        out = dest / os.path.basename(m.name)
                        out.write_bytes(f.read())
                        out.chmod(0o755)
        tmp.unlink(missing_ok=True)

        got_path = str(dest / _exe("ffmpeg"))
        # an archive that unpacked to nothing would otherwise read as success
        if not Path(got_path).is_file() or not _verify(got_path):
            raise ValueError("the download did not contain a working ffmpeg")
        config.apply({"ffmpeg_path": got_path})
        ensure_on_path()          # usable immediately, without a restart
        state.update(ok=True, detail=got_path)
        print(f"[ffmpeg] installed to {got_path}", flush=True)
        return {"status": "success", "path": got_path}
    except Exception as e:
        state.update(ok=False, detail=str(e)[:300])
        from clipshelf import problems
        problems.record("ffmpeg", e, "could not download ffmpeg — install it manually "
                                    "and set its path in Settings")
        return {"status": "error", "error": str(e)[:300]}
    finally:
        state["running"] = False


def ensure_on_path() -> str:
    """Put our ffmpeg where the rest of the app already looks for it.

    Fifty-three call sites say subprocess.run(["ffmpeg", ...]) and
    shutil.which("ffmpeg"), which is the right way to write them. Rather
    than thread a path through all of those, prepend the directory to PATH
    once at startup — a bundled copy then wins over a system one, and every
    existing call finds it unchanged."""
    path = find()
    if not path:
        return ""
    d = os.path.dirname(path)
    parts = os.environ.get("PATH", "").split(os.pathsep)
    if d and d not in parts:
        os.environ["PATH"] = d + os.pathsep + os.environ.get("PATH", "")
    return path


@router.get("/api/ffmpeg")
def ffmpeg_state():
    path = find()
    return {"present": bool(path), "path": path, "bundled": bool(bundled()),
            "on_path": bool(shutil.which("ffmpeg")), **state}


@router.post("/api/ffmpeg/install")
def ffmpeg_install():
    if state["running"]:
        return {"status": "already running"}
    threading.Thread(target=install, daemon=True).start()
    time.sleep(0.2)
    return {"status": "started"}
