"""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,
        }


# ── 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="smart",
        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="smart",
        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="smart",
        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="smart",
        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="smart",
        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="smart",
        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="smart",
        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="smart",
        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="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 ─────────────────────────────────────────────

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

    _instance: ToolRegistry | None = None

    def __init__(self) -> None:
        self._tools: dict[str, ToolDefinition] = {}
        for t in _BUILTINS:
            self._tools[t.name] = copy.copy(t)
        self._apply_env_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) -> ToolRegistry:
        """Return singleton instance."""
        if cls._instance is None:
            cls._instance = cls()
        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 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],
        }
