"""Backend venv bootstrap — ensure ~/.mugi/venv exists with requirements so
the app works without the user creating a venv manually."""
from __future__ import annotations

import importlib.util
import os
import subprocess
import sys

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

# ---------------------------------------------------------------------------
# Backend venv bootstrap — ensure ~/.mugi/venv exists with requirements so
# the app works without the user creating a venv manually.
# ---------------------------------------------------------------------------
_BACKEND_VENV_DIR = os.path.join(os.path.expanduser("~"), ".mugi", "venv")
_SETUP_LOCK_PATH = os.path.join(os.path.expanduser("~"), ".mugi", ".setup.lock")
_MUGI_DIR = os.path.join(os.path.expanduser("~"), ".mugi")


def _darwin_standalone_python_candidates() -> list[str]:
    return [
        os.path.join(_MUGI_DIR, "python-3.12", "bin", "python3"),
        os.path.join(_MUGI_DIR, "python", "bin", "python3"),
    ]


def _pick_bootstrap_python_darwin(in_mugi: bool, in_venv_dir: bool) -> str | None:
    """Return interpreter used to create ~/.mugi/venv — only Mugi standalone, never Homebrew.

    Using ~/.mugi/homebrew Python for the backend venv triggers macOS library validation
    failures (Team ID mismatch) when loading PyPI wheels such as pydantic_core.
    """
    if in_mugi and not in_venv_dir and os.path.isfile(sys.executable):
        return sys.executable
    for p in _darwin_standalone_python_candidates():
        if os.path.isfile(p):
            return p
    return None


def _entitlements_plist_path() -> str:
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), "mugi_python_runtime_entitlements.plist")


def _interpreter_stamp_current(real_bin: str) -> str:
    st = os.stat(real_bin)
    ns = getattr(st, "st_mtime_ns", int(st.st_mtime * 1_000_000_000))
    return f"{real_bin}\n{st.st_size}\n{ns}"


def _interpreter_codesign_stamp_path() -> str:
    return os.path.join(_MUGI_DIR, ".python_runtime_codesign_stamp")


def _needs_interpreter_codesign_stamp_refresh(real_bin: str) -> bool:
    want = _interpreter_stamp_current(real_bin)
    try:
        with open(_interpreter_codesign_stamp_path(), "r", encoding="utf-8") as f:
            return f.read().strip() != want
    except OSError:
        return True


def _write_interpreter_codesign_stamp(real_bin: str) -> None:
    os.makedirs(os.path.dirname(_interpreter_codesign_stamp_path()), exist_ok=True)
    with open(_interpreter_codesign_stamp_path(), "w", encoding="utf-8") as f:
        f.write(_interpreter_stamp_current(real_bin))


def _codesign_mugi_interpreter_binary(real_bin: str) -> bool:
    """Ad-hoc re-sign Mugi-owned python with disable-library-validation (macOS only)."""
    if sys.platform != "darwin":
        return False
    codesign = "/usr/bin/codesign"
    if not os.path.isfile(codesign) or not os.path.isfile(real_bin):
        return False
    mugi_abs = os.path.abspath(_MUGI_DIR) + os.sep
    real_abs = os.path.abspath(real_bin)
    if real_abs != os.path.abspath(_MUGI_DIR) and not real_abs.startswith(mugi_abs):
        return False
    ent = _entitlements_plist_path()
    if not os.path.isfile(ent):
        return False
    try:
        r = subprocess.run(
            [codesign, "-s", "-", "--force", "--timestamp=none", "--entitlements", ent, real_bin],
            capture_output=True,
            text=True,
            timeout=120,
        )
        if r.returncode != 0:
            err = (r.stderr or r.stdout or "").strip()
            if err:
                _bootstrap_progress(f"codesign failed ({real_bin}): {err[:500]}")
            return False
        return True
    except Exception as e:
        _bootstrap_progress(f"codesign error: {e}")
        return False


