"""`run.py --selftest` — verify an install on hardware the developer can't
touch, and print one report worth pasting.

Every check answers a question that only the target machine can answer
(does WebView2 exist? can we bind a global hotkey? does NVENC work? does a
real render come out right?), reports PASS/WARN/FAIL with the reason, and
never leaves anything behind.
"""
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path

ROWS = []


def _row(status: str, name: str, detail: str = ""):
    ROWS.append((status, name, detail))
    mark = {"PASS": "✓", "WARN": "!", "FAIL": "✕", "INFO": "·"}[status]
    print(f"  {mark} {name:<26} {detail}", flush=True)


def _run(cmd, timeout=60):
    return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, encoding="utf-8", errors="replace")


def check_platform():
    _row("INFO", "platform", f"{platform.system()} {platform.release()} ({platform.machine()})")
    _row("INFO", "python", sys.version.split()[0])


def check_binaries():
    try:
        from clipshelf import ffmpeg_setup
        if ffmpeg_setup.bundled():
            _row("PASS", "ffmpeg (bundled)", ffmpeg_setup.bundled())
    except Exception:
        pass
    for exe, why, hard in (("ffmpeg", "cutting/encoding", True),
                           ("ffprobe", "media probing", True),
                           ("node", "editor exports", False)):
        p = shutil.which(exe)
        if p:
            _row("PASS", exe, p)
        else:
            _row("FAIL" if hard else "WARN", exe, f"missing — needed for {why}")
    try:
        from clipshelf.config import ROOT
        _row("PASS" if (ROOT / "node_modules").is_dir() else "WARN", "node_modules",
             "present" if (ROOT / "node_modules").is_dir() else "run `npm ci` to enable exports")
    except Exception:
        pass


def check_chrome():
    """The render engine needs a Chromium. Windows normally has Edge, but a
    debloated install may have none — the app can download one."""
    from clipshelf.config import ROOT
    finder = ROOT / "render" / "chrome_find.js"
    if not shutil.which(os.getenv("NODE", "node")) or not finder.exists():
        _row("WARN", "render browser", "skipped (no node)")
        return
    env = dict(os.environ)
    try:
        from clipshelf.config import DATA, CFG
        env["CLIPSHELF_BROWSERS"] = str(DATA / "browsers")
        if CFG.get("chrome_path"):
            env["CHROME"] = str(CFG["chrome_path"])
    except Exception:
        pass
    r = subprocess.run([os.getenv("NODE", "node"), str(finder)],
                       capture_output=True, text=True, timeout=60, env=env, encoding="utf-8", errors="replace")
    if r.returncode == 0 and r.stdout.strip():
        _row("PASS", "render browser", r.stdout.strip()[:70])
    else:
        first = (r.stderr or "").strip().splitlines()
        _row("WARN", "render browser",
             (first[0] if first else "not found") +
             " — Settings can download one (~180 MB), or install Chrome")


def check_encoders():
    if not shutil.which("ffmpeg"):
        return
    r = _run(["ffmpeg", "-hide_banner", "-encoders"], timeout=60)
    have = r.stdout
    _row("PASS" if "libx264" in have else "FAIL", "libx264",
         "available" if "libx264" in have else "MISSING — this ffmpeg build can't cut clips")
    if "h264_nvenc" in have:
        # 64x64 is below NVENC's minimum on some drivers — probe at a real size
        t = _run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "lavfi",
                  "-i", "color=black:s=640x360:d=0.2", "-pix_fmt", "yuv420p",
                  "-c:v", "h264_nvenc", "-f", "null", "-"], timeout=60)
        if t.returncode == 0:
            _row("PASS", "nvenc", "usable (faster proxies)")
        else:
            why = " ".join((t.stderr or "").split())[-140:] or "no error output"
            _row("WARN", "nvenc", f"unusable, x264 fallback — {why}")
    else:
        _row("INFO", "nvenc", "not in this ffmpeg build — x264 fallback")


def check_desktop():
    """The two things a Linux dev box cannot answer."""
    try:
        import logging
        logging.getLogger("pywebview").setLevel(logging.CRITICAL)
        import webview
        gui = webview.initialize()          # returns the backend module
        backend = getattr(gui, "__name__", str(gui)).split(".")[-1]
        _row("PASS", "app window", f"{backend} backend")
    except Exception as e:
        hint = ""
        if os.name == "nt":
            hint = " — install the WebView2 Runtime (winget install Microsoft.EdgeWebView2Runtime)"
        _row("WARN", "app window", f"{type(e).__name__}: {e}{hint}")
    try:
        import pystray  # noqa: F401
        _row("PASS", "tray icon", "pystray available")
    except Exception as e:
        _row("WARN", "tray icon", f"unavailable ({type(e).__name__})")
    try:
        import keyboard
        hk = "ctrl+alt+f24"
        keyboard.add_hotkey(hk, lambda: None)
        keyboard.remove_hotkey(hk)
        _row("PASS", "global hotkeys", "can bind")
    except ImportError:
        _row("WARN", "global hotkeys", "the `keyboard` package isn't installed")
    except Exception as e:
        _row("WARN", "global hotkeys", f"can't bind ({type(e).__name__}) — API bindings still work")


def check_transcription():
    try:
        from faster_whisper import WhisperModel  # noqa: F401
        import ctranslate2
        _row("PASS", "transcription", f"faster-whisper ready (ctranslate2 {ctranslate2.__version__})")
    except ImportError as e:
        _row("WARN", "transcription", f"unavailable ({e}) — speech search and "
             "auto-captions are off; often means no wheel for this Python version yet")
    except Exception as e:
        _row("WARN", "transcription", f"{type(e).__name__}: {e}")


