"""
Command Executor - Execute slash commands with full template expansion

Applies all substitutions in order:
1. Argument substitution ($ARGUMENTS, $1, ${VAR})
2. File references (@filename)
3. Shell injection (!`command`)

Then sends the final prompt to an LLM callback.

Examples:
    executor = CommandExecutor(llm_callback=my_llm)
    result = executor.execute(command)
    print(result.output)
"""

from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable, Optional

from groknroll.commands.arguments import ArgumentSubstitutor, SubstitutionResult
from groknroll.commands.files import FileExpander, FileExpansionResult
from groknroll.commands.parser import Command, CommandParser
from groknroll.commands.shell import ShellExpander, ShellExpansionResult


@dataclass
class CommandExecutionResult:
    """
    Result of command execution

    Attributes:
        output: Final LLM output (or expanded prompt if no LLM)
        success: Whether execution completed successfully
        command: Original command that was executed
        expanded_prompt: The fully expanded prompt sent to LLM
        arg_result: Argument substitution result
        file_result: File expansion result
        shell_result: Shell injection result
        llm_called: Whether the LLM was invoked
        warnings: Combined warnings from all stages
        error: Error message (if any)
    """

    output: str
    success: bool = True
    command: Optional[Command] = None
    expanded_prompt: str = ""
    arg_result: Optional[SubstitutionResult] = None
    file_result: Optional[FileExpansionResult] = None
    shell_result: Optional[ShellExpansionResult] = None
    llm_called: bool = False
    warnings: list[str] = field(default_factory=list)
    error: Optional[str] = None


