"""
Bash Operations Module

Provides bash command execution with output capture and PTY support.

Security Features (SEC-010):
- Secret masking for API keys and tokens before logging
- Shell injection prevention
- Dangerous command blocking
"""

import os
import pty
import re
import select
import shlex
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Optional


# SEC-010: Secret patterns for masking sensitive data in logs/output
# These patterns match common API key and token formats
SECRET_PATTERNS = [
    # OpenAI API keys
    (re.compile(r'sk-[a-zA-Z0-9]{20,}'), 'sk-***MASKED***'),
    # Anthropic API keys
    (re.compile(r'sk-ant-[a-zA-Z0-9]{20,}'), 'sk-ant-***MASKED***'),
    # Generic API key patterns (API_KEY=, api_key=, apikey=)
    (re.compile(r'(?i)(api[_-]?key\s*[=:]\s*)["\']?([a-zA-Z0-9_\-]{20,})["\']?'), r'\1***MASKED***'),
    # Bearer tokens
    (re.compile(r'(?i)(bearer\s+)([a-zA-Z0-9_\-\.]{20,})'), r'\1***MASKED***'),
    # Authorization headers
    (re.compile(r'(?i)(authorization\s*[=:]\s*)["\']?([a-zA-Z0-9_\-\.]{20,})["\']?'), r'\1***MASKED***'),
    # AWS access keys
    (re.compile(r'AKIA[0-9A-Z]{16}'), '***AWS_KEY_MASKED***'),
    # AWS secret keys
    (re.compile(r'(?i)(aws_secret_access_key\s*[=:]\s*)["\']?([a-zA-Z0-9/+=]{40})["\']?'), r'\1***MASKED***'),
    # GitHub tokens
    (re.compile(r'gh[ps]_[a-zA-Z0-9]{36,}'), '***GITHUB_TOKEN_MASKED***'),
    (re.compile(r'github_pat_[a-zA-Z0-9_]{22,}'), '***GITHUB_PAT_MASKED***'),
    # Generic secrets (SECRET=, secret=, password=, passwd=)
    (re.compile(r'(?i)((?:secret|password|passwd|token|credential)[_\s]*[=:]\s*)["\']?([^\s"\']{8,})["\']?'), r'\1***MASKED***'),
    # Private keys (beginning)
    (re.compile(r'-----BEGIN [A-Z]+ PRIVATE KEY-----'), '***PRIVATE_KEY_MASKED***'),
    # Environment variable exports with secrets
    (re.compile(r'(?i)(export\s+(?:.*?(?:key|secret|token|password|credential).*?)=)["\']?([^\s"\']+)["\']?'), r'\1***MASKED***'),
]


def mask_secrets(text: str) -> str:
    """
    Mask sensitive data (API keys, tokens, passwords) in text.

    SEC-010: Prevents accidental exposure of secrets in logs and output.

    Args:
        text: Text that may contain secrets

    Returns:
        Text with secrets masked
    """
    if not text:
        return text

    result = text
    for pattern, replacement in SECRET_PATTERNS:
        result = pattern.sub(replacement, result)

    return result


# SEC-014: Shell injection prevention patterns
SHELL_INJECTION_PATTERNS = [
    # Command substitution
    re.compile(r'\$\([^)]+\)'),  # $(command)
    re.compile(r'`[^`]+`'),       # `command`
    # Variable expansion exploits
    re.compile(r'\$\{[^}]*[;&|`][^}]*\}'),  # ${var;cmd}
    # Multiple command execution (if not expected)
    re.compile(r'[;&|]{2,}'),  # ;; or && or ||  (double or more)
    # Null byte injection
    re.compile(r'\x00'),
]


def validate_command_safety(command: str, allow_shell_features: bool = True) -> None:
    """
    Validate command for shell injection vulnerabilities.

    SEC-014: Shell injection prevention.

    Args:
        command: The command to validate
        allow_shell_features: If False, rejects pipes, redirects, etc.

    Raises:
        ValueError: If command contains injection patterns
    """
    if not command or not command.strip():
        raise ValueError("Command cannot be empty")

    # Check for null bytes (serious injection vector)
    if '\x00' in command:
        raise ValueError("Command contains null bytes, which is not allowed for security reasons.")

    # Check for very long commands (potential DoS)
    if len(command) > 10000:
        raise ValueError(f"Command exceeds maximum length (10000 chars). Length: {len(command)}")

    if not allow_shell_features:
        # Strict mode: reject shell metacharacters
        shell_chars = set(';|&`$(){}[]<>')
        found_chars = set(command) & shell_chars
        if found_chars:
            raise ValueError(
                f"Command contains shell metacharacters which are not allowed in strict mode: "
                f"{', '.join(repr(c) for c in found_chars)}"
            )


