"""Dependencies step - installs required tools and packages."""

from __future__ import annotations

import subprocess
from pathlib import Path
from typing import Any

from installer.context import InstallContext
from installer.platform_utils import command_exists
from installer.steps.base import BaseStep
from installer.steps.deps_claude import (  # noqa: F401
    _clean_mcp_servers_from_claude_config,
    _clean_npm_stale_dirs,
    _configure_claude_defaults,
    _get_forced_claude_version,
    _get_installed_claude_version,
    _patch_claude_config,
    _patch_claude_settings,
    _remove_native_claude_binaries,
    install_claude_code,
)
from installer.steps.deps_npm import (  # noqa: F401
    _extract_npx_package_name,
    _is_ccusage_installed,
    _is_npx_package_cached,
    _is_vtsls_installed,
    _kill_proc,
    _precache_npx_mcp_servers,
    install_ccusage,
    install_mcp_cli,
    install_sx,
    install_typescript_lsp,
    update_sx,
)
from installer.steps.deps_playwright import (  # noqa: F401
    _get_playwright_cache_dirs,
    _install_playwright_system_deps,
    _is_playwright_cli_ready,
    install_playwright_cli,
)
from installer.steps.deps_ui import (  # noqa: F401
    _install_claude_code_with_ui,
    _install_playwright_cli_with_ui,
    _install_vexor_with_ui,
    _install_with_spinner,
)
from installer.steps.deps_utils import _run_bash_with_retry  # noqa: F401
from installer.steps.deps_vexor import (  # noqa: F401
    _configure_vexor_defaults,
    _configure_vexor_local,
    _is_vexor_local_model_installed,
    _setup_vexor_local_model,
    install_vexor,
)


def _get_nvm_source_cmd() -> str:
    """Get the command to source NVM for nvm-specific commands.

    Only needed for `nvm install`, `nvm use`, etc. - not for npm/node/claude.
    """
    nvm_locations = [
        Path.home() / ".nvm" / "nvm.sh",
        Path("/usr/local/share/nvm/nvm.sh"),
    ]

    for nvm_path in nvm_locations:
        if nvm_path.exists():
            return f"source {nvm_path} && "

    return ""


def install_nodejs() -> bool:
    """Install Node.js via NVM if not present."""
    if command_exists("node"):
        return True

    nvm_dir = Path.home() / ".nvm"
    if not nvm_dir.exists():
        if not _run_bash_with_retry("curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash"):
            return False

    nvm_src = _get_nvm_source_cmd()
    return _run_bash_with_retry(f"{nvm_src}nvm install 22 && nvm use 22")


def install_uv() -> bool:
    """Install uv package manager if not present."""
    if command_exists("uv"):
        return True

    return _run_bash_with_retry("curl -LsSf https://astral.sh/uv/install.sh | sh")


def install_python_tools() -> bool:
    """Install Python development tools."""
    tools = ["ruff", "basedpyright"]

    try:
        for tool in tools:
            if not command_exists(tool):
                subprocess.run(
                    ["uv", "tool", "install", tool],
                    check=True,
                    capture_output=True,
                )
        return True
    except subprocess.CalledProcessError:
        return False