class CommandExecutor:
    """
    Execute slash commands with full template expansion

    Applies all substitutions in order:
    1. Argument substitution ($ARGUMENTS, $1, ${VAR})
    2. File references (@filename)
    3. Shell injection (!`command`)

    Example:
        def my_llm(prompt: str) -> str:
            return f"LLM response to: {prompt}"

        executor = CommandExecutor(llm_callback=my_llm)

        # Execute from Command object
        cmd = parser.parse("/deploy staging --force")
        result = executor.execute(cmd)
        print(result.output)

        # Execute from input string
        result = executor.execute_string("/test auth")
        print(result.output)
    """

    def __init__(
        self,
        project_path: Optional[Path] = None,
        llm_callback: Optional[Callable[[str], str]] = None,
        shell_timeout: int = 30,
        strict: bool = False,
    ):
        """
        Initialize CommandExecutor

        Args:
            project_path: Project root directory (for file resolution)
            llm_callback: Function to call with expanded prompt (optional)
            shell_timeout: Timeout for shell commands in seconds
            strict: If True, keep unresolved substitutions (don't replace with empty)
        """
        self.project_path = project_path or Path.cwd()
        self.llm_callback = llm_callback
        self.shell_timeout = shell_timeout
        self.strict = strict

        # Initialize subsystems
        self._arg_sub = ArgumentSubstitutor()
        self._file_expander = FileExpander(project_path=self.project_path)
        self._shell_expander = ShellExpander(timeout=self.shell_timeout)
        self._parser = CommandParser(project_path=self.project_path)

    def execute(self, command: Command) -> CommandExecutionResult:
        """
        Execute a parsed command

        Args:
            command: Parsed Command object

        Returns:
            CommandExecutionResult with output and metadata
        """
        warnings: list[str] = []

        # Validate command has a template
        if not command.template:
            return CommandExecutionResult(
                output="",
                success=False,
                command=command,
                error=f"Command '/{command.name}' has no template",
            )

        template = command.template

        # Stage 1: Argument substitution
        arg_result = self._arg_sub.substitute(template, command.args, strict=self.strict)
        warnings.extend(arg_result.warnings)
        template = arg_result.output

        # Stage 2: File expansion
        file_result = self._file_expander.expand(template, strict=self.strict)
        warnings.extend(file_result.warnings)
        template = file_result.output

        # Stage 3: Shell injection
        shell_result = self._shell_expander.expand(template, strict=self.strict)
        warnings.extend(shell_result.warnings)
        expanded_prompt = shell_result.output

        # Determine overall success (all stages must succeed)
        expansion_success = arg_result.success and file_result.success and shell_result.success

        # Stage 4: LLM callback (if provided)
        llm_called = False
        llm_output = expanded_prompt  # Default to expanded prompt

        if self.llm_callback is not None:
            try:
                llm_output = self.llm_callback(expanded_prompt)
                llm_called = True
            except Exception as e:
                return CommandExecutionResult(
                    output=expanded_prompt,
                    success=False,
                    command=command,
                    expanded_prompt=expanded_prompt,
                    arg_result=arg_result,
                    file_result=file_result,
                    shell_result=shell_result,
                    llm_called=False,
                    warnings=warnings,
                    error=f"LLM callback failed: {e}",
                )

        return CommandExecutionResult(
            output=llm_output,
            success=expansion_success,
            command=command,
            expanded_prompt=expanded_prompt,
            arg_result=arg_result,
            file_result=file_result,
            shell_result=shell_result,
            llm_called=llm_called,
            warnings=warnings,
        )

    def execute_string(self, input_text: str) -> CommandExecutionResult:
        """
        Execute a command from input string

        Parses the input as a slash command and executes it.

        Args:
            input_text: Input string (e.g., "/test auth --verbose")

        Returns:
            CommandExecutionResult with output and metadata
        """
        command = self._parser.parse(input_text)

        if command is None:
            return CommandExecutionResult(
                output="",
                success=False,
                error=f"Invalid command: {input_text}",
            )

        if not command.template:
            return CommandExecutionResult(
                output="",
                success=False,
                command=command,
                error=f"Command '/{command.name}' not found",
            )

        return self.execute(command)

    def expand_only(self, command: Command) -> CommandExecutionResult:
        """
        Expand a command template without calling LLM

        Useful for previewing what would be sent to the LLM.

        Args:
            command: Parsed Command object

        Returns:
            CommandExecutionResult with expanded_prompt (no LLM output)
        """
        # Temporarily disable LLM callback
        original_callback = self.llm_callback
        self.llm_callback = None

        try:
            result = self.execute(command)
            return result
        finally:
            self.llm_callback = original_callback

    def validate_command(self, command: Command) -> list[str]:
        """
        Validate a command before execution

        Checks for issues that might cause execution to fail:
        - Missing required arguments
        - Missing environment variables
        - Missing file references

        Args:
            command: Parsed Command object

        Returns:
            List of validation issue messages (empty if valid)
        """
        issues: list[str] = []

        if not command.template:
            issues.append(f"Command '/{command.name}' has no template")
            return issues

        # Check for required arguments
        required_args = self._arg_sub.get_required_args(command.template)
        if required_args:
            # Parse actual args
            import shlex

            try:
                arg_list = shlex.split(command.args) if command.args else []
            except ValueError:
                arg_list = command.args.split() if command.args else []

            max_required = max(required_args) if required_args else 0
            if len(arg_list) < max_required:
                missing = [i for i in required_args if i > len(arg_list)]
                issues.append(f"Missing arguments: {', '.join(f'${i}' for i in missing)}")

        # Check for required env vars
        required_vars = self._arg_sub.get_required_env_vars(command.template)
        import os

        missing_vars = [v for v in required_vars if v not in os.environ]
        if missing_vars:
            issues.append(
                f"Missing environment variables: {', '.join(f'${{{v}}}' for v in missing_vars)}"
            )

        # Check file references (without actually reading files)
        file_refs = self._file_expander.find_references(command.template)
        for ref in file_refs:
            # Try to resolve the file
            path = Path(ref)
            if path.is_absolute():
                if not path.exists():
                    issues.append(f"File not found: {ref}")
            else:
                # Check relative to project path
                if not (self.project_path / ref).exists():
                    issues.append(f"File not found: {ref}")

        return issues

    def reload(self) -> None:
        """Reload command parser (clears cache)"""
        self._parser.reload()

    def __str__(self) -> str:
        return f"CommandExecutor(project={self.project_path})"

    def __repr__(self) -> str:
        has_llm = self.llm_callback is not None
        return f"CommandExecutor(project_path='{self.project_path}', has_llm={has_llm})"


# Convenience functions


def execute_command(
    command: Command,
    llm_callback: Optional[Callable[[str], str]] = None,
    project_path: Optional[Path] = None,
    strict: bool = False,
) -> CommandExecutionResult:
    """
    Execute a command (convenience function)

    Args:
        command: Parsed Command object
        llm_callback: Optional LLM callback function
        project_path: Project root directory
        strict: If True, keep unresolved substitutions

    Returns:
        CommandExecutionResult with output and metadata
    """
    executor = CommandExecutor(
        project_path=project_path,
        llm_callback=llm_callback,
        strict=strict,
    )
    return executor.execute(command)


def execute_command_string(
    input_text: str,
    llm_callback: Optional[Callable[[str], str]] = None,
    project_path: Optional[Path] = None,
    strict: bool = False,
) -> CommandExecutionResult:
    """
    Execute a command from input string (convenience function)

    Args:
        input_text: Input string (e.g., "/test auth")
        llm_callback: Optional LLM callback function
        project_path: Project root directory
        strict: If True, keep unresolved substitutions

    Returns:
        CommandExecutionResult with output and metadata
    """
    executor = CommandExecutor(
        project_path=project_path,
        llm_callback=llm_callback,
        strict=strict,
    )
    return executor.execute_string(input_text)
