"""Spawn a short-lived Claude Code CLI subprocess for read-only codebase research.

Used by the Discord agent to answer questions about any project's code without
giving the research subprocess write access or peon-mcp tool access.
"""

from __future__ import annotations

import asyncio
import json
import logging
import subprocess
from dataclasses import dataclass, field
from shutil import which

logger = logging.getLogger(__name__)

# Limit concurrent research subprocesses to avoid resource exhaustion.
_research_semaphore = asyncio.Semaphore(2)

# Grace period (seconds) after SIGTERM before escalating to SIGKILL.
_KILL_GRACE = 10


@dataclass
class ResearchResult:
    """Result of a codebase research query."""

    answer: str = ""
    cost_usd: float = 0.0
    timed_out: bool = False
    error: str = ""


class ResearchRunner:
    """Spawns ``claude`` CLI for read-only codebase research.

    Each call creates a fresh subprocess restricted to Read/Glob/Grep tools,
    parses the JSON output, and returns a :class:`ResearchResult`.
    """

    def __init__(
        self,
        model: str = "sonnet",
        timeout_seconds: int = 120,
        max_budget_usd: float = 0.50,
    ) -> None:
        self.model = model
        self.timeout_seconds = timeout_seconds
        self.max_budget_usd = max_budget_usd
        self._claude_bin: str | None = None

    def _find_claude(self) -> str:
        """Locate the ``claude`` CLI binary, caching the result."""
        if self._claude_bin is not None:
            return self._claude_bin
        found = which("claude")
        if not found:
            raise FileNotFoundError(
                "Could not find 'claude' CLI. "
                "Install it from https://claude.ai/download"
            )
        self._claude_bin = found
        return found

    def _build_research_prompt(
        self, project_id: str, repo_path: str, question: str
    ) -> str:
        """Build the prompt injected into the Claude CLI subprocess."""
        return (
            f"You are researching the codebase for project '{project_id}' "
            f"located at {repo_path}.\n\n"
            f"Answer the following question:\n{question}\n\n"
            "Guidelines:\n"
            "- Be concise — keep your answer under 3000 characters.\n"
            "- Include relevant file paths (e.g. src/foo.py:42).\n"
            "- Include brief code snippets when they clarify the answer.\n"
            "- Do NOT attempt to modify any files.\n"
            "- If you cannot find the answer, say so clearly."
        )

    def _build_command(self, prompt: str) -> list[str]:
        """Build the CLI argument list for the research subprocess."""
        claude_bin = self._find_claude()
        return [
            claude_bin,
            "-p", prompt,
            "--allowedTools", "Read,Glob,Grep",
            "--permission-mode", "bypassPermissions",
            "--model", self.model,
            "--output-format", "json",
            "--max-budget-usd", str(self.max_budget_usd),
        ]

    def _run_subprocess(self, cmd: list[str], cwd: str) -> ResearchResult:
        """Run the CLI subprocess (blocking). Called via ``asyncio.to_thread``."""
        try:
            proc = subprocess.Popen(
                cmd,
                cwd=cwd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )
        except OSError as exc:
            return ResearchResult(error=f"Failed to start claude CLI: {exc}")

        timed_out = False
        try:
            stdout, stderr = proc.communicate(timeout=self.timeout_seconds)
        except subprocess.TimeoutExpired:
            timed_out = True
            proc.terminate()
            try:
                stdout, stderr = proc.communicate(timeout=_KILL_GRACE)
            except subprocess.TimeoutExpired:
                proc.kill()
                stdout, stderr = proc.communicate()

        if timed_out:
            return ResearchResult(
                timed_out=True,
                error=f"Research timed out after {self.timeout_seconds}s",
            )

        if proc.returncode != 0:
            err_detail = (stderr or stdout or "").strip()[:500]
            return ResearchResult(
                error=f"claude CLI exited with code {proc.returncode}: {err_detail}"
            )

        # Parse the JSON output: {"result": "...", "total_cost_usd": ...}
        try:
            data = json.loads(stdout)
        except (json.JSONDecodeError, TypeError):
            # Fallback: treat raw stdout as the answer
            return ResearchResult(answer=stdout.strip()[:3000])

        answer = data.get("result", "")
        if isinstance(answer, str):
            answer = answer[:3000]
        else:
            answer = str(answer)[:3000]

        cost = 0.0
        try:
            cost = float(data.get("total_cost_usd", 0))
        except (TypeError, ValueError):
            pass

        return ResearchResult(answer=answer, cost_usd=cost)

    async def research(
        self, project_id: str, repo_path: str, question: str
    ) -> ResearchResult:
        """Run a codebase research query (async entry point).

        Acquires the module-level semaphore to limit concurrency, then
        delegates to :meth:`_run_subprocess` via ``asyncio.to_thread``.
        """
        prompt = self._build_research_prompt(project_id, repo_path, question)
        cmd = self._build_command(prompt)

        async with _research_semaphore:
            logger.info(
                "Starting research for project '%s': %s",
                project_id,
                question[:80],
            )
            result = await asyncio.to_thread(self._run_subprocess, cmd, repo_path)
            if result.error:
                logger.warning("Research error for '%s': %s", project_id, result.error)
            else:
                logger.info(
                    "Research complete for '%s' (cost=$%.4f)",
                    project_id,
                    result.cost_usd,
                )
            return result