def _ensure_launcher_binary(ui: Any = None) -> bool:
    """Create ~/.tribunal/bin/tribunal when the pre-built binary is missing.

    The pre-built binary is normally downloaded by install.sh from GitHub
    Releases.  When that download fails (e.g. no release for the current
    version, network issues, or running the Python installer directly), this
    function creates a lightweight shell wrapper that replicates the essential
    launcher behaviour: setting CLAUDE_PLUGIN_DIR and SKILLFIELD_HOME, then
    exec-ing into the ``claude`` CLI.
    """
    bin_dir = Path.home() / ".tribunal" / "bin"
    wrapper_path = bin_dir / "tribunal"

    if wrapper_path.exists():
        return True

    bin_dir.mkdir(parents=True, exist_ok=True)

    wrapper_content = """\
#!/bin/bash
# Tribunal launcher (source fallback)
# Created by the installer when the pre-built binary is unavailable.

export CLAUDE_PLUGIN_DIR="$HOME/.claude/tribunal"
export SKILLFIELD_HOME="${SKILLFIELD_HOME:-$HOME/.tribunal}"

# Load extra env from config.json if present
# Pass the path via environment variable so it is never interpolated into
# the Python code string (which would allow injection if SKILLFIELD_HOME
# contained shell metacharacters like single-quotes).
CONFIG_FILE="$SKILLFIELD_HOME/config.json"
if command -v python3 >/dev/null 2>&1 && [ -f "$CONFIG_FILE" ]; then
    eval "$(SF_CONFIG_FILE="$CONFIG_FILE" python3 -c "
import json, os, re
try:
    cfg = json.load(open(os.environ['SF_CONFIG_FILE']))
    for k, v in cfg.get('env', {}).items():
        if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', k):
            print(f'export {k}={v!r}')
except Exception:
    pass
" 2>/dev/null)" 2>/dev/null || true
fi

# Route all invocations through the Python CLI so the startup banner,
# access verification, and env setup always run.
if [ -x "$SKILLFIELD_HOME/.venv/bin/python3" ]; then
    exec "$SKILLFIELD_HOME/.venv/bin/python3" -m launcher "$@"
elif command -v uv >/dev/null 2>&1; then
    exec uv run python -m launcher "$@"
elif command -v python3 >/dev/null 2>&1; then
    exec python3 -m launcher "$@"
else
    echo "Error: python3 not found. Install Python 3.12+." >&2
    exit 1
fi
"""
    try:
        wrapper_path.write_text(wrapper_content)
        wrapper_path.chmod(0o755)
        if ui:
            ui.success("Created launcher binary (shell fallback)")
        return True
    except (OSError, IOError) as e:
        if ui:
            ui.warning(f"Could not create launcher binary: {e}")
        return False


def _install_plugin_dependencies(_project_dir: Path, ui: Any = None) -> bool:
    """Install plugin dependencies by running bun/npm install in the plugin folder.

    This installs all Node.js dependencies defined in plugin/package.json,
    which includes runtime dependencies for MCP servers and hooks.
    """
    plugin_dir = Path.home() / ".claude" / "tribunal"

    if not plugin_dir.exists():
        if ui:
            ui.warning("Plugin directory not found - skipping plugin dependencies")
        return False

    package_json = plugin_dir / "package.json"
    if not package_json.exists():
        if ui:
            ui.warning("No package.json in plugin directory - skipping")
        return False

    success = False

    if command_exists("bun"):
        if _run_bash_with_retry("bun install", cwd=plugin_dir):
            success = True

    return success


def _setup_sf_memory(ui: Any) -> bool:
    """Setup sf-memory (no-op, kept for compatibility)."""
    return True


class DependenciesStep(BaseStep):
    """Step that installs all required dependencies."""

    name = "dependencies"

    def check(self, ctx: InstallContext) -> bool:
        """Always returns False - dependencies should always be checked."""
        return False

    def run(self, ctx: InstallContext) -> None:
        """Install all required dependencies."""
        ui = ctx.ui
        installed: list[str] = []

        if _install_with_spinner(ui, "Node.js", install_nodejs):
            installed.append("nodejs")

        if _install_with_spinner(ui, "uv", install_uv):
            installed.append("uv")

        if ctx.enable_python:
            if _install_with_spinner(ui, "Python tools", install_python_tools):
                installed.append("python_tools")

        if _install_claude_code_with_ui(ui, ctx.project_dir):
            installed.append("claude_code")

        if _setup_sf_memory(ui):
            installed.append("sf_memory")

        if _install_with_spinner(ui, "Plugin dependencies", _install_plugin_dependencies, ctx.project_dir, ui):
            installed.append("plugin_deps")

        if _install_with_spinner(ui, "mcp-cli", install_mcp_cli):
            installed.append("mcp_cli")

        if _install_with_spinner(ui, "vtsls (TypeScript LSP server)", install_typescript_lsp):
            installed.append("typescript_lsp")

        if _install_playwright_cli_with_ui(ui):
            installed.append("playwright_cli")

        if _install_vexor_with_ui(ui):
            installed.append("vexor")

        if _install_with_spinner(ui, "ccusage (usage tracking)", install_ccusage):
            installed.append("ccusage")

        if _install_with_spinner(ui, "sx (team assets)", install_sx):
            installed.append("sx")
            _install_with_spinner(ui, "sx update", update_sx)

        if _install_with_spinner(ui, "MCP server packages", _precache_npx_mcp_servers, ui):
            installed.append("mcp_npx_cache")

        if _ensure_launcher_binary(ui):
            installed.append("launcher_binary")

        _clean_mcp_servers_from_claude_config(ui)

        ctx.config["installed_dependencies"] = installed
