"""Generic shell adapter for IDE tools.

Used for IDE tools (windsurf, cursor, cline, roo-code) and browser
where a direct CLI invocation is possible but tool-specific args vary.
When the binary is not found, delegates to LLM fallback for real execution.
"""
from __future__ import annotations

import asyncio
import shutil
from pathlib import Path

import structlog

from proxym.tools.adapters._base import BaseAdapter, _build_proxy_env, _resolve_model_for_tool
from proxym.tools.adapters._result import ToolResult

logger = structlog.get_logger()


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

    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]
        if binary:
            return [binary, prompt[:200]]
        # No binary — will use LLM fallback in run()
        return []

    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:
        """Run tool binary or delegate to LLM fallback."""
        import time
        from proxym.tools.adapters._llm_fallback import _llm_fallback_execute

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

        # Check if the real binary exists
        binary = shutil.which(self.tool.binary) if self.tool.binary else None
        if not binary:
            logger.info("shell_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 and inject LiteLLM proxy env
        resolved_model = _resolve_model_for_tool(model, self.tool.name) if model else model
        t0 = time.monotonic()
        args = self.build_args(prompt, project_dir, mode, resolved_model, files)
        env = _build_proxy_env()
        try:
            proc = await asyncio.create_subprocess_exec(
                *args,
                cwd=project_dir if Path(project_dir).is_dir() else None,
                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
            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,
                execution_metadata={
                    "adapter": "shell", "binary": binary,
                    "args": args[:5], "model": resolved_model,
                },
            )
        except FileNotFoundError:
            duration = time.monotonic() - t0
            logger.info("shell_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,
            )
