"""Tool adapters — launch and interact with AI dev tools.

Each adapter knows how to:
  1. Build CLI arguments from a prompt + config
  2. Launch the tool process
  3. Stream/capture output
  4. Report completion status
"""

from __future__ import annotations

import asyncio
import shutil
import subprocess
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import structlog

from proxym.tools import ToolDefinition, ToolRegistry
from proxym.tools.prompt_builder import build_prompt

logger = structlog.get_logger()


@dataclass
class ToolResult:
    """Result of a tool execution."""
    tool: str
    ticket_id: str
    exit_code: int = -1
    stdout: str = ""
    stderr: str = ""
    duration_s: float = 0
    files_changed: list[str] = field(default_factory=list)

    @property
    def success(self) -> bool:
        return self.exit_code == 0

    def summary(self) -> dict[str, Any]:
        return {
            "tool": self.tool,
            "ticket_id": self.ticket_id,
            "success": self.success,
            "exit_code": self.exit_code,
            "duration_s": round(self.duration_s, 2),
            "files_changed": self.files_changed,
            "stdout_lines": len(self.stdout.splitlines()),
            "stderr_lines": len(self.stderr.splitlines()),
        }


class BaseAdapter(ABC):
    """Base class for tool adapters."""

    def __init__(self, tool: ToolDefinition) -> None:
        self.tool = tool

    @abstractmethod
    def build_args(
        self, prompt: str, project_dir: str, mode: str = "", model: str = "",
        files: list[str] | None = None,
    ) -> list[str]:
        """Build CLI argument list."""
        ...

    async def run(
        self,
        prompt: str,
        project_dir: str,
        ticket_id: str = "",
        mode: str = "",
        model: str = "",
        files: list[str] | None = None,
        timeout: float = 300,
    ) -> ToolResult:
        """Launch the tool and wait for completion."""
        import time
        args = self.build_args(prompt, project_dir, mode, model, files)
        binary = shutil.which(self.tool.binary)
        if not binary:
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=127, stderr=f"{self.tool.binary} not found on PATH",
            )

        logger.info("tool_adapter_run", tool=self.tool.name, args=args[:3], cwd=project_dir)
        t0 = time.monotonic()
        try:
            proc = await asyncio.create_subprocess_exec(
                *args,
                cwd=project_dir,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout_b, stderr_b = await asyncio.wait_for(
                proc.communicate(), timeout=timeout,
            )
            duration = time.monotonic() - t0
            result = ToolResult(
                tool=self.tool.name,
                ticket_id=ticket_id,
                exit_code=proc.returncode or 0,
                stdout=stdout_b.decode(errors="replace"),
                stderr=stderr_b.decode(errors="replace"),
                duration_s=duration,
            )
            logger.info("tool_adapter_done", tool=self.tool.name,
                        exit_code=result.exit_code, duration=f"{duration:.1f}s")
            return result
        except asyncio.TimeoutError:
            duration = time.monotonic() - t0
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=-1, stderr=f"Timeout after {timeout}s",
                duration_s=duration,
            )
        except Exception as exc:
            duration = time.monotonic() - t0
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=-1, stderr=str(exc), duration_s=duration,
            )


class AiderAdapter(BaseAdapter):
    """Adapter for aider CLI."""

    def build_args(
        self, prompt: str, project_dir: str, mode: str = "", model: str = "",
        files: list[str] | None = None,
    ) -> list[str]:
        args = ["aider", "--yes-always", "--no-auto-commits"]
        if mode == "architect":
            args.append("--architect")
        if model:
            args.extend(["--model", model])
        if files:
            for f in files:
                args.append(f)
        args.extend(["--message", prompt])
        return args


class ClaudeCodeAdapter(BaseAdapter):
    """Adapter for claude CLI (Anthropic Claude Code)."""

    def build_args(
        self, prompt: str, project_dir: str, mode: str = "", model: str = "",
        files: list[str] | None = None,
    ) -> list[str]:
        args = ["claude", "--print", "--dangerously-skip-permissions"]
        if model:
            args.extend(["--model", model])
        args.append(prompt)
        return args