def escape_shell_arg(arg: str) -> str:
    """
    Safely escape a shell argument for use in commands.

    SEC-014: Shell injection prevention.

    Args:
        arg: The argument to escape

    Returns:
        Safely escaped argument string
    """
    # Use shlex.quote for proper escaping
    return shlex.quote(arg)


def build_safe_command(cmd: str, args: list[str]) -> str:
    """
    Build a safe command string with properly escaped arguments.

    SEC-014: Shell injection prevention.

    Args:
        cmd: The base command
        args: List of arguments to append

    Returns:
        Safe command string with escaped arguments
    """
    escaped_args = [escape_shell_arg(arg) for arg in args]
    return f"{cmd} {' '.join(escaped_args)}"


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

    success: bool
    stdout: str
    stderr: str
    exit_code: int
    command: str
    cwd: Optional[Path] = None


class BashOperations:
    """
    Bash operations for groknroll agent

    Features:
    - Execute shell commands with output capture
    - PTY support for interactive commands
    - Working directory management
    - Environment variable handling
    - Command history and safety checks
    """

    def __init__(self, project_path: Path, default_shell: str = "/bin/bash", timeout: int = 300):
        """
        Initialize bash operations

        Args:
            project_path: Root project path (default working directory)
            default_shell: Shell to use for execution
            timeout: Default command timeout in seconds
        """
        self.project_path = project_path.resolve()
        self.default_shell = default_shell
        self.default_timeout = timeout
        self.command_history: list[BashResult] = []

        # Dangerous commands that require confirmation (SEC-002: Complete patterns)
        self.dangerous_commands = {
            # Original patterns
            "rm -rf /",
            "dd if=",
            "mkfs",
            ":(){ :|:& };:",  # Fork bomb
            # Privilege escalation
            "chmod +s",
            "chmod 777",
            "chmod -R 777",
            "chown -R",
            "sudo ",
            "su -",
            # Remote code execution via download
            "curl | bash",
            "curl | sh",
            "wget | bash",
            "wget | sh",
            "curl|bash",
            "curl|sh",
            "wget|bash",
            "wget|sh",
            # Code evaluation
            "eval ",
            "exec ",
            # Destructive operations
            "> /dev/sd",
            "mv /* ",
            "rm /* ",
            # Process killing
            "kill -9 -1",
            "killall -9",
            # System modification
            "> /etc/passwd",
            "> /etc/shadow",
            "rm -rf /*",
        }

    def execute(
        self,
        command: str,
        cwd: Optional[Path] = None,
        env: Optional[dict[str, str]] = None,
        timeout: Optional[int] = None,
        shell: bool = True,
        capture_output: bool = True,
    ) -> BashResult:
        """
        Execute bash command

        Args:
            command: Command to execute
            cwd: Working directory (defaults to project_path)
            env: Environment variables (merged with os.environ)
            timeout: Command timeout in seconds
            shell: Execute in shell (required for pipes, redirects, etc.)
            capture_output: Capture stdout/stderr

        Returns:
            BashResult
        """
        # Safety check
        if self._is_dangerous(command):
            return BashResult(
                success=False,
                stdout="",
                stderr=f"Dangerous command blocked: {mask_secrets(command)}",
                exit_code=-1,
                command=mask_secrets(command),  # SEC-010: Mask secrets in stored command
                cwd=cwd,
            )

        # Resolve working directory
        work_dir = self._resolve_path(cwd) if cwd else self.project_path

        # Prepare environment
        exec_env = os.environ.copy()
        if env:
            exec_env.update(env)

        # Set timeout
        exec_timeout = timeout or self.default_timeout

        try:
            # Execute command
            if shell:
                result = subprocess.run(
                    command,
                    shell=True,
                    cwd=str(work_dir),
                    env=exec_env,
                    capture_output=capture_output,
                    text=True,
                    timeout=exec_timeout,
                    executable=self.default_shell,
                )
            else:
                # Parse command into args
                args = shlex.split(command)
                result = subprocess.run(
                    args,
                    cwd=str(work_dir),
                    env=exec_env,
                    capture_output=capture_output,
                    text=True,
                    timeout=exec_timeout,
                )

            # SEC-010: Mask secrets in stored command and output
            masked_command = mask_secrets(command)
            bash_result = BashResult(
                success=result.returncode == 0,
                stdout=mask_secrets(result.stdout) if capture_output else "",
                stderr=mask_secrets(result.stderr) if capture_output else "",
                exit_code=result.returncode,
                command=masked_command,
                cwd=work_dir,
            )

            # Add to history
            self.command_history.append(bash_result)

            return bash_result

        except subprocess.TimeoutExpired:
            return BashResult(
                success=False,
                stdout="",
                stderr=f"Command timed out after {exec_timeout} seconds",
                exit_code=-1,
                command=mask_secrets(command),
                cwd=work_dir,
            )
        except Exception as e:
            return BashResult(
                success=False,
                stdout="",
                stderr=f"Error executing command: {mask_secrets(str(e))}",
                exit_code=-1,
                command=mask_secrets(command),
                cwd=work_dir,
            )

    def execute_interactive(
        self, command: str, cwd: Optional[Path] = None, env: Optional[dict[str, str]] = None
    ) -> BashResult:
        """
        Execute command with PTY (for interactive commands like vim, top, etc.)

        Args:
            command: Command to execute
            cwd: Working directory
            env: Environment variables

        Returns:
            BashResult
        """
        # Safety check
        if self._is_dangerous(command):
            return BashResult(
                success=False,
                stdout="",
                stderr=f"Dangerous command blocked: {mask_secrets(command)}",
                exit_code=-1,
                command=mask_secrets(command),  # SEC-010: Mask secrets in stored command
                cwd=cwd,
            )

        # Resolve working directory
        work_dir = self._resolve_path(cwd) if cwd else self.project_path

        # Prepare environment
        exec_env = os.environ.copy()
        if env:
            exec_env.update(env)

        try:
            # Create PTY
            master, slave = pty.openpty()

            # Execute command in PTY
            process = subprocess.Popen(
                command,
                shell=True,
                cwd=str(work_dir),
                env=exec_env,
                stdin=slave,
                stdout=slave,
                stderr=slave,
                executable=self.default_shell,
            )

            # Close slave (parent doesn't need it)
            os.close(slave)

            # Wait for process to complete
            process.wait()

            # Read output
            output = b""
            while True:
                try:
                    # Use select to check if data is available
                    ready, _, _ = select.select([master], [], [], 0.1)
                    if ready:
                        chunk = os.read(master, 4096)
                        if not chunk:
                            break
                        output += chunk
                    else:
                        break
                except OSError:
                    break

            os.close(master)

            # SEC-010: Mask secrets in stored command and output
            masked_command = mask_secrets(command)
            decoded_output = output.decode("utf-8", errors="replace")
            bash_result = BashResult(
                success=process.returncode == 0,
                stdout=mask_secrets(decoded_output),
                stderr="",
                exit_code=process.returncode,
                command=masked_command,
                cwd=work_dir,
            )

            self.command_history.append(bash_result)

            return bash_result

        except Exception as e:
            return BashResult(
                success=False,
                stdout="",
                stderr=f"Error executing interactive command: {mask_secrets(str(e))}",
                exit_code=-1,
                command=mask_secrets(command),
                cwd=work_dir,
            )

    def execute_pipeline(
        self,
        commands: list[str],
        cwd: Optional[Path] = None,
        env: Optional[dict[str, str]] = None,
        timeout: Optional[int] = None,
    ) -> BashResult:
        """
        Execute pipeline of commands (command1 | command2 | ...)

        Args:
            commands: List of commands to pipe together
            cwd: Working directory
            env: Environment variables
            timeout: Command timeout

        Returns:
            BashResult
        """
        # Create pipeline command
        pipeline = " | ".join(commands)

        return self.execute(command=pipeline, cwd=cwd, env=env, timeout=timeout, shell=True)

    def execute_script(
        self,
        script_path: Path,
        args: Optional[list[str]] = None,
        cwd: Optional[Path] = None,
        env: Optional[dict[str, str]] = None,
        timeout: Optional[int] = None,
    ) -> BashResult:
        """
        Execute bash script

        Args:
            script_path: Path to script file
            args: Script arguments
            cwd: Working directory
            env: Environment variables
            timeout: Command timeout

        Returns:
            BashResult
        """
        script_file = self._resolve_path(script_path)

        if not script_file.exists():
            return BashResult(
                success=False,
                stdout="",
                stderr=f"Script not found: {script_path}",
                exit_code=-1,
                command=f"bash {script_path}",
                cwd=cwd,
            )

        # Build command
        command_parts = [self.default_shell, str(script_file)]
        if args:
            command_parts.extend(args)

        command = " ".join(shlex.quote(part) for part in command_parts)

        return self.execute(command=command, cwd=cwd, env=env, timeout=timeout, shell=False)

    def get_history(self, limit: int = 10) -> list[BashResult]:
        """Get recent command history"""
        return self.command_history[-limit:]

    def clear_history(self) -> None:
        """Clear command history"""
        self.command_history.clear()

    # =========================================================================
    # Common Commands (Convenience Methods)
    # =========================================================================

    def ls(self, path: Optional[Path] = None, options: str = "-la") -> BashResult:
        """List directory contents"""
        target = self._resolve_path(path) if path else self.project_path
        return self.execute(f"ls {options} {shlex.quote(str(target))}")

    def pwd(self) -> BashResult:
        """Print working directory"""
        return self.execute("pwd", cwd=self.project_path)

    def which(self, command: str) -> BashResult:
        """Find command location"""
        return self.execute(f"which {shlex.quote(command)}")

    def env_vars(self) -> BashResult:
        """List environment variables"""
        return self.execute("env")

    def ps(self, options: str = "aux") -> BashResult:
        """List processes"""
        return self.execute(f"ps {options}")

    def grep(self, pattern: str, path: Optional[Path] = None, options: str = "-r") -> BashResult:
        """Search for pattern in files"""
        target = self._resolve_path(path) if path else self.project_path
        return self.execute(f"grep {options} {shlex.quote(pattern)} {shlex.quote(str(target))}")

    def find(
        self,
        path: Optional[Path] = None,
        name: Optional[str] = None,
        type_filter: Optional[str] = None,
    ) -> BashResult:
        """Find files"""
        target = self._resolve_path(path) if path else self.project_path
        command_parts = ["find", shlex.quote(str(target))]

        if type_filter:
            command_parts.extend(["-type", type_filter])

        if name:
            command_parts.extend(["-name", shlex.quote(name)])

        return self.execute(" ".join(command_parts))

    # =========================================================================
    # Helper Methods
    # =========================================================================

    def _resolve_path(self, path: Path) -> Path:
        """Resolve path relative to project root"""
        if path.is_absolute():
            return path.resolve()
        return (self.project_path / path).resolve()

    def _is_dangerous(self, command: str) -> bool:
        """Check if command is potentially dangerous"""
        # Check against known dangerous patterns
        for dangerous in self.dangerous_commands:
            if dangerous in command:
                return True

        # Additional pattern checks (SEC-002)
        dangerous_patterns = [
            "rm -rf /",
            "> /dev/sda",
            "> /dev/sdb",
            "> /dev/nvme",
            "chmod -R 777 /",
            # Pipe to shell variations
            "| bash",
            "| sh",
            "|bash",
            "|sh",
            # Python/Perl/Ruby code execution
            "python -c",
            "python3 -c",
            "perl -e",
            "ruby -e",
            # Netcat reverse shells
            "nc -e",
            "ncat -e",
            "/dev/tcp/",
            "/dev/udp/",
        ]

        for pattern in dangerous_patterns:
            if pattern in command:
                return True

        # Check for base64 decode piped to shell (common obfuscation technique)
        import re
        if re.search(r'base64\s+-d.*\|\s*(bash|sh)', command):
            return True

        return False

    def test_command_available(self, command: str) -> bool:
        """Test if command is available in PATH"""
        result = self.which(command)
        return result.success

    def get_shell_version(self) -> BashResult:
        """Get shell version"""
        return self.execute(f"{self.default_shell} --version")
