#!/usr/bin/env python3
"""Layer 2 of the install-confidence ladder: real venv + pip install smoke.

This is the slow, network-bound, opt-in layer. It reproduces what a fresh
machine actually does on first launch (``backend/bootstrap.py``):

1. Create a brand-new virtual environment from a clean interpreter (preferring
   Python 3.12, the version the shipped standalone runtime uses).
2. ``pip install --prefer-binary -r requirements-headless.txt`` into it — the
   exact command and flags the bootstrap runs.
3. Verify the backend's hard runtime dependencies actually import in that fresh
   venv (the ``_ensure_backend_runtime_deps`` list).

It catches the "dependency resolution / wheel availability" breakage class:
a requirement that no longer resolves, a missing transitive wheel for the
target Python, or a dep the backend imports that nobody listed in requirements.

It does NOT need a second machine, but it DOES download packages, so it is
opt-in (``make install-smoke`` or ``MUGI_INSTALL_SMOKE=1 pytest``) and never
runs in the normal test suite.

Usage:
    python scripts/install_smoke.py [--python PATH] [--requirements PATH]
                                    [--no-cache] [--keep]

Exit 0 + "INSTALL_SMOKE_OK" on success; non-zero with captured pip/import
output on failure.
"""

from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
import tempfile

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Hard runtime deps the backend imports on boot. Mirrors
# backend/bootstrap.py::_ensure_backend_runtime_deps. If a fresh install can't
# import one of these, first run is dead — exactly what we want to catch here.
REQUIRED_RUNTIME_MODULES = (
    "fastapi",
    "openai",
    "pydantic",
    "requests",
    "httpx",
    "cryptography",
    "zeroconf",
)


def _resolve_requirements(override: str | None) -> str:
    if override:
        path = override if os.path.isabs(override) else os.path.join(REPO_ROOT, override)
        if not os.path.isfile(path):
            raise SystemExit(f"FAIL: requirements file not found: {path}")
        return path
    # Mirror bootstrap: headless first (no PyObjC), then full.
    for name in ("requirements-headless.txt", "requirements.txt"):
        candidate = os.path.join(REPO_ROOT, name)
        if os.path.isfile(candidate):
            return candidate
    raise SystemExit("FAIL: no requirements-headless.txt or requirements.txt found")


def _resolve_install_python(override: str | None) -> tuple[str, bool]:
    """Pick the interpreter to build the venv from. Returns (path, is_312)."""
    candidates: list[str] = []
    if override:
        candidates.append(override)
    env_py = os.environ.get("MUGI_INSTALL_SMOKE_PYTHON")
    if env_py:
        candidates.append(env_py)
    which_312 = shutil.which("python3.12")
    if which_312:
        candidates.append(which_312)
    candidates.append(
        os.path.join(os.path.expanduser("~"), ".mugi", "python-3.12", "bin", "python3")
    )
    for cand in candidates:
        if cand and os.path.isfile(shutil.which(cand) or cand):
            return (shutil.which(cand) or cand), True
    # Last resort: current interpreter (may be a different minor version).
    return sys.executable, False


def _python_version(py: str) -> str:
    try:
        out = subprocess.run(
            [py, "-c", "import sys;print('.'.join(map(str, sys.version_info[:3])))"],
            capture_output=True,
            text=True,
            timeout=30,
        )
        return out.stdout.strip() or "?"
    except Exception:
        return "?"