class VSCodeAdapter(BaseAdapter):
    """Adapter for VS Code Web / code-server (starts container, sends task via API)."""

    def build_args(
        self, prompt: str, project_dir: str, mode: str = "", model: str = "",
        files: list[str] | None = None,
    ) -> list[str]:
        # VS Code Web is started via docker compose, not a direct CLI call
        return ["docker", "compose", "--profile", "vscode", "up", "-d", "vscode"]

    async def run(
        self,
        prompt: str,
        project_dir: str,
        ticket_id: str = "",
        mode: str = "",
        model: str = "",
        files: list[str] | None = None,
        timeout: float = 120,
    ) -> ToolResult:
        """Start VS Code Web container and verify it's running."""
        import time

        logger.info("vscode_adapter_run", ticket_id=ticket_id, project_dir=project_dir)
        t0 = time.monotonic()

        # Check docker is available before attempting
        if not shutil.which("docker"):
            duration = time.monotonic() - t0
            logger.warning("vscode_adapter_no_docker", ticket_id=ticket_id)
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=127,
                stderr=(
                    "docker not found on PATH. VS Code Web requires docker compose. "
                    "Run from host: docker compose --profile vscode up -d"
                ),
                duration_s=duration,
            )

        # Find docker-compose.yml directory
        compose_dir = self._find_compose_dir(project_dir)
        args = self.build_args(prompt, project_dir, mode, model, files)

        try:
            proc = await asyncio.create_subprocess_exec(
                *args,
                cwd=compose_dir,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout_b, stderr_b = await asyncio.wait_for(
                proc.communicate(), timeout=timeout,
            )
            duration = time.monotonic() - t0
            stdout = stdout_b.decode(errors="replace")
            stderr = stderr_b.decode(errors="replace")

            if proc.returncode == 0:
                stdout += f"\n[proxym] VS Code Web started for ticket {ticket_id}"
                stdout += f"\n[proxym] Prompt: {prompt[:200]}"

            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=proc.returncode or 0,
                stdout=stdout, stderr=stderr, duration_s=duration,
            )
        except asyncio.TimeoutError:
            duration = time.monotonic() - t0
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=-1, stderr=f"Timeout after {timeout}s", duration_s=duration,
            )
        except FileNotFoundError:
            duration = time.monotonic() - t0
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=127,
                stderr="docker binary not found. Run from host: docker compose --profile vscode up -d",
                duration_s=duration,
            )
        except Exception as exc:
            duration = time.monotonic() - t0
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=-1, stderr=str(exc), duration_s=duration,
            )

    def _find_compose_dir(self, project_dir: str) -> str:
        for candidate in [
            Path(project_dir),
            Path(project_dir).parent,
            Path(__file__).resolve().parent.parent.parent.parent,  # repo root
            Path("/app"),
        ]:
            if (candidate / "docker-compose.yml").exists():
                return str(candidate)
        return project_dir


class ShellAdapter(BaseAdapter):
    """Generic shell adapter — runs tool binary with prompt via stdin/args.

    Used for IDE tools (windsurf, cursor, cline, roo-code) and browser
    where a direct CLI invocation is possible but tool-specific args vary.
    Falls back to echo+log if the binary is not found.
    """

    def build_args(
        self, prompt: str, project_dir: str, mode: str = "", model: str = "",
        files: list[str] | None = None,
    ) -> list[str]:
        binary = shutil.which(self.tool.binary)
        if binary and self.tool.name in ("windsurf", "cursor"):
            # IDE tools: open project dir (no direct prompt injection)
            return [binary, project_dir]
        # Generic: echo the dispatch (placeholder for tools without CLI API or missing binary)
        return ["echo", f"[{self.tool.name}] Task dispatched: {prompt[:200]}"]

    async def run(
        self,
        prompt: str,
        project_dir: str,
        ticket_id: str = "",
        mode: str = "",
        model: str = "",
        files: list[str] | None = None,
        timeout: float = 30,
    ) -> ToolResult:
        """Run tool or log dispatch if binary not available."""
        import time

        logger.info("shell_adapter_run", tool=self.tool.name,
                    ticket_id=ticket_id, project_dir=project_dir)
        t0 = time.monotonic()

        # Check if the real binary exists; if not, fall back to echo dispatch
        binary = shutil.which(self.tool.binary)
        if not binary and self.tool.name in ("windsurf", "cursor"):
            logger.info("shell_adapter_no_binary", tool=self.tool.name,
                        binary=self.tool.binary)

        args = self.build_args(prompt, project_dir, mode, model, files)
        try:
            proc = await asyncio.create_subprocess_exec(
                *args,
                cwd=project_dir if Path(project_dir).is_dir() else None,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout_b, stderr_b = await asyncio.wait_for(
                proc.communicate(), timeout=timeout,
            )
            duration = time.monotonic() - t0
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=proc.returncode or 0,
                stdout=stdout_b.decode(errors="replace"),
                stderr=stderr_b.decode(errors="replace"),
                duration_s=duration,
            )
        except FileNotFoundError:
            duration = time.monotonic() - t0
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=127,
                stderr=f"{self.tool.binary} not found on PATH. Install {self.tool.name} or run from a host with the tool available.",
                duration_s=duration,
            )
        except Exception as exc:
            duration = time.monotonic() - t0
            return ToolResult(
                tool=self.tool.name, ticket_id=ticket_id,
                exit_code=-1, stderr=str(exc), duration_s=duration,
            )


# ── Adapter registry ─────────────────────────────────────

_ADAPTER_MAP: dict[str, type[BaseAdapter]] = {
    "aider": AiderAdapter,
    "claude-code": ClaudeCodeAdapter,
    "vscode": VSCodeAdapter,
    "roo-code": ShellAdapter,
    "cline": ShellAdapter,
    "windsurf": ShellAdapter,
    "cursor": ShellAdapter,
    "browser": ShellAdapter,
}


def get_adapter(tool_name: str) -> BaseAdapter | None:
    """Return an adapter instance for the named tool, or None."""
    registry = ToolRegistry.get()
    tool = registry.get_tool(tool_name)
    if not tool:
        return None
    adapter_cls = _ADAPTER_MAP.get(tool_name)
    if not adapter_cls:
        return None
    return adapter_cls(tool)


def register_adapter(tool_name: str, adapter_cls: type[BaseAdapter]) -> None:
    """Register a custom adapter class for a tool."""
    _ADAPTER_MAP[tool_name] = adapter_cls
