"""
Shell Injection - Replace !`command` with shell command output

Supports:
- !`command` - Execute command and replace with stdout
- Multiple shell injections in single template
- Timeout for long-running commands

Examples:
    expand_shell("Deploy to !`git branch --show-current`")
    → "Deploy to main"

    expand_shell("Version: !`cat VERSION`")
    → "Version: 1.0.0"
"""

import re
import subprocess
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class ShellExecution:
    """
    Represents a shell command execution

    Attributes:
        original: Original !`command` text
        command: Extracted command
        output: Command output (stdout)
        exit_code: Command exit code
        error: Error message (if any)
    """

    original: str
    command: str
    output: Optional[str] = None
    exit_code: Optional[int] = None
    error: Optional[str] = None

    @property
    def success(self) -> bool:
        """Whether command executed successfully"""
        return self.exit_code == 0 and self.error is None


@dataclass
class ShellExpansionResult:
    """
    Result of shell injection expansion

    Attributes:
        output: Template with shell commands replaced
        success: Whether all commands succeeded
        executions: List of shell executions
        warnings: List of warning messages
    """

    output: str
    success: bool = True
    executions: list[ShellExecution] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)


class ShellExpander:
    """
    Expand !`command` shell injections in templates

    Executes shell commands and replaces with their output.

    Example:
        expander = ShellExpander()
        result = expander.expand("Branch: !`git branch --show-current`")
        print(result.output)  # "Branch: main"
    """

    # Pattern to match !`command`
    SHELL_PATTERN = re.compile(r"!`([^`]+)`")

    def __init__(
        self,
        timeout: int = 30,
        shell: bool = True,
        cwd: Optional[str] = None,
    ):
        """
        Initialize ShellExpander

        Args:
            timeout: Command timeout in seconds
            shell: Whether to run commands through shell
            cwd: Working directory for commands
        """
        self.timeout = timeout
        self.shell = shell
        self.cwd = cwd

    def expand(
        self,
        template: str,
        strict: bool = False,
    ) -> ShellExpansionResult:
        """
        Expand shell injections in template

        Args:
            template: Template with !`command` syntax
            strict: If True, fail on command errors

        Returns:
            ShellExpansionResult with expanded output
        """
        executions: list[ShellExecution] = []
        warnings: list[str] = []
        success = True

        # Find all shell injections
        matches = list(self.SHELL_PATTERN.finditer(template))

        if not matches:
            return ShellExpansionResult(output=template, success=True)

        # Process each injection (reverse order to preserve indices)
        output = template
        for match in reversed(matches):
            command = match.group(1)
            original = match.group(0)

            execution = self._execute_command(command, original)
            executions.insert(0, execution)  # Maintain order

            if execution.error:
                warnings.append(execution.error)
                success = False

            # Replace in output
            if execution.success and execution.output is not None:
                replacement = execution.output
            elif strict:
                replacement = original  # Keep original on error in strict mode
            else:
                replacement = f"[Command failed: {command}]"

            output = output[: match.start()] + replacement + output[match.end() :]

        return ShellExpansionResult(
            output=output,
            success=success,
            executions=executions,
            warnings=warnings,
        )

    def _execute_command(self, command: str, original: str) -> ShellExecution:
        """
        Execute a shell command

        Args:
            command: Command to execute
            original: Original !`command` text

        Returns:
            ShellExecution with output and status
        """
        execution = ShellExecution(original=original, command=command)

        try:
            result = subprocess.run(
                command,
                shell=self.shell,
                capture_output=True,
                text=True,
                timeout=self.timeout,
                cwd=self.cwd,
            )

            execution.exit_code = result.returncode
            execution.output = result.stdout.rstrip("\n")  # Strip trailing newlines

            if result.returncode != 0:
                stderr = result.stderr.strip()
                execution.error = f"Command '{command}' failed (exit {result.returncode})"
                if stderr:
                    execution.error += f": {stderr}"

        except subprocess.TimeoutExpired:
            execution.error = f"Command '{command}' timed out after {self.timeout}s"
            execution.exit_code = -1
        except FileNotFoundError:
            execution.error = f"Command not found: {command.split()[0]}"
            execution.exit_code = -1
        except Exception as e:
            execution.error = f"Error executing '{command}': {e}"
            execution.exit_code = -1

        return execution

    def find_commands(self, template: str) -> list[str]:
        """
        Find all shell commands in template without executing

        Args:
            template: Template to search

        Returns:
            List of commands found
        """
        return [match.group(1) for match in self.SHELL_PATTERN.finditer(template)]

    def has_injections(self, template: str) -> bool:
        """
        Check if template has any shell injections

        Args:
            template: Template to check

        Returns:
            True if template contains !`command` syntax
        """
        return bool(self.SHELL_PATTERN.search(template))


# Convenience functions


def expand_shell(
    template: str,
    timeout: int = 30,
    cwd: Optional[str] = None,
    strict: bool = False,
) -> str:
    """
    Expand shell injections in template (convenience function)

    Args:
        template: Template with !`command` syntax
        timeout: Command timeout in seconds
        cwd: Working directory for commands
        strict: If True, keep original on error

    Returns:
        Template with command outputs substituted
    """
    expander = ShellExpander(timeout=timeout, cwd=cwd)
    result = expander.expand(template, strict=strict)
    return result.output


def expand_shell_with_result(
    template: str,
    timeout: int = 30,
    cwd: Optional[str] = None,
    strict: bool = False,
) -> ShellExpansionResult:
    """
    Expand shell injections and return full result

    Args:
        template: Template with !`command` syntax
        timeout: Command timeout in seconds
        cwd: Working directory for commands
        strict: If True, keep original on error

    Returns:
        ShellExpansionResult with output and metadata
    """
    expander = ShellExpander(timeout=timeout, cwd=cwd)
    return expander.expand(template, strict=strict)
