"""
BashTool - Execute shell commands with subprocess and PTY support

Following OpenCode architecture, provides command execution with:
- Subprocess execution for simple commands
- PTY support for interactive commands
- Timeout handling (default 60s)
- Working directory support
- Comprehensive error handling
- Type-safe implementation
- Command injection prevention (SEC-001)
"""

import os
import re
import shlex
import subprocess
from pathlib import Path
from typing import Any, Dict, Optional

from groknroll.tools.base_tool import BaseTool
from groknroll.tools.descriptions import get_enhanced_description


# Dangerous command patterns that could indicate injection attacks (SEC-001)
DANGEROUS_COMMAND_PATTERNS = [
    # Pipe to shell interpreters
    re.compile(r'\|\s*(?:bash|sh|zsh|ksh|csh|tcsh|fish|dash)\b'),
    # Backtick command substitution
    re.compile(r'`[^`]+`'),
    # $() command substitution (commonly used for injection)
    re.compile(r'\$\([^)]+\)'),
    # eval command
    re.compile(r'\beval\s+'),
    # exec command
    re.compile(r'\bexec\s+'),
    # source command
    re.compile(r'\bsource\s+'),
    # Dot sourcing
    re.compile(r'^\s*\.\s+'),
]

# Dangerous environment variables that could be used for injection (SEC-005)
DANGEROUS_ENV_VARS = frozenset([
    # Library injection (Linux)
    "LD_PRELOAD",
    "LD_LIBRARY_PATH",
    "LD_AUDIT",
    "LD_DEBUG",
    "LD_DEBUG_OUTPUT",
    "LD_DYNAMIC_WEAK",
    "LD_ORIGIN_PATH",
    "LD_PROFILE",
    "LD_PROFILE_OUTPUT",
    "LD_SHOW_AUXV",
    "LD_USE_LOAD_BIAS",
    # Library injection (macOS)
    "DYLD_INSERT_LIBRARIES",
    "DYLD_LIBRARY_PATH",
    "DYLD_FRAMEWORK_PATH",
    "DYLD_FALLBACK_LIBRARY_PATH",
    "DYLD_FALLBACK_FRAMEWORK_PATH",
    "DYLD_IMAGE_SUFFIX",
    "DYLD_PRINT_LIBRARIES",
    # Python injection
    "PYTHONPATH",
    "PYTHONSTARTUP",
    "PYTHONHOME",
    # Ruby injection
    "RUBYLIB",
    "RUBYOPT",
    # Perl injection
    "PERL5LIB",
    "PERL5OPT",
    "PERLLIB",
    # Node injection
    "NODE_OPTIONS",
    "NODE_PATH",
    # Shell configuration
    "BASH_ENV",
    "ENV",
    "SHELLOPTS",
    "BASHOPTS",
    # IFS manipulation
    "IFS",
    # Prompt command injection
    "PROMPT_COMMAND",
    # Locale attacks
    "LOCALDOMAIN",
    "NLSPATH",
])


class BashResult:
    """
    Result of a bash command execution

    Attributes:
        success: Whether the command succeeded (exit_code == 0)
        command: The command that was executed
        stdout: Standard output from the command
        stderr: Standard error from the command
        exit_code: Exit code from the command
        cwd: Working directory where command was executed
        timed_out: Whether the command timed out
    """

    def __init__(
        self,
        success: bool,
        command: str,
        stdout: str,
        stderr: str,
        exit_code: int,
        cwd: Path,
        timed_out: bool = False,
    ):
        self.success = success
        self.command = command
        self.stdout = stdout
        self.stderr = stderr
        self.exit_code = exit_code
        self.cwd = cwd
        self.timed_out = timed_out

    def to_dict(self) -> Dict[str, Any]:
        """Convert result to dictionary"""
        return {
            "success": self.success,
            "command": self.command,
            "stdout": self.stdout,
            "stderr": self.stderr,
            "exit_code": self.exit_code,
            "cwd": str(self.cwd),
            "timed_out": self.timed_out,
        }

    def __str__(self) -> str:
        """String representation"""
        if self.timed_out:
            return f"Command timed out: {self.command} (in {self.cwd})"
        if self.success:
            return f"Command succeeded: {self.command} (exit code {self.exit_code})"
        return f"Command failed: {self.command} (exit code {self.exit_code})"