def run_install_smoke(
    *,
    python_override: str | None = None,
    requirements_override: str | None = None,
    no_cache: bool = False,
    keep: bool = False,
) -> int:
    req_path = _resolve_requirements(requirements_override)
    install_python, is_312 = _resolve_install_python(python_override)
    ver = _python_version(install_python)
    print(f"[install-smoke] requirements: {req_path}")
    print(f"[install-smoke] interpreter:  {install_python} (Python {ver})")
    if not is_312:
        print(
            "[install-smoke] WARNING: not building from Python 3.12 (the shipped "
            "standalone runtime version). Results may diverge from a real install. "
            "Install python3.12 or pass --python for a faithful run.",
            file=sys.stderr,
        )

    tmp = tempfile.mkdtemp(prefix="mugi_install_smoke_")
    venv_dir = os.path.join(tmp, "venv")
    try:
        print(f"[install-smoke] creating venv -> {venv_dir}")
        r = subprocess.run(
            [install_python, "-m", "venv", venv_dir],
            capture_output=True,
            text=True,
            timeout=300,
        )
        if r.returncode != 0:
            print("FAIL: venv creation failed.", file=sys.stderr)
            print((r.stderr or r.stdout).rstrip(), file=sys.stderr)
            return 1

        venv_python = os.path.join(venv_dir, "bin", "python3")
        if not os.path.isfile(venv_python):
            venv_python = os.path.join(venv_dir, "bin", "python")
        if not os.path.isfile(venv_python):
            print(f"FAIL: venv python missing under {venv_dir}", file=sys.stderr)
            return 1

        # Best-effort pip upgrade (mirrors bootstrap; older pip may still work).
        print("[install-smoke] upgrading pip")
        subprocess.run(
            [venv_python, "-m", "pip", "install", "--upgrade", "pip"],
            timeout=300,
        )

        pip_cmd = [venv_python, "-m", "pip", "install", "--prefer-binary"]
        if no_cache:
            pip_cmd.append("--no-cache-dir")
        pip_cmd += ["-r", req_path]
        print(f"[install-smoke] installing requirements (this can take minutes)\n    {' '.join(pip_cmd)}")
        # Stream live so a human watching sees progress; captured by pytest.
        install = subprocess.run(pip_cmd, cwd=REPO_ROOT, timeout=2400)
        if install.returncode != 0:
            print(
                f"FAIL: pip install -r {os.path.basename(req_path)} failed "
                f"(rc={install.returncode}). See pip output above.",
                file=sys.stderr,
            )
            return 1

        print("[install-smoke] verifying backend runtime imports")
        import_check = subprocess.run(
            [
                venv_python,
                "-c",
                "import importlib.util, sys\n"
                "mods=" + repr(list(REQUIRED_RUNTIME_MODULES)) + "\n"
                "missing=[m for m in mods if importlib.util.find_spec(m) is None]\n"
                "print('MISSING=' + ','.join(missing))\n"
                "sys.exit(1 if missing else 0)",
            ],
            capture_output=True,
            text=True,
            timeout=120,
        )
        print((import_check.stdout or "").rstrip())
        if import_check.returncode != 0:
            print(
                "FAIL: backend runtime modules missing after install — a required "
                "dependency is not in the requirements file.",
                file=sys.stderr,
            )
            if import_check.stderr.strip():
                print(import_check.stderr.rstrip(), file=sys.stderr)
            return 1

        print("INSTALL_SMOKE_OK")
        print("PASS: fresh venv installed requirements and imports the backend stack.")
        return 0
    finally:
        if keep:
            print(f"[install-smoke] kept venv dir: {tmp}")
        else:
            shutil.rmtree(tmp, ignore_errors=True)


def main() -> int:
    parser = argparse.ArgumentParser(description="Real venv + pip install smoke.")
    parser.add_argument("--python", help="interpreter to build the venv from")
    parser.add_argument("--requirements", help="requirements file (default: headless)")
    parser.add_argument(
        "--no-cache", action="store_true", help="pip --no-cache-dir (cold resolution)"
    )
    parser.add_argument("--keep", action="store_true", help="keep the temp venv dir")
    args = parser.parse_args()
    return run_install_smoke(
        python_override=args.python,
        requirements_override=args.requirements,
        no_cache=args.no_cache,
        keep=args.keep,
    )


if __name__ == "__main__":
    raise SystemExit(main())
