"""Claude Code execution engine."""

import re
import shutil
import subprocess
from collections.abc import Callable

from messirve.engines.base import Engine, EngineResult
from messirve.exceptions import ClaudeCodeError
from messirve.models.config import ClaudePermissions, MessirveConfig
from messirve.models.task import Task


class ClaudeCodeEngine(Engine):
    """Engine that executes tasks using Claude Code CLI."""

    CLAUDE_BINARY = "claude"

    def __init__(self) -> None:
        """Initialize the Claude Code engine."""
        self._claude_path: str | None = None

    def is_available(self) -> bool:
        """Check if Claude Code CLI is available.

        Returns:
            True if Claude Code is installed and accessible.
        """
        return shutil.which(self.CLAUDE_BINARY) is not None

    def _get_claude_path(self) -> str:
        """Get the path to the Claude Code binary.

        Returns:
            Path to the Claude Code binary.

        Raises:
            ClaudeCodeError: If Claude Code is not installed.
        """
        if self._claude_path is None:
            self._claude_path = shutil.which(self.CLAUDE_BINARY)
            if self._claude_path is None:
                raise ClaudeCodeError(
                    "Claude Code CLI not found. Please install it first.",
                    exit_code=1,
                )
        return self._claude_path

    def build_prompt(self, task: Task, config: MessirveConfig) -> str:
        """Build the prompt for Claude Code.

        Args:
            task: The task to build a prompt for.
            config: Messirve configuration.

        Returns:
            Complete prompt string.
        """
        lines: list[str] = []

        # Task header
        lines.append(f"Task: {task.title}")
        lines.append(f"Task ID: {task.id}")
        lines.append("")

        # Description
        lines.append("Description:")
        lines.append(task.description.strip())
        lines.append("")

        # Context
        lines.append("Context:")
        lines.append(task.context.strip())
        lines.append("")

        # Acceptance Criteria
        lines.append("Acceptance Criteria:")
        for criterion in task.acceptance_criteria:
            lines.append(f"- {criterion}")
        lines.append("")

        # Project Rules
        if config.rules:
            lines.append("Project Rules:")
            for rule in config.rules:
                lines.append(f"- {rule}")
            lines.append("")

        # File Boundaries
        if config.boundaries.never_modify or config.boundaries.read_only:
            lines.append("File Boundaries:")
            if config.boundaries.never_modify:
                lines.append("Never modify these files:")
                for pattern in config.boundaries.never_modify:
                    lines.append(f"  - {pattern}")
            if config.boundaries.read_only:
                lines.append("Read-only files (do not modify):")
                for pattern in config.boundaries.read_only:
                    lines.append(f"  - {pattern}")
            lines.append("")

        # Instructions
        lines.append("Instructions:")
        lines.append("- Implement the task as described above")
        lines.append("- Ensure all acceptance criteria are met")
        lines.append("- Follow all project rules")
        lines.append("- Respect file boundaries")
        lines.append("- Write tests if applicable")

        return "\n".join(lines)

    def execute(
        self,
        task: Task,
        config: MessirveConfig,
        output_callback: Callable[[str], None] | None = None,
    ) -> EngineResult:
        """Execute a task using Claude Code CLI.

        Args:
            task: The task to execute.
            config: Messirve configuration.
            output_callback: Optional callback for streaming output.

        Returns:
            EngineResult with execution details.
        """
        claude_path = self._get_claude_path()
        prompt = self.build_prompt(task, config)

        # Build command
        cmd = [claude_path, "-p", prompt, "--output-format", "text"]

        # Add permission flag
        if config.defaults.claude_code_permissions == ClaudePermissions.SKIP:
            cmd.append("--dangerously-skip-permissions")

        try:
            # Run Claude Code
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1,
            )

            output_lines: list[str] = []
            if process.stdout:
                for line in process.stdout:
                    output_lines.append(line)
                    if output_callback:
                        output_callback(line)

            process.wait()
            output = "".join(output_lines)
            exit_code = process.returncode

            # Parse tokens and cost from output if available
            tokens_used = self._parse_tokens(output)
            cost_usd = self._parse_cost(output)

            if exit_code == 0:
                return EngineResult(
                    success=True,
                    output=output,
                    exit_code=exit_code,
                    tokens_used=tokens_used,
                    cost_usd=cost_usd,
                )
            else:
                return EngineResult(
                    success=False,
                    output=output,
                    exit_code=exit_code,
                    tokens_used=tokens_used,
                    cost_usd=cost_usd,
                    error=f"Claude Code exited with code {exit_code}",
                )

        except FileNotFoundError:
            raise ClaudeCodeError(
                "Claude Code CLI not found. Please install it first.",
                exit_code=1,
            )
        except subprocess.SubprocessError as e:
            return EngineResult(
                success=False,
                output="",
                exit_code=1,
                error=str(e),
            )

    def _parse_tokens(self, output: str) -> int:
        """Parse token count from Claude Code output.

        Args:
            output: Output from Claude Code.

        Returns:
            Number of tokens used, or 0 if not found.
        """
        # Look for patterns like "tokens: 1234" or "1234 tokens"
        patterns = [
            r"(\d+)\s*tokens",
            r"tokens:\s*(\d+)",
            r"token[s]?\s*used:\s*(\d+)",
        ]
        for pattern in patterns:
            match = re.search(pattern, output, re.IGNORECASE)
            if match:
                return int(match.group(1))
        return 0

    def _parse_cost(self, output: str) -> float:
        """Parse cost from Claude Code output.

        Args:
            output: Output from Claude Code.

        Returns:
            Cost in USD, or 0.0 if not found.
        """
        # Look for patterns like "$1.23" or "cost: $1.23"
        patterns = [
            r"\$(\d+\.?\d*)",
            r"cost:\s*\$?(\d+\.?\d*)",
        ]
        for pattern in patterns:
            match = re.search(pattern, output, re.IGNORECASE)
            if match:
                return float(match.group(1))
        return 0.0