def _ensure_darwin_backend_python_signed_before_native_imports() -> None:
    """Before importing FastAPI/pydantic: sign real interpreter, then execv once if needed."""
    if sys.platform != "darwin":
        return
    if not _running_in_backend_venv():
        return
    mugi_abs = os.path.abspath(_MUGI_DIR)
    try:
        real_bin = os.path.realpath(sys.executable)
    except OSError:
        return
    real_abs = os.path.abspath(real_bin)
    if real_abs != mugi_abs and not real_abs.startswith(mugi_abs + os.sep):
        return
    if not _needs_interpreter_codesign_stamp_refresh(real_bin):
        return
    if not _codesign_mugi_interpreter_binary(real_bin):
        return
    try:
        real_bin = os.path.realpath(sys.executable)
        _write_interpreter_codesign_stamp(real_bin)
    except OSError:
        return
    os.execv(sys.executable, [sys.executable] + sys.argv[1:])


def _pid_exists(pid: int) -> bool:
    """Return True if the process with the given PID is still running."""
    if pid <= 0:
        return False
    try:
        os.kill(pid, 0)
        return True
    except OSError:
        return False


def _acquire_setup_lock() -> bool:
    """Acquire setup lock. If another instance holds it and is alive, return False.
    If lock is stale (PID dead), remove and acquire. Returns True on success."""
    os.makedirs(os.path.dirname(_SETUP_LOCK_PATH), exist_ok=True)
    if os.path.isfile(_SETUP_LOCK_PATH):
        try:
            with open(_SETUP_LOCK_PATH, "r", encoding="utf-8") as f:
                pid_str = f.read().strip()
            pid = int(pid_str)
            if _pid_exists(pid):
                return False
        except (ValueError, OSError):
            pass
        try:
            os.remove(_SETUP_LOCK_PATH)
        except OSError:
            pass
    try:
        with open(_SETUP_LOCK_PATH, "w", encoding="utf-8") as f:
            f.write(str(os.getpid()))
        return True
    except OSError:
        return False


def _release_setup_lock() -> None:
    """Release setup lock (remove file)."""
    try:
        if os.path.isfile(_SETUP_LOCK_PATH):
            os.remove(_SETUP_LOCK_PATH)
    except OSError:
        pass


def _running_in_backend_venv() -> bool:
    """True if sys.executable is inside ~/.mugi/venv. Only then do we have fastapi, openai, etc.
    Uses abspath NOT realpath: venv/bin/python3 is a symlink to the base Python (outside the
    venv), so realpath would resolve it away from the venv directory and always return False."""
    if not os.path.isdir(_BACKEND_VENV_DIR):
        return False
    try:
        venv_abs = os.path.abspath(_BACKEND_VENV_DIR) + os.sep
        exe_abs = os.path.abspath(sys.executable)
        return exe_abs.startswith(venv_abs) or exe_abs == os.path.abspath(_BACKEND_VENV_DIR)
    except OSError:
        return False


_SETUP_LOG_PATH = os.path.join(_MUGI_DIR, "logs", "setup.log")


def _setup_log_write(text: str) -> None:
    """Append to the durable first-run log shared with the Swift app.

    First-run venv/pip failures used to live only on stderr (captured transiently by
    the app). Persisting them here makes a failed clean-machine install diagnosable
    and recoverable from ~/.mugi/logs/setup.log regardless of who captured stderr.
    """
    try:
        os.makedirs(os.path.dirname(_SETUP_LOG_PATH), exist_ok=True)
        from datetime import datetime, timezone

        stamp = datetime.now(timezone.utc).isoformat()
        with open(_SETUP_LOG_PATH, "a", encoding="utf-8") as f:
            f.write(f"[{stamp}] {text}\n")
    except OSError:
        pass


def _bootstrap_progress(msg: str) -> None:
    """Print setup progress to stderr so the app can show it during first run."""
    print(f"[mugi] {msg}", file=sys.stderr, flush=True)
    _setup_log_write(f"[backend bootstrap] {msg}")