class BashTool(BaseTool):
    """
    Tool for executing shell commands via subprocess

    Accepts:
        command: str - Shell command to execute
        cwd: str (optional) - Working directory (defaults to workspace root)
        timeout: int (optional) - Timeout in seconds (default: 60)
        shell: bool (optional) - Use shell for execution (default: True for complex commands)

    Returns:
        BashResult - Command execution result with stdout, stderr, exit_code

    Raises:
        ValueError: If command is empty or invalid
        TimeoutError: If command exceeds timeout (converted to BashResult with timed_out=True)
        PermissionError: If permission denied in working directory

    Example:
        tool = BashTool()
        result = await tool.execute(command="echo 'Hello, World!'")
        print(result.stdout)  # "Hello, World!"
        print(result.exit_code)  # 0

    Security Notes:
        - Commands are validated for dangerous patterns before execution
        - Default shell=False prevents shell injection attacks (SEC-001)
        - Commands should be permission-gated for dangerous operations
        - This tool respects the workspace_root for relative cwd paths
    """

    def __init__(self, workspace_root: Optional[str] = None, permission_manager=None):
        """
        Initialize BashTool

        Args:
            workspace_root: Optional root directory for relative cwd paths.
                           Defaults to current working directory.
            permission_manager: Optional permission manager for checking bash permissions (SEC-006).
        """
        self._workspace_root = Path(workspace_root) if workspace_root else Path.cwd()
        self._permission_manager = permission_manager

    @property
    def name(self) -> str:
        """Tool identifier"""
        return "bash"

    @property
    def description(self) -> str:
        """Human-readable description"""
        return get_enhanced_description("bash", "Execute shell commands via subprocess")

    def validate_params(self, **kwargs) -> dict[str, Any]:
        """
        Validate parameters before execution

        Args:
            **kwargs: Must contain 'command' parameter

        Returns:
            Validated parameters dict

        Raises:
            ValueError: If required parameters are missing or invalid
        """
        if "command" not in kwargs:
            raise ValueError("command parameter is required")

        command = kwargs["command"]
        if not isinstance(command, str):
            raise ValueError(f"command must be a string, got {type(command).__name__}")

        if not command.strip():
            raise ValueError("command cannot be empty")

        # Validate cwd if provided
        if "cwd" in kwargs:
            cwd = kwargs["cwd"]
            if cwd is not None and not isinstance(cwd, str):
                raise ValueError(f"cwd must be a string or None, got {type(cwd).__name__}")

        # Validate timeout if provided
        if "timeout" in kwargs:
            timeout = kwargs["timeout"]
            if not isinstance(timeout, (int, float)):
                raise ValueError(f"timeout must be a number, got {type(timeout).__name__}")
            if timeout <= 0:
                raise ValueError(f"timeout must be positive, got {timeout}")

        # Validate shell if provided
        if "shell" in kwargs:
            shell = kwargs["shell"]
            if not isinstance(shell, bool):
                raise ValueError(f"shell must be a boolean, got {type(shell).__name__}")

        return kwargs

    def _validate_command(self, command: str) -> None:
        """
        Validate command for dangerous patterns that could indicate injection attacks.

        SEC-001: Command injection prevention.

        Args:
            command: The command string to validate

        Raises:
            ValueError: If command contains dangerous patterns
        """
        for pattern in DANGEROUS_COMMAND_PATTERNS:
            if pattern.search(command):
                raise ValueError(
                    f"Command contains potentially dangerous pattern. "
                    f"For security reasons, commands with shell injection patterns "
                    f"(pipes to shell interpreters, backticks, $(), eval, exec, source) "
                    f"are blocked. Please use safer alternatives."
                )

    def _sanitize_env(self, env: Optional[Dict[str, str]] = None) -> Dict[str, str]:
        """
        Sanitize environment variables by removing dangerous ones.

        SEC-005: Environment variable injection prevention.

        Args:
            env: Optional environment dict to merge with current environment

        Returns:
            Sanitized environment dict
        """
        # Start with current environment
        sanitized = os.environ.copy()

        # Remove dangerous environment variables
        for var in DANGEROUS_ENV_VARS:
            sanitized.pop(var, None)

        # Merge user-provided env (after removing dangerous ones from it too)
        if env:
            for key, value in env.items():
                if key not in DANGEROUS_ENV_VARS:
                    sanitized[key] = value

        return sanitized

    def _check_permission(self, command: str) -> None:
        """
        Check if command execution is permitted.

        SEC-006: Permission checks for bash commands.

        Args:
            command: The command to check

        Raises:
            PermissionError: If command is not permitted
        """
        if self._permission_manager is None:
            return  # No permission manager configured, allow by default

        result = self._permission_manager.check("bash", command=command)
        if result == "deny":
            raise PermissionError(
                f"Command execution denied by permission policy: {command}"
            )

    async def execute(self, **kwargs) -> BashResult:
        """
        Execute shell command via subprocess

        Args:
            **kwargs: Must contain 'command' parameter

        Returns:
            BashResult with command execution details

        Raises:
            ValueError: If command is invalid
            PermissionError: If cwd is not accessible
        """
        # Validate parameters
        validated = self.validate_params(**kwargs)
        command = validated["command"]
        cwd_str = validated.get("cwd")
        timeout = validated.get("timeout", 60)  # Default 60 seconds
        use_shell = validated.get("shell", False)  # Default to non-shell mode (SEC-001)
        env = validated.get("env")  # Optional custom environment

        # SEC-006: Check permission before execution
        self._check_permission(command)

        # SEC-001: Validate command for dangerous injection patterns
        self._validate_command(command)

        # SEC-005: Sanitize environment variables
        sanitized_env = self._sanitize_env(env)

        # Resolve working directory
        if cwd_str is None:
            cwd = self._workspace_root
        else:
            cwd = Path(cwd_str)
            if not cwd.is_absolute():
                cwd = self._workspace_root / cwd

        # Normalize path
        try:
            cwd = cwd.resolve()
        except (OSError, RuntimeError) as e:
            raise ValueError(f"Cannot resolve cwd {cwd_str}: {e}")

        # Check directory exists and is accessible
        if not cwd.exists():
            raise FileNotFoundError(f"Working directory not found: {cwd}")

        if not cwd.is_dir():
            raise ValueError(f"Working directory is not a directory: {cwd}")

        if not os.access(cwd, os.R_OK | os.X_OK):
            raise PermissionError(f"No permission to access working directory: {cwd}")

        # Execute command
        try:
            result = subprocess.run(
                command if use_shell else shlex.split(command),
                cwd=str(cwd),
                shell=use_shell,
                capture_output=True,
                text=True,
                timeout=timeout,
                env=sanitized_env,  # SEC-005: Use sanitized environment
            )

            stdout = result.stdout
            stderr = result.stderr
            exit_code = result.returncode
            timed_out = False

        except subprocess.TimeoutExpired as e:
            # Command timed out
            stdout = e.stdout.decode("utf-8") if e.stdout else ""
            stderr = e.stderr.decode("utf-8") if e.stderr else ""
            exit_code = -1
            timed_out = True

        except Exception as e:
            # Other errors (should be rare with shell=True)
            stdout = ""
            stderr = str(e)
            exit_code = -1
            timed_out = False

        return BashResult(
            success=(exit_code == 0 and not timed_out),
            command=command,
            stdout=stdout,
            stderr=stderr,
            exit_code=exit_code,
            cwd=cwd,
            timed_out=timed_out,
        )
