"""Tool registry — manage AI development tools (aider, claude-code, windsurf, etc.).

Each tool is described by a ToolDefinition and can be discovered, configured,
and dispatched to via the ToolRegistry singleton.
"""

from __future__ import annotations

import copy
import os
import shutil
import subprocess
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any

import structlog

logger = structlog.get_logger()


class ToolCategory(str, Enum):
    """High-level tool categories."""
    IDE = "ide"
    AGENT_CLI = "agent_cli"
    BROWSER = "browser"
    CUSTOM = "custom"
    HUMAN = "human"


class ToolCapability(str, Enum):
    """Capabilities a tool can advertise."""
    CODE_EDIT = "code_edit"
    CODE_REVIEW = "code_review"
    TEST_RUN = "test_run"
    CHAT = "chat"
    FILE_CREATE = "file_create"
    REFACTOR = "refactor"
    DEBUG = "debug"
    ARCHITECT = "architect"
    MANUAL = "manual"


@dataclass
class ToolInstance:
    """Concrete runtime instance for a registered tool."""
    name: str
    kind: str  # "host", "docker", "kvm", "vm"
    command: list[str] = field(default_factory=list)
    service: str = ""
    profile: str = ""
    container_name: str = ""
    vm_name: str = ""           # KVM/QEMU VM name
    vm_host: str = ""           # VM host/IP for SSH
    vm_user: str = ""           # SSH user for VM
    vm_ssh_port: int = 22       # SSH port for VM
    description: str = ""
    notes: str = ""
    force_available: bool | None = None

    @property
    def is_available(self) -> bool:
        if self.force_available is not None:
            return self.force_available
        if self.kind == "docker":
            return shutil.which("docker") is not None
        if self.kind in ("kvm", "vm"):
            return self._check_vm_available()
        if self.command:
            return shutil.which(self.command[0]) is not None
        return False

    def _check_vm_available(self) -> bool:
        """Check if KVM/VM instance is available via SSH or virsh."""
        # Check if we can connect via SSH to the VM
        if self.vm_host and self.vm_user:
            try:
                result = subprocess.run(
                    ["ssh", "-o", "ConnectTimeout=2", "-o", "BatchMode=yes",
                     f"{self.vm_user}@{self.vm_host}", "echo", "available"],
                    capture_output=True,
                    timeout=5,
                )
                return result.returncode == 0 and b"available" in result.stdout
            except (subprocess.TimeoutExpired, FileNotFoundError):
                pass
        # Check via virsh if VM name is provided and host has libvirt
        if self.vm_name and shutil.which("virsh"):
            try:
                result = subprocess.run(
                    ["virsh", "domstate", self.vm_name],
                    capture_output=True,
                    timeout=3,
                )
                return result.returncode == 0 and b"running" in result.stdout.lower()
            except (subprocess.TimeoutExpired, FileNotFoundError):
                pass
        return False

    def summary(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "kind": self.kind,
            "command": self.command,
            "service": self.service,
            "profile": self.profile,
            "container_name": self.container_name,
            "vm_name": self.vm_name,
            "vm_host": self.vm_host,
            "vm_user": self.vm_user,
            "vm_ssh_port": self.vm_ssh_port,
            "description": self.description,
            "notes": self.notes,
            "available": self.is_available,
        }