def _ensure_backend_venv_and_reexec() -> None:
    """If openai (or deps) are missing, create ~/.mugi/venv, pip install -r requirements.txt, re-exec."""
    if not _acquire_setup_lock():
        print("Setup is already in progress. Please wait or quit the other Mugi window.", file=sys.stderr)
        sys.exit(1)
    try:
        _ensure_backend_venv_and_reexec_impl()
    finally:
        _release_setup_lock()


def _ensure_backend_venv_and_reexec_impl() -> None:
    """Implementation of backend venv bootstrap. Caller must hold setup lock."""
    _bootstrap_progress("Starting setup...")
    if sys.platform == "win32":
        venv_python = os.path.join(_BACKEND_VENV_DIR, "Scripts", "python.exe")
    else:
        venv_python = os.path.join(_BACKEND_VENV_DIR, "bin", "python3")
    if not os.path.isfile(venv_python):
        venv_python = os.path.join(_BACKEND_VENV_DIR, "bin", "python")

    already_in_venv = (
        os.path.abspath(sys.executable).startswith(os.path.abspath(_BACKEND_VENV_DIR) + os.sep)
        if os.path.isdir(_BACKEND_VENV_DIR)
        else False
    )
    if already_in_venv:
        print("Backend venv missing 'openai'; run: pip install -r requirements.txt", file=sys.stderr)
        sys.exit(1)

    # Remove existing venv so we start clean (avoids partial/failed state from a previous run).
    if os.path.isdir(_BACKEND_VENV_DIR):
        _bootstrap_progress("Removing old environment...")
        import shutil
        try:
            shutil.rmtree(_BACKEND_VENV_DIR)
        except OSError as e:
            print(f"Could not remove existing venv: {e}", file=sys.stderr)
            sys.exit(1)

    os.makedirs(os.path.dirname(_BACKEND_VENV_DIR), exist_ok=True)
    # Use headless requirements (no PyObjC) so bootstrap works without Xcode/build tools
    req_path = os.path.join(_project_root, "requirements-headless.txt")
    if not os.path.isfile(req_path):
        req_path = os.path.join(_project_root, "requirements.txt")
    if not os.path.isfile(req_path):
        print(f"requirements not found at {req_path!r}", file=sys.stderr)
        sys.exit(1)

    # Use only Mugi-owned Python (never system). App launches us with ~/.mugi/python-3.12 or venv.
    # Never use sys.executable if it points into the venv we just removed (symlink can make
    # realpath differ so we enter bootstrap, but sys.executable still points to deleted path).
    _exec_abs = os.path.abspath(sys.executable)
    _venv_abs = os.path.abspath(_BACKEND_VENV_DIR)
    _mugi_abs = os.path.abspath(_MUGI_DIR)
    _in_venv_dir = _exec_abs == _venv_abs or _exec_abs.startswith(_venv_abs + os.sep)
    _in_mugi = _exec_abs == _mugi_abs or _exec_abs.startswith(_mugi_abs + os.sep)
    if sys.platform == "darwin":
        bootstrap_python = _pick_bootstrap_python_darwin(_in_mugi, _in_venv_dir)
        if not bootstrap_python:
            print(
                "Mugi standalone Python not found under ~/.mugi/python-3.12/. "
                "Open the Mugi app once so it can install Python (offline builds use PythonStandalone.zip). "
                "The backend venv must be created from python-build-standalone only — "
                "Homebrew Python causes macOS to reject PyPI native wheels (e.g. pydantic_core).",
                file=sys.stderr,
            )
            sys.exit(1)
        try:
            _sign = os.path.realpath(bootstrap_python)
        except OSError:
            _sign = bootstrap_python
        _bootstrap_progress("Preparing Python runtime for extensions (codesign)...")
        if _codesign_mugi_interpreter_binary(_sign):
            _write_interpreter_codesign_stamp(_sign)
    else:
        # Non-darwin: self-contained Python not yet implemented; still uses launcher's Python.
        bootstrap_python = sys.executable  # non-darwin
    _bootstrap_progress("Creating virtual environment...")
    venv_ok = False

    def _run_venv(python_exe: str) -> bool:
        try:
            r = subprocess.run(
                [python_exe, "-m", "venv", _BACKEND_VENV_DIR],
                capture_output=True,
                text=True,
                timeout=120,
                cwd=_project_root,
            )
            if r.returncode == 0:
                return True
            err = (r.stderr or r.stdout or "").strip()
            print(f"venv failed: {err}", file=sys.stderr)
            return False
        except FileNotFoundError as e:
            print(f"venv: executable not found: {e}", file=sys.stderr)
            return False
        except subprocess.CalledProcessError as e:
            err = (e.stderr or e.stdout or str(e)).strip()
            print(f"venv failed: {err}", file=sys.stderr)
            return False

    if _run_venv(bootstrap_python):
        venv_ok = True
    elif sys.platform == "darwin":
        # install_only_stripped may lack venv; try virtualenv into the same standalone interpreter.
        _bootstrap_progress("venv not available, trying virtualenv...")
        pip_user_env = {**os.environ, "PYTHONUSERBASE": os.path.join(os.path.expanduser("~"), ".mugi", "pip-user")}
        try:
            subprocess.run(
                [bootstrap_python, "-m", "pip", "install", "--user", "virtualenv"],
                check=True,
                capture_output=True,
                text=True,
                timeout=120,
                cwd=_project_root,
                env=pip_user_env,
            )
            subprocess.run(
                [bootstrap_python, "-m", "virtualenv", _BACKEND_VENV_DIR],
                check=True,
                capture_output=True,
                text=True,
                timeout=120,
                cwd=_project_root,
                env=pip_user_env,
            )
            venv_ok = True
        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            err = getattr(e, "stderr", None) or getattr(e, "stdout", None) or str(e)
            print(f"virtualenv failed: {err}", file=sys.stderr)

    if not venv_ok:
        print(
            "Could not create virtual environment from ~/.mugi/python-3.12. Rebuild PythonStandalone.zip with:\n"
            "  ./scripts/build_python_standalone_zip.sh\n"
            "(Uses install_only variant which includes venv.)",
            file=sys.stderr,
        )
        sys.exit(1)

    if not os.path.isfile(venv_python):
        alt = os.path.join(_BACKEND_VENV_DIR, "bin", "python")
        venv_python = alt if os.path.isfile(alt) else venv_python
    if not os.path.isfile(venv_python):
        print(f"Venv created but Python not found at {venv_python!r}", file=sys.stderr)
        sys.exit(1)

    # Homebrew (poppler, tesseract, ffmpeg, etc.) is a runtime dep for tools — not a pip build dep.
    # PyPI ships pre-built macOS wheels for Pillow, opencv, numpy and every other C-extension in
    # requirements. Skip the Homebrew bootstrap here; it requires Xcode CLT to build formulae
    # from source and blocks first launch for all users who don't have developer tools.
    # Homebrew will be set up lazily the first time a tool that needs it (PDF, OCR, ffmpeg) is used.
    pip_env = os.environ.copy()

    _bootstrap_progress("Installing Python packages (this can take several minutes)...")
    try:
        subprocess.run(
            [venv_python, "-m", "pip", "install", "--upgrade", "pip"],
            check=True,
            capture_output=True,
            text=True,
            timeout=60,
            cwd=_project_root,
            env=pip_env,
        )
    except subprocess.CalledProcessError:
        pass  # continue; older pip may still work
    try:
        subprocess.run(
            # --prefer-binary: always use a pre-built wheel over a source dist.
            # All macOS-relevant packages (Pillow, opencv, numpy, etc.) have wheels on PyPI,
            # so no compiler or system libraries are needed.
            [venv_python, "-m", "pip", "install", "--prefer-binary", "-r", req_path],
            check=True,
            capture_output=True,
            text=True,
            timeout=300,
            cwd=_project_root,
            env=pip_env,
        )
    except subprocess.CalledProcessError as e:
        err = (e.stderr or e.stdout or str(e)).strip()
        print(f"pip install -r requirements failed: {err}", file=sys.stderr)
        _setup_log_write("FAILED: pip install -r requirements failed:")
        _setup_log_write(err)
        sys.exit(1)

    _release_setup_lock()
    _bootstrap_progress("Starting backend...")
    # POSIX: replace this process with the venv interpreter (same PID Swift's Process tracks).
    # subprocess.run kept the bootstrap parent blocked until the child exited — an easy way to
    # accumulate confusing extra interpreters during first-run setup and complicate teardown.
    try:
        os.chdir(_project_root)
    except OSError as e:
        print(f"[mugi] chdir to project root failed: {e}", file=sys.stderr, flush=True)
        sys.exit(1)
    exec_argv = [venv_python, "-u", "-m", "mugi_backend"]
    if sys.platform == "win32":
        result = subprocess.run(exec_argv, cwd=_project_root, env=os.environ.copy())
        sys.exit(result.returncode)
    try:
        os.execve(venv_python, exec_argv, os.environ)
    except OSError as e:
        print(f"[mugi] execve into venv interpreter failed: {e}", file=sys.stderr, flush=True)
        sys.exit(1)


