# tools/cli/venvs.py - Isolated venv management for pip/pipx-installed CLI programs.
# Each CLI tool installed via pip gets its own venv under ~/.mugi/cli-venvs/<name>
# so dependencies never bleed into the main agent venv (~/.mugi/venv) or each other.
# Design mirrors Kraken OCR's venv isolation (see tools/ocr_backends/kraken.py).
from __future__ import annotations

import os
import re
import subprocess
import sys

from persistence import CLI_VENVS_DIR, MUGI_DIR
from tools._logging import _debug_log

_STANDALONE_PYTHON312 = os.path.join(MUGI_DIR, "python-3.12", "bin", "python3")


def _sanitize_name(name: str) -> str:
    """Turn a pip package name into a safe directory name (lowercase, hyphens to underscores)."""
    name = name.strip().lower()
    name = re.sub(r"[^a-z0-9_\-]", "_", name)
    return name


def get_cli_venv_dir(package_name: str) -> str:
    """Return the venv directory path for a named CLI package (not guaranteed to exist)."""
    return os.path.join(CLI_VENVS_DIR, _sanitize_name(package_name))


def get_cli_venv_python(package_name: str) -> str:
    """Return the path to the venv's Python binary (may not exist if venv not created)."""
    venv_dir = get_cli_venv_dir(package_name)
    if sys.platform == "win32":
        return os.path.join(venv_dir, "Scripts", "python.exe")
    return os.path.join(venv_dir, "bin", "python")


def get_cli_venv_bin(package_name: str, binary_name: str | None = None) -> str | None:
    """Return the path to a binary inside a CLI venv's bin directory, or None if not found.
    If binary_name is None, uses package_name as the binary name."""
    venv_dir = get_cli_venv_dir(package_name)
    bin_name = binary_name or _sanitize_name(package_name).replace("_", "-")
    if sys.platform == "win32":
        candidate = os.path.join(venv_dir, "Scripts", bin_name + ".exe")
    else:
        candidate = os.path.join(venv_dir, "bin", bin_name)
    if os.path.isfile(candidate):
        return candidate
    return None


def _bootstrap_python() -> str:
    """Return the best Python to use for creating CLI venvs.
    Prefers the current interpreter (already in a managed venv), then the standalone 3.12."""
    if os.path.isfile(sys.executable):
        return sys.executable
    if os.path.isfile(_STANDALONE_PYTHON312):
        return _STANDALONE_PYTHON312
    return "python3"


def ensure_cli_venv(package_name: str, extra_packages: list[str] | None = None) -> tuple[bool, str]:
    """Create (if needed) and populate a CLI venv for a pip package.

    Creates ~/.mugi/cli-venvs/<sanitized_name>/, then pip-installs package_name
    (and any extra_packages). Returns (success, message).

    Uses the same isolation principle as Kraken OCR: each program's dependencies
    are completely separate from the main agent venv (~/.mugi/venv).
    """
    venv_dir = get_cli_venv_dir(package_name)
    venv_python = get_cli_venv_python(package_name)
    os.makedirs(CLI_VENVS_DIR, exist_ok=True)

    if not os.path.isfile(venv_python):
        _debug_log(f"[cli_venvs] Creating venv for {package_name!r} at {venv_dir}", "INFO")
        py = _bootstrap_python()
        try:
            r = subprocess.run(
                [py, "-m", "venv", venv_dir],
                capture_output=True,
                text=True,
                timeout=120,
            )
            if r.returncode != 0:
                err = (r.stderr or r.stdout or "").strip()
                return False, f"Failed to create venv for {package_name}: {err}"
        except Exception as e:
            return False, f"Venv creation error for {package_name}: {e}"

    packages = [package_name] + (extra_packages or [])
    _debug_log(f"[cli_venvs] pip installing {packages} into {venv_dir}", "INFO")
    try:
        r = subprocess.run(
            [venv_python, "-m", "pip", "install", "--upgrade"] + packages,
            capture_output=True,
            text=True,
            timeout=300,
        )
        if r.returncode != 0:
            err = (r.stderr or r.stdout or "").strip()
            return False, f"pip install failed for {package_name}: {err[:500]}"
    except Exception as e:
        return False, f"pip install error for {package_name}: {e}"

    return True, f"{package_name} installed in isolated venv at {venv_dir}."


def remove_cli_venv(package_name: str) -> tuple[bool, str]:
    """Remove the isolated venv for a CLI package. Returns (success, message)."""
    import shutil
    venv_dir = get_cli_venv_dir(package_name)
    if not os.path.isdir(venv_dir):
        return True, f"No venv found for {package_name!r}."
    try:
        shutil.rmtree(venv_dir)
        return True, f"Removed CLI venv for {package_name!r}."
    except Exception as e:
        return False, f"Failed to remove CLI venv for {package_name!r}: {e}"


def list_cli_venvs() -> list[str]:
    """Return names of all installed CLI venvs under CLI_VENVS_DIR."""
    if not os.path.isdir(CLI_VENVS_DIR):
        return []
    return sorted(
        d for d in os.listdir(CLI_VENVS_DIR)
        if os.path.isdir(os.path.join(CLI_VENVS_DIR, d))
    )