@dataclass
class ToolDefinition:
    """Describes a registered development tool."""
    name: str
    category: ToolCategory
    binary: str = ""                                # e.g. "aider", "claude"
    capabilities: list[ToolCapability] = field(default_factory=list)
    default_model: str = ""                         # alias like "smart" or full id
    modes: list[str] = field(default_factory=list)  # e.g. ["code", "architect", "ask"]
    config: dict[str, Any] = field(default_factory=dict)
    instances: list[ToolInstance] = field(default_factory=list)
    default_instance: str = "host"
    description: str = ""
    force_available: bool | None = None

    @property
    def is_available(self) -> bool:
        """Check tool availability.

        Uses *force_available* override first (set via PROXYM_TOOLS_AVAILABLE
        env var for containerised deployments), then falls back to binary
        detection on PATH and docker-backed instances.
        """
        if self.force_available is not None:
            return self.force_available
        if self.binary and shutil.which(self.binary) is not None:
            return True
        return any(instance.is_available for instance in self.instances)

    def summary(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "category": self.category.value,
            "binary": self.binary,
            "available": self.is_available,
            "capabilities": [c.value for c in self.capabilities],
            "modes": self.modes,
            "default_model": self.default_model,
            "default_instance": self.default_instance,
            "instances": [i.summary() for i in self.instances],
            "description": self.description,
        }

    def get_instance(self, instance_name: str) -> ToolInstance | None:
        instance_name = (instance_name or "").strip()
        if not instance_name:
            return None
        for instance in self.instances:
            if instance.name == instance_name:
                return instance
        return None


# ── Built-in tool definitions ────────────────────────────


def _host_instance(binary: str, description: str) -> ToolInstance:
    return ToolInstance(
        name="host",
        kind="host",
        command=[binary] if binary else [],
        description=description,
    )


def _docker_instance(
    *,
    service: str,
    profile: str,
    command: list[str],
    description: str,
    container_name: str = "",
    notes: str = "",
) -> ToolInstance:
    return ToolInstance(
        name="docker",
        kind="docker",
        command=command,
        service=service,
        profile=profile,
        container_name=container_name,
        description=description,
        notes=notes,
    )

def _kvm_instance(
    *,
    vm_name: str = "",
    vm_host: str = "",
    vm_user: str = "",
    vm_ssh_port: int = 22,
    command: list[str] | None = None,
    description: str = "",
    notes: str = "",
) -> ToolInstance:
    return ToolInstance(
        name="kvm",
        kind="kvm",
        command=command or [],
        vm_name=vm_name,
        vm_host=vm_host,
        vm_user=vm_user,
        vm_ssh_port=vm_ssh_port,
        description=description,
        notes=notes,
    )