def _sync_backend_venv_packages() -> tuple[bool, str]:
    """Run pip install -r requirements in the existing backend venv (~/.mugi/venv). Does not remove or recreate the venv. Returns (ok, message). Only touches the main backend venv; Kraken and Playwright use separate venvs."""
    if sys.platform == "win32":
        venv_python = os.path.join(_BACKEND_VENV_DIR, "Scripts", "python.exe")
    else:
        venv_python = os.path.join(_BACKEND_VENV_DIR, "bin", "python3")
    if not os.path.isfile(venv_python):
        venv_python = os.path.join(_BACKEND_VENV_DIR, "bin", "python")
    if not os.path.isfile(venv_python):
        return False, "Backend venv not found. Restart the app to create it."
    req_path = os.path.join(_project_root, "requirements-headless.txt")
    if not os.path.isfile(req_path):
        req_path = os.path.join(_project_root, "requirements.txt")
    if not os.path.isfile(req_path):
        return False, "Requirements file not found."
    try:
        r = subprocess.run(
            [venv_python, "-m", "pip", "install", "--prefer-binary", "-r", req_path],
            cwd=_project_root,
            capture_output=True,
            text=True,
            timeout=300,
        )
        if r.returncode != 0:
            err = (r.stderr or r.stdout or "").strip()
            return False, err or "pip install failed."
        return True, "Packages updated successfully."
    except subprocess.TimeoutExpired:
        return False, "Update timed out (5 minutes)."
    except Exception as e:
        return False, str(e)


def _module_available(module_name: str) -> bool:
    try:
        return importlib.util.find_spec(module_name) is not None
    except Exception:
        return False


def _ensure_backend_runtime_deps() -> None:
    """Self-heal stale backend venvs when new runtime deps are introduced."""
    required_modules = [
        "fastapi",
        "openai",
        "pydantic",
        "requests",
        "httpx",
        "cryptography",
        "zeroconf",
        "psutil",
    ]
    missing = [m for m in required_modules if not _module_available(m)]
    if not missing:
        return
    _bootstrap_progress(
        "Detected missing backend packages "
        f"({', '.join(missing[:4])}{'…' if len(missing) > 4 else ''}); syncing environment..."
    )
    ok, msg = _sync_backend_venv_packages()
    if not ok:
        print(f"[mugi] Failed to sync backend packages: {msg}", file=sys.stderr, flush=True)
        sys.exit(1)
    still_missing = [m for m in required_modules if not _module_available(m)]
    if still_missing:
        print(
            "[mugi] Backend packages still missing after sync: "
            + ", ".join(still_missing),
            file=sys.stderr,
            flush=True,
        )
        sys.exit(1)
