"""Shell command execution implementation"""

import os
import subprocess
import asyncio
from typing import Dict, Tuple, Optional, List

from .config import ShellConfig
from .path_validator import CommandValidator, validate_path_traversal


class ShellCommander:
    """Shell command execution class"""
    
    def __init__(self, config: Optional[ShellConfig] = None):
        """Initialize ShellCommander
        
        Args:
            config: Shell configuration
        """
        self.config = config or ShellConfig()
        self._init_validator()
    
    def _init_validator(self):
        """Initialize command validator"""
        allowed_dirs = self.config.allowed_dirs or self.config.cwd_whitelist
        self._validator = CommandValidator(
            command_blacklist=self.config.command_blacklist,
            allowed_dirs=allowed_dirs,
            allow_absolute_paths=self.config.allow_absolute_paths,
        )
    
    def _is_command_blocked(self, command: str) -> bool:
        """Check if command contains any blacklisted patterns
        
        Args:
            command: Command to check
        
        Returns:
            bool: True if command is blocked, False otherwise
        """
        if not self.config.command_blacklist:
            return False
        
        command_lower = command.lower()
        
        if self._contains_script_interpreter(command_lower):
            return True
        
        if self._contains_command_substitution(command_lower):
            return True
        
        if self._contains_absolute_path(command_lower):
            return True
        
        if self._contains_path_traversal(command_lower):
            return True
        
        if self._contains_home_directory(command_lower):
            return True
        
        if self._contains_environment_variable(command_lower):
            return True
        
        if self._contains_pipe(command_lower):
            return True
        
        if self._contains_cd_command(command_lower):
            return True
        
        parts = self._split_command(command)
        
        for part in parts:
            part_lower = part.lower()
            for blocked in self.config.command_blacklist:
                if blocked.lower() in part_lower:
                    return True
        
        return False
    
    def _contains_command_substitution(self, command: str) -> bool:
        """Check for command substitution patterns"""
        import re
        patterns = [r'`[^`]+`', r'\$\([^)]+\)', r'\$\{[^}]+\}']
        for pattern in patterns:
            if re.search(pattern, command):
                return True
        return False
    
    def _contains_absolute_path(self, command: str) -> bool:
        """Check for absolute paths"""
        import re
        if re.search(r'/etc/', command):
            return True
        if re.search(r'/bin/', command):
            return True
        if re.search(r'/usr/', command):
            return True
        if re.search(r'/root/', command):
            return True
        if re.search(r'/var/', command):
            return True
        if re.search(r'/tmp/', command):
            return True
        return False
    
    def _contains_path_traversal(self, command: str) -> bool:
        """Check for path traversal patterns"""
        import re
        patterns = [r'\.\.[\\/]', r'\.\.\.']
        for pattern in patterns:
            if re.search(pattern, command):
                return True
        return False
    
    def _contains_home_directory(self, command: str) -> bool:
        """Check for home directory ~"""
        import re
        if re.search(r'~[\\/]', command):
            return True
        if re.search(r'~$', command):
            return True
        if re.search(r'~\s', command):
            return True
        return False
    
    def _contains_environment_variable(self, command: str) -> bool:
        """Check for environment variable patterns"""
        import re
        if re.search(r'%[A-Za-z_]+%', command):
            return True
        if re.search(r'\$[A-Za-z_]+', command):
            return True
        if re.search(r'\$\{[^}]+\}', command):
            return True
        return False
    
    def _contains_pipe(self, command: str) -> bool:
        """Check for pipe operator"""
        import re
        if re.search(r'\|', command):
            return True
        return False
    
    def _contains_cd_command(self, command: str) -> bool:
        """Check for cd command that could escape working directory
        
        Args:
            command: Command to check (lowercase)
            
        Returns:
            bool: True if cd command detected
        """
        import re
        
        cd_patterns = [
            r'^cd\s+\.\.',
            r'\bcd\s+\.\.',
            r'^cd\s+[a-z]:',
            r'\bcd\s+[a-z]:',
            r'^cd\s+/',
            r'\bcd\s+/',
            r'^pushd',
            r'\bpushd',
        ]
        
        for pattern in cd_patterns:
            if re.search(pattern, command):
                return True
        
        return False
    
    def _contains_script_interpreter(self, command: str) -> bool:
        """Check if command contains script interpreter patterns
        
        Args:
            command: Command to check (lowercase)
            
        Returns:
            bool: True if script interpreter detected
        """
        script_patterns = [
            r'\bbash\b',
            r'\bsh\b',
            r'\bzsh\b',
            r'\bfish\b',
            r'\bpowershell\b',
            r'\bpwsh\b',
            r'\bcmd\b',
            r'\bpython\d*\b',
            r'\bperl\b',
            r'\bruby\b',
            r'\bnode\b',
            r'\bnpm\b',
            r'\byarn\b',
            r'\bpip\b',
            r'\bpip3\b',
            r'\bphp\b',
            r'\blua\b',
            r'\brscript\b',
            r'\bpythonw\b',
            r'\bipython\b',
            r'\bjulia\b',
            r'\br\b',
        ]
        
        import re
        
        for pattern in script_patterns:
            if re.search(pattern, command):
                return True
        
        return False
    
    def _split_command(self, command: str) -> list:
        """Split command by shell separators
        
        Args:
            command: Command to split
            
        Returns:
            List of command parts
        """
        import re
        
        parts = re.split(r'[;\&\|\n\r]+', command)
        
        result = []
        for part in parts:
            part = part.strip()
            if part:
                result.append(part)
        
        return result
    
    def _validate_cwd(self, cwd: str) -> bool:
        """Validate working directory against whitelist
        
        Args:
            cwd: Working directory to validate
        
        Returns:
            bool: True if cwd is valid, False otherwise
        """
        if not self.config.cwd_whitelist:
            return True
        
        for whitelist_path in self.config.cwd_whitelist:
            if cwd.startswith(whitelist_path):
                return True
        
        return False
    
    def _validate_command(self, command: str, cwd: Optional[str] = None) -> None:
        """Validate command for security
        
        Args:
            command: Command to validate
            cwd: Working directory
            
        Raises:
            ValueError: If command is not allowed
        """
        is_valid, error = self._validator.validate(command, cwd)
        if not is_valid:
            raise ValueError(error)
    
    def _validate_path_traversal(self, command: str) -> None:
        """Check for path traversal in command
        
        Args:
            command: Command to validate
            
        Raises:
            ValueError: If path traversal detected
        """
        import re
        
        if self.config.allow_absolute_paths:
            return
        
        if self.config.expand_home_directory and not self.config.block_home_directory:
            return
        
        abs_patterns = [
            r'[A-Za-z]:[\\\/][^\s]+',
            r'(?:^|[\s;|&])(/[^\s]+)',
        ]
        
        for pattern in abs_patterns:
            if re.search(pattern, command):
                raise ValueError("Absolute paths are not allowed")
        
        traversal_patterns = [
            r'(?:^|[\s;|&])(\.\.[\\\/])',
            r'\.\.(?:[\\\/])',
        ]
        
        for pattern in traversal_patterns:
            if re.search(pattern, command, re.IGNORECASE):
                raise ValueError("Path traversal detected")
    
    def _expand_home_directory(self, command: str) -> str:
        """Expand ~ to user home directory on Windows
        
        Args:
            command: Command string
            
        Returns:
            Command with ~ expanded (Windows only)
        """
        if not self.config.expand_home_directory:
            return command
        
        if os.name != 'nt':
            return command
        
        import re
        
        home = os.path.expanduser('~')
        home_pattern = r'~(?=[\\\/]|\s|$)'
        
        return re.sub(home_pattern, home.replace('\\', '\\\\'), command)
    
    def _preprocess_command(self, command: str) -> str:
        """Preprocess command before validation
        
        Args:
            command: Raw command string
            
        Returns:
            Preprocessed command
        """
        import re
        import os
        
        if self.config.block_home_directory:
            home_patterns = [
                r'~[\\\/]',
                r'~(?=\s|$)',
            ]
            for pattern in home_patterns:
                if re.search(pattern, command, re.IGNORECASE):
                    if self.config.expand_home_directory:
                        return self._expand_home_directory(command)
                    raise ValueError("Home directory not allowed")
        
        if self.config.expand_home_directory:
            return self._expand_home_directory(command)
        
        return command
    
    def _init_validator(self):
        """Initialize command validator"""
        import os
        
        allowed_dirs = self.config.allowed_dirs or self.config.cwd_whitelist
        
        if self.config.expand_home_directory:
            home = os.path.expanduser('~')
            if allowed_dirs:
                allowed_dirs = list(allowed_dirs) + [home]
            else:
                allowed_dirs = [home]
        
        allow_absolute = self.config.allow_absolute_paths
        if self.config.expand_home_directory and not self.config.block_home_directory:
            allow_absolute = True
        
        self._validator = CommandValidator(
            command_blacklist=self.config.command_blacklist,
            allowed_dirs=allowed_dirs,
            allow_absolute_paths=allow_absolute,
        )
    
    def execute(self, command: str, cwd: Optional[str] = None, env: Optional[Dict[str, str]] = None, timeout: Optional[int] = None) -> Tuple[int, str, str]:
        """Execute shell command synchronously
        
        Args:
            command: Command to execute
            cwd: Working directory
            env: Environment variables
            timeout: Timeout in seconds
        
        Returns:
            Tuple[int, str, str]: (return code, stdout, stderr)
        """
        target_cwd = cwd or self.config.cwd or os.getcwd()
        
        command = self._preprocess_command(command)
        
        if self._is_command_blocked(command):
            raise ValueError("Command not allowed")
        
        if self._contains_script_interpreter(command.lower()):
            raise ValueError("Script interpreter execution is not allowed")
        
        self._validate_command(command, target_cwd)
        self._validate_path_traversal(command)
        
        if not self._validate_cwd(target_cwd):
            raise ValueError(f"Working directory not allowed: {target_cwd}")
        
        target_env = os.environ.copy()
        if self.config.env:
            target_env.update(self.config.env)
        if env:
            target_env.update(env)
        
        process = subprocess.Popen(
            command,
            shell=True,
            cwd=target_cwd,
            env=target_env,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            encoding=self.config.encoding,
            errors='replace'
        )
        
        try:
            stdout, stderr = process.communicate(timeout=timeout or self.config.timeout)
        except subprocess.TimeoutExpired:
            process.kill()
            stdout, stderr = process.communicate()
            return process.returncode, stdout, stderr
        
        return process.returncode, stdout, stderr
    
    async def execute_async(self, command: str, cwd: Optional[str] = None, env: Optional[Dict[str, str]] = None, timeout: Optional[int] = None) -> Tuple[int, str, str]:
        """Execute shell command asynchronously
        
        Args:
            command: Command to execute
            cwd: Working directory
            env: Environment variables
            timeout: Timeout in seconds
        
        Returns:
            Tuple[int, str, str]: (return code, stdout, stderr)
        """
        target_cwd = cwd or self.config.cwd or os.getcwd()
        
        command = self._preprocess_command(command)
        
        if self._is_command_blocked(command):
            raise ValueError("Command not allowed")
        
        if self._contains_script_interpreter(command.lower()):
            raise ValueError("Script interpreter execution is not allowed")
        
        self._validate_command(command, target_cwd)
        self._validate_path_traversal(command)
        
        if not self._validate_cwd(target_cwd):
            raise ValueError(f"Working directory not allowed: {target_cwd}")
        
        target_env = os.environ.copy()
        if self.config.env:
            target_env.update(self.config.env)
        if env:
            target_env.update(env)
        
        process = await asyncio.create_subprocess_shell(
            command,
            cwd=target_cwd,
            env=target_env,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        
        try:
            if timeout or self.config.timeout:
                stdout_bytes, stderr_bytes = await asyncio.wait_for(
                    process.communicate(),
                    timeout=timeout or self.config.timeout
                )
            else:
                stdout_bytes, stderr_bytes = await process.communicate()
        except asyncio.TimeoutError:
            process.kill()
            stdout_bytes, stderr_bytes = await process.communicate()
        
        try:
            stdout = stdout_bytes.decode(self.config.encoding, errors='replace')
        except (AttributeError, UnicodeDecodeError):
            stdout = ""
        
        try:
            stderr = stderr_bytes.decode(self.config.encoding, errors='replace')
        except (AttributeError, UnicodeDecodeError):
            stderr = ""
        
        return process.returncode, stdout, stderr
    
    def set_config(self, config: ShellConfig):
        """Set configuration
        
        Args:
            config: New shell configuration
        """
        self.config = config
        self._init_validator()
    
    def get_config(self) -> ShellConfig:
        """Get current configuration
        
        Returns:
            ShellConfig: Current configuration
        """
        return self.config


class DefaultShellCommander(ShellCommander):
    """Default shell commander with pre-configured settings"""
    
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        if not hasattr(self, '_initialized'):
            default_config = ShellConfig()
            super().__init__(default_config)
            self._initialized = True