_BUILTINS: list[ToolDefinition] = [
    ToolDefinition(
        name="aider",
        category=ToolCategory.AGENT_CLI,
        binary="aider",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.REFACTOR,
            ToolCapability.TEST_RUN, ToolCapability.CHAT,
        ],
        default_model="balanced",
        modes=["code", "architect", "ask"],
        instances=[
            _host_instance("aider", "Run the aider CLI directly on the host."),
            _docker_instance(
                service="aider",
                profile="tools",
                command=["docker", "compose", "--profile", "tools", "run", "--rm", "aider"],
                container_name="proxym-aider",
                description="Run aider as a docker-backed service instance.",
                notes="Compose service placeholder: add a matching 'aider' service/image when you want container execution.",
            ),
        ],
        description="AI pair programming in the terminal",
    ),
    ToolDefinition(
        name="claude-code",
        category=ToolCategory.AGENT_CLI,
        binary="claude",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.REFACTOR,
            ToolCapability.FILE_CREATE, ToolCapability.CHAT,
            ToolCapability.DEBUG, ToolCapability.ARCHITECT,
        ],
        default_model="balanced",
        modes=["code", "architect", "debug", "ask"],
        instances=[
            _host_instance("claude", "Run the Claude Code CLI directly on the host."),
            _docker_instance(
                service="claude-code",
                profile="tools",
                command=["docker", "compose", "--profile", "tools", "run", "--rm", "claude-code"],
                container_name="proxym-claude-code",
                description="Run Claude Code as a docker-backed service instance.",
                notes="Compose service placeholder: add a matching 'claude-code' service/image when you want container execution.",
            ),
        ],
        description="Anthropic Claude Code CLI agent",
    ),
    ToolDefinition(
        name="windsurf",
        category=ToolCategory.IDE,
        binary="windsurf",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.REFACTOR,
            ToolCapability.FILE_CREATE, ToolCapability.CHAT,
        ],
        default_model="balanced",
        modes=["code", "architect"],
        instances=[
            _host_instance("windsurf", "Launch the Windsurf desktop app on the host."),
            _docker_instance(
                service="windsurf",
                profile="tools",
                command=["docker", "compose", "--profile", "tools", "run", "--rm", "windsurf"],
                container_name="proxym-windsurf",
                description="Docker service descriptor for a Windsurf-compatible instance.",
                notes="Placeholder command for a future Windsurf service image or wrapper container.",
            ),
        ],
        description="Codeium Windsurf IDE with Cascade agent",
    ),
    ToolDefinition(
        name="cursor",
        category=ToolCategory.IDE,
        binary="cursor",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.REFACTOR,
            ToolCapability.FILE_CREATE, ToolCapability.CHAT,
        ],
        default_model="balanced",
        modes=["code", "architect"],
        instances=[
            _host_instance("cursor", "Launch the Cursor desktop app on the host."),
            _docker_instance(
                service="cursor",
                profile="tools",
                command=["docker", "compose", "--profile", "tools", "run", "--rm", "cursor"],
                container_name="proxym-cursor",
                description="Docker service descriptor for a Cursor-compatible instance.",
                notes="Placeholder command for a future Cursor service image or wrapper container.",
            ),
        ],
        description="Cursor IDE with Composer agent",
    ),
    ToolDefinition(
        name="vscode",
        category=ToolCategory.IDE,
        binary="code",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.CHAT,
        ],
        default_model="balanced",
        modes=["code"],
        default_instance="docker",
        instances=[
            _host_instance("code", "Open VS Code on the host."),
            _docker_instance(
                service="vscode",
                profile="vscode",
                command=["docker", "compose", "--profile", "vscode", "up", "-d", "vscode"],
                container_name="proxym-vscode",
                description="VS Code Web running as the proxym Docker service.",
                notes="This is the primary container-backed instance used by Roo Code and Cline.",
            ),
        ],
        description="VS Code with Roo Code / Continue extension",
    ),
    ToolDefinition(
        name="roo-code",
        category=ToolCategory.IDE,
        binary="code",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.REFACTOR,
            ToolCapability.CHAT, ToolCapability.DEBUG,
        ],
        default_model="balanced",
        modes=["code", "architect", "debug", "ask"],
        default_instance="docker",
        instances=[
            _host_instance("code", "Use the local VS Code binary if available."),
            _docker_instance(
                service="vscode",
                profile="vscode",
                command=["docker", "compose", "--profile", "vscode", "up", "-d", "vscode"],
                container_name="proxym-vscode",
                description="Roo Code running inside the proxym VS Code Web container.",
                notes="Shares the same docker service as the main VS Code instance.",
            ),
        ],
        description="Roo Code extension in VS Code Web",
    ),
    ToolDefinition(
        name="cline",
        category=ToolCategory.IDE,
        binary="code",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.CHAT,
        ],
        default_model="balanced",
        modes=["code"],
        default_instance="docker",
        instances=[
            _host_instance("code", "Use the local VS Code binary if available."),
            _docker_instance(
                service="vscode",
                profile="vscode",
                command=["docker", "compose", "--profile", "vscode", "up", "-d", "vscode"],
                container_name="proxym-vscode",
                description="Cline running inside the proxym VS Code Web container.",
                notes="Shares the same docker service as the main VS Code instance.",
            ),
        ],
        description="Cline extension in VS Code",
    ),
    ToolDefinition(
        name="browser",
        category=ToolCategory.BROWSER,
        binary="",
        capabilities=[ToolCapability.CHAT],
        default_model="balanced",
        modes=[],
        default_instance="docker",
        instances=[
            _docker_instance(
                service="open-webui",
                profile="webui",
                command=["docker", "compose", "--profile", "webui", "up", "-d", "open-webui"],
                container_name="proxym-open-webui",
                description="Browser-based chat service backed by Open WebUI.",
                notes="This is the docker-native browser/chat instance shipped with proxym.",
            ),
        ],
        description="Browser-based chat (Open WebUI, ChatGPT, etc.)",
    ),
    ToolDefinition(
        name="codex",
        category=ToolCategory.AGENT_CLI,
        binary="codex",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.REFACTOR,
            ToolCapability.FILE_CREATE, ToolCapability.CHAT,
        ],
        default_model="balanced",
        modes=["code", "ask"],
        instances=[
            _host_instance("codex", "Run the OpenAI Codex CLI directly on the host."),
        ],
        description="OpenAI Codex CLI — agentive terminal assistant",
    ),
    ToolDefinition(
        name="goose",
        category=ToolCategory.AGENT_CLI,
        binary="goose",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.FILE_CREATE,
            ToolCapability.REFACTOR, ToolCapability.CHAT,
        ],
        default_model="balanced",
        modes=["code", "ask"],
        instances=[
            _host_instance("goose", "Run Goose CLI directly on the host."),
        ],
        description="Goose — autonomous coding assistant by Block",
    ),
    ToolDefinition(
        name="open-interpreter",
        category=ToolCategory.AGENT_CLI,
        binary="interpreter",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.FILE_CREATE,
            ToolCapability.TEST_RUN, ToolCapability.CHAT,
        ],
        default_model="balanced",
        modes=["code", "ask"],
        instances=[
            _host_instance("interpreter", "Run Open Interpreter CLI on the host."),
        ],
        description="Open Interpreter — agent executing code and shell commands",
    ),
    ToolDefinition(
        name="plandex",
        category=ToolCategory.AGENT_CLI,
        binary="plandex",
        capabilities=[
            ToolCapability.CODE_EDIT, ToolCapability.FILE_CREATE,
            ToolCapability.ARCHITECT, ToolCapability.REFACTOR,
        ],
        default_model="balanced",
        modes=["code", "architect"],
        instances=[
            _host_instance("plandex", "Run Plandex CLI on the host."),
        ],
        description="Plandex — multi-file planning and coding agent",
    ),
    ToolDefinition(
        name="llm-cli",
        category=ToolCategory.AGENT_CLI,
        binary="llm",
        capabilities=[
            ToolCapability.CHAT, ToolCapability.CODE_REVIEW,
        ],
        default_model="balanced",
        modes=["ask"],
        instances=[
            _host_instance("llm", "Run llm CLI (Simon Willison) on the host."),
        ],
        description="llm CLI — universal LLM client, pipe-friendly",
    ),
    ToolDefinition(
        name="human",
        category=ToolCategory.HUMAN,
        binary="",
        capabilities=[ToolCapability.MANUAL],
        default_instance="manual",
        force_available=True,
        description="Human-in-the-loop collaborator for tokens, installs, approvals, clicking, and onboarding steps",
    ),
]