def check_config_and_db():
    from clipshelf import config, db
    _row("PASS" if config.configured() else "WARN", "recordings folder",
         config.VOD_DIR or "not set (run the setup wizard)")
    try:
        db.init()
        conn = db.connect()
        try:
            from clipshelf.migrations import MIGRATIONS, current_version
            v = current_version(conn)
            n = conn.execute("SELECT COUNT(*) FROM vods").fetchone()[0]
        finally:
            conn.close()
        _row("PASS", "database", f"schema v{v}/{MIGRATIONS[-1][0]}, {n} recordings")
    except Exception as e:
        _row("FAIL", "database", f"{type(e).__name__}: {e}")


def check_obs():
    from clipshelf import config, obs_client
    try:
        obs_client.check_and_reconnect()
        if obs_client.connected():
            tc = obs_client.get_obs_timecode()
            _row("PASS", "obs websocket", f"connected{f', recording {tc//1000}s in' if tc else ''}")
        else:
            ws = config.CFG.get("obs_ws") or {}
            _row("WARN", "obs websocket", f"not reachable at "
                 f"{ws.get('host','127.0.0.1')}:{ws.get('port',4455)} — events fall back to wall clock")
    except Exception as e:
        _row("WARN", "obs websocket", f"{type(e).__name__}: {e}")


def check_plugins():
    from clipshelf.plugin_host import loader, registry
    plugins = loader.load_all()
    ok = [p for p in plugins.values() if p["status"] == "ok"]
    bad = [p for p in plugins.values() if p["status"] not in ("ok", "disabled")]
    _row("PASS" if not bad else "WARN", "plugins",
         f"{len(ok)} loaded" + (f", {len(bad)} broken: " +
                                ", ".join(f"{p['id']}({p['status']})" for p in bad) if bad else ""))
    _row("INFO", "game adapters", ", ".join(sorted(registry.game_adapters)) or "none")


def check_render():
    """A real end-to-end export — the only proof the render path works on
    this machine's GPU/driver/Chrome combination."""
    if not shutil.which("ffmpeg"):
        _row("WARN", "test render", "skipped (no ffmpeg)")
        return
    tmp = Path(tempfile.mkdtemp(prefix="selftest-"))
    try:
        src = tmp / "src.mp4"
        r = _run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
                  "-f", "lavfi", "-i", "testsrc=size=640x360:rate=30:duration=3",
                  "-c:v", "libx264", "-preset", "veryfast", str(src)], timeout=120)
        if r.returncode != 0:
            _row("FAIL", "test render", "couldn't even create a test clip")
            return
        from clipshelf.cutting import cut_range, decodes_clean
        out = tmp / "cut.mp4"
        t0 = time.time()
        if cut_range(str(src), 0.5, 2.0, str(out)) and decodes_clean(str(out)):
            _row("PASS", "clip cutting", f"{time.time()-t0:.1f}s, decodes clean")
        else:
            _row("FAIL", "clip cutting", "cut failed or didn't decode")
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def check_environment():
    """Things about the machine that quietly break the app."""
    for var in ("PYTHONHOME", "PYTHONPATH"):
        if os.environ.get(var):
            _row("WARN", var,
                 f"set to {os.environ[var][:60]} — clear it; it makes Python "
                 "fail to find its own stdlib")
    try:
        from clipshelf.config import ROOT
        low = str(ROOT).lower()
        if "onedrive" in low or "dropbox" in low or "google drive" in low:
            _row("WARN", "install location", f"{ROOT} is inside a syncing cloud folder — "
                 "renders and the database will churn uploads; move it to e.g. C:\\Apps")
        else:
            _row("PASS", "install location", str(ROOT))
    except Exception:
        pass


def check_disk():
    from clipshelf import config
    try:
        du = shutil.disk_usage(config.VOD_DIR or str(config.DATA))
        gb = du.free / 1e9
        _row("PASS" if gb > 20 else "WARN", "free disk", f"{gb:.0f} GB")
    except Exception as e:
        _row("WARN", "free disk", str(e))


def main() -> int:
    from clipshelf import __version__
    print(f"\n  self-test — Clipshelf {__version__}\n" + "  " + "-" * 52)
    for fn in (check_platform, check_environment, check_binaries, check_chrome,
               check_encoders, check_desktop, check_transcription,
               check_config_and_db, check_obs,
               check_plugins, check_render, check_disk):
        try:
            fn()
        except Exception as e:  # a broken check must not hide the rest
            _row("FAIL", fn.__name__.replace("check_", ""), f"{type(e).__name__}: {e}")
    fails = [r for r in ROWS if r[0] == "FAIL"]
    warns = [r for r in ROWS if r[0] == "WARN"]
    print("  " + "-" * 52)
    print(f"  {len(fails)} failed · {len(warns)} warnings · "
          f"{len([r for r in ROWS if r[0] == 'PASS'])} passed")
    if fails:
        print("\n  BLOCKING:")
        for _, name, detail in fails:
            print(f"    ✕ {name}: {detail}")
    print("\n  --- paste-able report ---")
    print(json.dumps({"version": __version__,
                      "platform": f"{platform.system()} {platform.release()}",
                      "rows": [{"status": s, "check": n, "detail": d} for s, n, d in ROWS]},
                     indent=1))
    return 1 if fails else 0


if __name__ == "__main__":
    sys.exit(main())
