"""BaseAdapter ABC — shared interface for all tool adapters.

All CLI tools connect to LLMs through the LiteLLM proxy. The base adapter
injects ``OPENAI_API_BASE``, ``OPENAI_API_KEY``, ``ANTHROPIC_BASE_URL``, and
``ANTHROPIC_API_KEY`` into the subprocess environment so every tool routes
requests through the proxy automatically.
"""
from __future__ import annotations

import asyncio
import os
import shutil
from abc import ABC, abstractmethod

import structlog

from proxym.tools import ToolDefinition
from proxym.tools.adapters._result import ToolResult

logger = structlog.get_logger()


def _resolve_model_for_tool(model: str, tool_name: str) -> str:
    """Resolve a model ID to a litellm-routable model string for tool CLI.

    Looks up the model in the registry by ID or litellm_model name.
    Returns the litellm_model string prefixed with ``openai/`` when needed
    so the LiteLLM proxy routes correctly.
    """
    try:
        from proxym.providers import resolve_model
        spec = resolve_model(model)
        if spec:
            return spec.litellm_model
    except Exception:
        pass

    # If model is already a full litellm path (provider/model), pass through
    if "/" in model:
        return model

    # Last resort: prefix with openai/ for proxy routing
    return f"openai/{model}"


def _build_proxy_env() -> dict[str, str]:
    """Build subprocess environment with LiteLLM proxy connection vars.

    Reads the proxy URL and API key from proxym settings and injects them
    as environment variables that CLI tools recognise.
    """
    env = dict(os.environ)
    try:
        from proxym.config import get_settings
        settings = get_settings()
        proxy_url = f"http://localhost:5002"  # LiteLLM proxy port
        api_key = settings.master_key or os.environ.get("PROXYM_API_KEY", "sk-proxym-master")

        # OpenAI-compatible (aider, goose, codex, open-interpreter, plandex, llm-cli)
        env.setdefault("OPENAI_API_KEY", api_key)
        env["OPENAI_API_BASE"] = f"{proxy_url}/v1"
        env["OPENAI_BASE_URL"] = f"{proxy_url}/v1"

        # Aider-specific
        env["AIDER_OPENAI_API_BASE"] = f"{proxy_url}/v1"

        # Anthropic (claude-code)
        env.setdefault("ANTHROPIC_API_KEY", api_key)
        env["ANTHROPIC_BASE_URL"] = proxy_url
    except Exception:
        pass
    return env


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.

        If the tool binary is not found, delegates to LLM fallback
        for real execution via the proxy API.
        """
        import time
        from proxym.tools.adapters._llm_fallback import _llm_fallback_execute

        binary = shutil.which(self.tool.binary) if self.tool.binary else None
        if not binary:
            logger.info("adapter_llm_fallback", tool=self.tool.name,
                        binary=self.tool.binary, reason="binary not found")
            return await _llm_fallback_execute(
                self.tool.name, prompt, ticket_id, project_dir, mode, model, timeout,
            )

        # Resolve model aliases before passing to CLI
        resolved_model = _resolve_model_for_tool(model, self.tool.name) if model else model
        args = self.build_args(prompt, project_dir, mode, resolved_model, files)

        # Inject LiteLLM proxy env vars into subprocess
        env = _build_proxy_env()

        logger.info("tool_adapter_run", tool=self.tool.name, args=args[:5],
                     cwd=project_dir, model=resolved_model)
        t0 = time.monotonic()
        try:
            proc = await asyncio.create_subprocess_exec(
                *args,
                cwd=project_dir,
                env=env,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout_b, stderr_b = await asyncio.wait_for(
                proc.communicate(), timeout=timeout,
            )
            duration = time.monotonic() - t0
            exit_code = proc.returncode if proc.returncode is not None else -1
            stdout_str = stdout_b.decode(errors="replace")
            stderr_str = stderr_b.decode(errors="replace")

            # Detect known failure patterns even when exit_code == 0
            if exit_code == 0:
                combined = stdout_str + stderr_str
                _FAILURE_PATTERNS = [
                    "AuthenticationError",
                    "No api key passed in",
                    "Invalid API key",
                    "Connection refused",
                    "ECONNREFUSED",
                    "Rate limit exceeded",
                    "Could not connect",
                ]
                for pattern in _FAILURE_PATTERNS:
                    if pattern in combined:
                        exit_code = 1
                        if not stderr_str.strip():
                            stderr_str = f"Detected failure pattern in output: {pattern}"
                        logger.warning("adapter_exit0_but_failed",
                                       tool=self.tool.name, pattern=pattern)
                        break

            result = ToolResult(
                tool=self.tool.name,
                ticket_id=ticket_id,
                exit_code=exit_code,
                stdout=stdout_str,
                stderr=stderr_str,
                duration_s=duration,
                execution_metadata={
                    "adapter": self.tool.name, "binary": binary,
                    "args": args[:5], "model": resolved_model,
                },
            )
            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 FileNotFoundError:
            duration = time.monotonic() - t0
            logger.info("adapter_binary_vanished", tool=self.tool.name)
            return await _llm_fallback_execute(
                self.tool.name, prompt, ticket_id, project_dir, mode, model, timeout,
            )
        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,
            )