# ── Registry ─────────────────────────────────────────────

_TOOL_OVERRIDES_NAMESPACE = "tool_overrides"
_TOOL_OVERRIDE_FIELDS = {"default_model", "default_instance", "description", "force_available"}


class ToolRegistry:
    """Singleton registry of available development tools."""

    _instance: ToolRegistry | None = None

    def __init__(self, persist_path: Path | None = None) -> None:
        self._persist_path = persist_path
        self._store: Any = None
        if self._persist_path is not None:
            from proxym.storage import SQLiteNamespaceStore
            self._store = SQLiteNamespaceStore(self._persist_path)
        self._tools: dict[str, ToolDefinition] = {}
        for t in _BUILTINS:
            self._tools[t.name] = copy.copy(t)
        self._apply_env_overrides()
        self._load_overrides()
        logger.info("tool_registry_init", count=len(self._tools),
                    available=len(self.list_available()))

    def _apply_env_overrides(self) -> None:
        """Mark tools available via PROXYM_TOOLS_AVAILABLE env var.

        Value: comma-separated tool names, or ``*`` for all.
        Useful when the server runs inside a container but dispatches
        to host-side binaries.
        """
        raw = os.environ.get("PROXYM_TOOLS_AVAILABLE", "").strip()
        if not raw:
            return
        if raw == "*":
            names = set(self._tools.keys())
        else:
            names = {n.strip() for n in raw.split(",") if n.strip()}
        for name in names:
            tool = self._tools.get(name)
            if tool:
                tool.force_available = True
                logger.debug("tool_force_available", name=name)

    @classmethod
    def get(cls, persist_path: Path | None = None) -> ToolRegistry:
        """Return singleton instance."""
        if cls._instance is None:
            if persist_path is None:
                from proxym.storage import DEFAULT_SQLITE_PATH
                persist_path = DEFAULT_SQLITE_PATH
            cls._instance = cls(persist_path)
        return cls._instance

    def register(self, tool: ToolDefinition) -> None:
        """Register or override a tool definition."""
        self._tools[tool.name] = tool
        logger.info("tool_registered", name=tool.name)

    def unregister(self, name: str) -> bool:
        """Remove a tool. Returns False if not found."""
        if name in self._tools:
            del self._tools[name]
            return True
        return False

    def get_tool(self, name: str) -> ToolDefinition | None:
        return self._tools.get(name)

    def list_tools(
        self,
        category: ToolCategory | None = None,
        available_only: bool = False,
    ) -> list[ToolDefinition]:
        tools = list(self._tools.values())
        if category:
            tools = [t for t in tools if t.category == category]
        if available_only:
            tools = [t for t in tools if t.is_available]
        return sorted(tools, key=lambda t: t.name)

    def update_tool(self, name: str, updates: dict[str, Any]) -> ToolDefinition | None:
        """Update mutable fields of a tool definition and persist. Returns updated tool or None."""
        tool = self._tools.get(name)
        if not tool:
            return None
        persisted_keys = []
        for key, value in updates.items():
            if key in _TOOL_OVERRIDE_FIELDS and hasattr(tool, key):
                setattr(tool, key, value)
                persisted_keys.append(key)
        if persisted_keys:
            self._save_overrides()
        logger.info("tool_updated", name=name, updates=persisted_keys)
        return tool

    # ── Persistence ───────────────────────────────────────────

    def _load_overrides(self) -> None:
        """Load user overrides from SQLite and apply on top of builtins."""
        if self._store is None:
            return
        try:
            data = self._store.load_namespace(_TOOL_OVERRIDES_NAMESPACE)
            applied = 0
            for tool_name, overrides in data.items():
                tool = self._tools.get(tool_name)
                if not tool or not isinstance(overrides, dict):
                    continue
                for key, value in overrides.items():
                    if key in _TOOL_OVERRIDE_FIELDS and hasattr(tool, key):
                        setattr(tool, key, value)
                        applied += 1
            if applied:
                logger.info("tool_overrides_loaded", count=applied)
        except Exception as exc:
            logger.warning("tool_overrides_load_failed", error=str(exc))

    def _save_overrides(self) -> None:
        """Persist only user-changed fields (diff vs builtins) to SQLite."""
        if self._persist_path is None:
            return
        if self._store is None:
            from proxym.storage import SQLiteNamespaceStore
            self._store = SQLiteNamespaceStore(self._persist_path)
        builtin_map = {t.name: t for t in _BUILTINS}
        overrides: dict[str, dict[str, Any]] = {}
        for name, tool in self._tools.items():
            builtin = builtin_map.get(name)
            diff: dict[str, Any] = {}
            for field_name in _TOOL_OVERRIDE_FIELDS:
                current = getattr(tool, field_name, None)
                default = getattr(builtin, field_name, None) if builtin else None
                if current != default:
                    diff[field_name] = current
            if diff:
                overrides[name] = diff
        self._store.save_namespace(_TOOL_OVERRIDES_NAMESPACE, overrides)

    def list_available(self) -> list[ToolDefinition]:
        return self.list_tools(available_only=True)

    def summary(self) -> dict[str, Any]:
        tools = self.list_tools()
        available = [t for t in tools if t.is_available]
        return {
            "total": len(tools),
            "available": len(available),
            "tools": [t.summary() for t in tools],
        }
