"""
Shell Scaffold - Terminal-like shell interface with persistent session state

Provides LLM agents with a familiar terminal experience including command execution,
history navigation, and intelligent output management.
"""

import os
import re
import uuid
import subprocess
from typing import Optional, Union, Dict, List, Callable, Tuple, Any
from datetime import datetime
from pathlib import Path

from egregore.core.context_scaffolds.xml_base import BaseXMLScaffold
from egregore.core.context_scaffolds.data_types import StateOperatorResult
from egregore.core.context_scaffolds.decorators import operation
from egregore.core.context_scaffolds.notifications import notification
from egregore.core.context_scaffolds.approval import ApprovalResult, PolicyResult
from egregore.core.context_management.pact.components.core import XMLComponent

from .shell_state import ShellState, CommandEntry


class Shell(BaseXMLScaffold):
    """
    Terminal-like shell interface with command history and navigation.

    Provides persistent shell session with:
    - Command execution with timeout protection
    - Command history with navigation controls
    - Security system with configurable levels
    - Safe file deletion with recovery
    - Token-aware output management
    - Cross-platform support (Windows/POSIX)
    """

    type: str = "shell"
    state: ShellState = ShellState()

    # =====================================================================
    # V2 ENHANCEMENTS - Enhanced user experience for Shell operations
    # =====================================================================

    OPERATION_SYNONYMS: Dict[str, List[str]] = {
        "execute": ["run", "exec", "cmd", "command", "shell", "bash", "$"],
        "scroll_up": ["up", "prev", "previous", "back", "earlier"],
        "scroll_down": ["down", "next", "forward", "later", "recent"],
        "list_history": ["history", "hist", "commands", "log"],
    }

    PARAMETER_SUGGESTIONS: Dict[Tuple[str, str], Union[str, Callable]] = {
        ("execute", "command"): lambda self: f"Shell command to run (cwd: {self.state.current_directory})",
        ("execute", "timeout"): lambda self: f"Timeout in seconds (default: {self.state.command_timeout}s)",
    }

    ERROR_RECOVERY_PATTERNS: Dict[str, List[str]] = {
        r"command not found|No such file": [
            "Check if the command exists with 'which <command>'",
            "Verify you're in the correct directory with 'pwd'",
            "List files in current directory with 'ls'"
        ],
        r"permission denied|Permission denied": [
            "Check file permissions with 'ls -la'",
            "Consider using 'chmod' to modify permissions",
            "Verify you have access to the file/directory"
        ],
        r"blocked.*command": [
            "This command is blocked for safety",
            "Check available commands with list_history()",
            "Use safe alternatives or adjust security settings"
        ]
    }

    def __init__(self,
                 position: str = "active",
                 cwd: Optional[str] = None,
                 security: Union[str, Dict] = "secure",
                 rm_to_mv: bool = True,
                 recovery_dir: str = ".temp",
                 viewport_end: int = -1,
                 viewport_size: int = 8,
                 max_output_tokens: int = 800,
                 command_timeout: int = 30,
                 shell_executable: str = "/bin/bash",
                 **kwargs):
        """
        Initialize Shell scaffold with custom configuration.

        Args:
            position: Context tree position ("active", "system", "history")
            cwd: Working directory (overrides agent state)
            security: Security configuration (level string or detailed dict)
            rm_to_mv: Convert rm commands to mv for file recovery
            recovery_dir: Directory for recovered files
            viewport_end: History viewport end position (-1 = most recent)
            viewport_size: Commands in history section
            max_output_tokens: Per-command output limit
            command_timeout: Default command timeout in seconds
            shell_executable: Which shell to use
            **kwargs: Additional BaseContextScaffold arguments
        """
        # Call parent init first - now accepts **kwargs
        super().__init__(position=position, **kwargs)

        # Initialize state with configuration values
        self.state.viewport_end = viewport_end
        self.state.viewport_size = viewport_size
        self.state.max_output_tokens = max_output_tokens
        self.state.command_timeout = command_timeout
        self.state.shell_executable = shell_executable
        self.state.rm_to_mv = rm_to_mv
        self.state.recovery_dir = recovery_dir

        # Initialize security configuration
        self.state.security_config = self._resolve_security_config(security)

        # Generate unique session ID
        self.state.session_id = f"shell_{str(uuid.uuid4())[:8]}"

        # Platform detection for cross-platform compatibility
        # NOTE: Windows support disabled - not verified yet
        # self.state.is_windows = os.name == 'nt'
        self.state.is_windows = False  # Temporarily disabled
        self.state.is_posix = os.name == 'posix'
        self.state.platform_name = os.name

        # Set platform-appropriate shell executable if not explicitly provided
        # NOTE: Windows shell selection disabled - POSIX only for now
        # if shell_executable == "/bin/bash":  # Default was provided
        #     if self.state.is_windows:
        #         self.state.shell_executable = "cmd.exe"
        #     else:
        #         self.state.shell_executable = "/bin/bash"
        # else:
        #     # User provided explicit shell, use as-is
        #     self.state.shell_executable = shell_executable

        # POSIX-only for now (Windows support TBD)
        self.state.shell_executable = shell_executable

        # Initialize working directory with flexible priority:
        # 1. Constructor cwd parameter (highest priority)
        # 2. Agent state cwd (if agent exists and has cwd set)
        # 3. Current working directory via os.getcwd() (fallback only)

        if cwd is not None:
            # Explicit constructor parameter takes highest priority
            working_directory = os.path.abspath(cwd)
        elif hasattr(self, 'agent') and self.agent and hasattr(self.agent.state, 'cwd') and self.agent.state.cwd:
            # Agent state cwd exists and is set
            working_directory = self.agent.state.cwd
        else:
            # Fallback to current directory only if no other option
            working_directory = os.getcwd()

        # Set both state properties for consistency
        self.state.current_directory = working_directory
        self.state.cwd = working_directory

        # Update agent state if agent exists (for cross-scaffold coordination)
        if hasattr(self, 'agent') and self.agent:
            self.agent.state.cwd = working_directory

        # Display file recovery info on initialization
        if self.state.rm_to_mv:
            print("ℹ️  SHELL FILE RECOVERY ENABLED")
            print(f"   • 'rm' commands will move files to {self.state.recovery_dir}/ directory for recovery")
            print("   • Use scaffold.list_temp_files() to see recoverable files")
            print("   • Use scaffold.restore_from_temp(filename) to recover files")
            print("   • Set rm_to_mv=False to disable this behavior")

    def _resolve_security_config(self, security: Union[str, Dict]) -> Dict:
        """Resolve unified security parameter into internal configuration."""
        # Built-in security levels
        SECURITY_LEVELS = {
            "disabled": {
                "default_action": "allow",
                "allow_commands": [],
                "prompt_commands": [],
                "block_commands": [],
                "block_patterns": [r"sudo rm -rf /.*"]  # Still block extremely dangerous
            },
            "permissive": {
                "default_action": "allow",
                "allow_commands": [],
                "prompt_commands": ["sudo", "systemctl", "mount", "dd"],
                "block_commands": [],
                "block_patterns": [r"sudo rm -rf /.*", r"dd if=/dev/.*"]
            },
            "secure": {  # Default level
                "default_action": "prompt",
                "allow_commands": ["ls", "pwd", "cd", "cat", "grep", "find", "echo"],
                "prompt_commands": ["sudo", "systemctl", "chmod", "chown", "mount", "umount", "dd", "fdisk"],
                "block_commands": [],
                "block_patterns": [r"sudo rm -rf /.*", r"dd if=/dev/.*", r"mkfs.*"]
            },
            "strict": {
                "default_action": "block",
                "allow_commands": ["ls", "pwd", "cd"],
                "prompt_commands": ["cat", "grep", "mkdir", "cp", "mv"],
                "block_commands": [],
                "block_patterns": [r".*rm.*", r".*sudo.*", r".*dd.*"]
            },
            "paranoid": {
                "default_action": "block",
                "allow_commands": ["ls", "pwd"],
                "prompt_commands": ["cd"],
                "block_commands": [],
                "block_patterns": [r".*"]  # Block everything not explicitly allowed
            }
        }

        # Start with base configuration
        if isinstance(security, str):
            # Simple string level
            base_config = SECURITY_LEVELS.get(security, SECURITY_LEVELS["secure"]).copy()
        else:
            # Dictionary configuration - get base level first
            level = security.get("level", "secure")
            base_config = SECURITY_LEVELS.get(level, SECURITY_LEVELS["secure"]).copy()

            # Apply overrides and extensions
            if "allow" in security:
                if isinstance(security["allow"], list):
                    base_config["allow_commands"].extend(security["allow"])

            if "block" in security:
                if isinstance(security["block"], list):
                    base_config["block_commands"].extend(security["block"])

            if "prompt" in security:
                if isinstance(security["prompt"], list):
                    base_config["prompt_commands"].extend(security["prompt"])

            # Add per-command mappings
            for key, action in security.items():
                if key not in ["level", "allow", "block", "prompt", "rules"] and isinstance(action, str):
                    if action == "allow":
                        base_config["allow_commands"].append(key)
                    elif action == "prompt":
                        base_config["prompt_commands"].append(key)
                    elif action == "block":
                        base_config["block_commands"].append(key)

            # Add pattern rules (highest priority)
            if "rules" in security:
                base_config["pattern_rules"] = security["rules"]

        return base_config

    def render(self):
        """Clean BaseXMLScaffold render - return XMLComponents directly!"""
        # Set XML attributes
        self.xml.attrs['session'] = self.state.session_id
        self.xml.attrs['cwd'] = self.state.current_directory
        self.xml.attrs['history_count'] = str(len(self.state.command_history))

        if not self.state.command_history:
            return [XMLComponent(type="shell_status", content="[No commands executed yet]")]

        # Return viewport of command history + active command
        viewport_commands = self._get_history_viewport()
        result_components = []

        # Add history section
        if viewport_commands:
            for cmd in viewport_commands:
                result_components.append(cmd)

        # Add active command if different from last in history
        if self.state.active_command and (
            not self.state.command_history or
            self.state.active_command != self.state.command_history[-1]
        ):
            result_components.append(self.state.active_command)

        return result_components if result_components else [XMLComponent(type="shell_status", content="[Shell ready]")]

    @notification
    def safety_warning(self) -> str:
        """Show safety warnings when needed."""
        if self.state.rm_to_mv and self.state.active_command:
            if any(blocked in self.state.active_command.input.lower()
                   for blocked in ['sudo', 'rm', 'systemctl']):
                return "⚠️ Shell security active - dangerous commands may be blocked or modified"
        return ""

    # =====================================================================
    # CORE OPERATIONS (@operation decorated - LLM accessible)
    # =====================================================================

    @operation
    def execute(self, command: str, timeout: Optional[int] = None) -> StateOperatorResult:
        """
        Execute shell command in persistent session with automatic safety features.

        Args:
            command: Shell command to execute
            timeout: Command timeout in seconds (uses default if None)

        Returns:
            StateOperatorResult with execution status and output information
        """
        try:
            # Use provided timeout or default
            cmd_timeout = timeout if timeout is not None else self.state.command_timeout

            # Execute command using subprocess
            # NOTE: Windows-specific conditional disabled - POSIX only
            process = subprocess.Popen(
                command,
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                cwd=self.state.current_directory,
                executable=self.state.shell_executable  # Always use configured shell (POSIX)
                # Original: executable=self.state.shell_executable if not self.state.is_windows else None
            )

            try:
                stdout, stderr = process.communicate(timeout=cmd_timeout)
                exit_code = process.returncode
            except subprocess.TimeoutExpired:
                process.kill()
                stdout, stderr = process.communicate()
                return StateOperatorResult(
                    success=False,
                    message=f"Command timed out after {cmd_timeout} seconds"
                )

            # Create command entry
            cmd_entry = CommandEntry(
                input=command,
                output=stdout.decode('utf-8', errors='replace'),
                error=stderr.decode('utf-8', errors='replace'),
                exit_code=exit_code,
                working_directory=self.state.current_directory,
                timestamp=datetime.now().isoformat()
            )

            # Update state
            self.state.command_history.append(cmd_entry)
            self.state.active_command = cmd_entry

            # Update working directory if cd command
            if command.strip().startswith('cd '):
                new_dir = command.strip()[3:].strip()
                if new_dir:
                    try:
                        os.chdir(new_dir)
                        self.state.current_directory = os.getcwd()
                        self.state.cwd = self.state.current_directory

                        # Update agent state
                        if hasattr(self, 'agent') and self.agent:
                            self.agent.state.cwd = self.state.current_directory
                    except Exception:
                        pass  # Stay in current directory if cd fails

            return StateOperatorResult(
                success=exit_code == 0,
                message=f"Command executed with exit code {exit_code}" if exit_code == 0 else f"Command failed with exit code {exit_code}"
            )

        except Exception as e:
            return StateOperatorResult(
                success=False,
                message=f"Command execution error: {str(e)}"
            )

    @operation
    def clear_history(self) -> StateOperatorResult:
        """Clear all command history display (keeps current directory and session)."""
        self.state.command_history.clear()
        self.state.active_command = None
        return StateOperatorResult(
            success=True,
            message="Command history cleared"
        )

    @operation
    def scroll_up(self, count: int = 5) -> StateOperatorResult:
        """Show earlier commands in history section (scroll back)."""
        history_len = len(self.state.command_history)
        if self.state.viewport_end == -1:
            self.state.viewport_end = history_len - count
        else:
            self.state.viewport_end = max(0, self.state.viewport_end - count)

        return StateOperatorResult(
            success=True,
            message=f"Scrolled up {count} commands"
        )

    @operation
    def scroll_down(self, count: int = 5) -> StateOperatorResult:
        """Show more recent commands in history section (scroll forward)."""
        history_len = len(self.state.command_history)
        if self.state.viewport_end == -1:
            return StateOperatorResult(
                success=True,
                message="Already at most recent commands"
            )

        self.state.viewport_end = min(history_len, self.state.viewport_end + count)

        # If we reached the end, reset to -1
        if self.state.viewport_end >= history_len:
            self.state.viewport_end = -1

        return StateOperatorResult(
            success=True,
            message=f"Scrolled down {count} commands"
        )

    @operation
    def show_recent(self, count: int = 10) -> StateOperatorResult:
        """Jump to most recent N commands in history section."""
        self.state.viewport_end = -1
        self.state.viewport_size = count

        return StateOperatorResult(
            success=True,
            message=f"Showing {count} most recent commands"
        )

    # =====================================================================
    # DEVELOPER-ONLY RECOVERY METHODS (NOT @operation - not LLM accessible)
    # =====================================================================

    def list_temp_files(self) -> List[str]:
        """List files currently in recovery directory."""
        recovery_path = Path(self.state.recovery_dir)
        if not recovery_path.exists():
            return []

        return [f.name for f in recovery_path.iterdir() if f.is_file()]

    def restore_from_temp(self, filename: str, destination: Optional[str] = None) -> bool:
        """Restore a file from recovery directory to specified or current location."""
        recovery_path = Path(self.state.recovery_dir) / filename

        if not recovery_path.exists():
            return False

        # Determine destination
        if destination:
            dest_path = Path(destination) / filename.split('_')[0]  # Remove timestamp
        else:
            dest_path = Path(self.state.current_directory) / filename.split('_')[0]

        try:
            import shutil
            shutil.move(str(recovery_path), str(dest_path))
            return True
        except Exception:
            return False

    def _empty_temp_internal(self) -> None:
        """Internal method: Clear recovery directory (developer only)."""
        recovery_path = Path(self.state.recovery_dir)
        if recovery_path.exists():
            import shutil
            shutil.rmtree(recovery_path)
            recovery_path.mkdir()

    # =====================================================================
    # HELPER METHODS
    # =====================================================================

    def _get_history_viewport(self) -> List[CommandEntry]:
        """Get viewport slice of command history."""
        if not self.state.command_history:
            return []

        history_len = len(self.state.command_history)

        if self.state.viewport_end == -1:
            # Show most recent
            start = max(0, history_len - self.state.viewport_size)
            end = history_len
        else:
            # Show specific range
            end = min(self.state.viewport_end, history_len)
            start = max(0, end - self.state.viewport_size)

        return list(self.state.command_history[start:end])

    def _get_viewport_description(self) -> str:
        """Get human-readable viewport description."""
        if not self.state.command_history:
            return "no commands"

        history_len = len(self.state.command_history)
        viewport = self._get_history_viewport()

        if not viewport:
            return "no commands in viewport"

        start_idx = self.state.command_history.index(viewport[0]) + 1
        end_idx = self.state.command_history.index(viewport[-1]) + 1

        return f"commands {start_idx}-{end_idx} of {history_len} total"

    # =====================================================================
    # PHASE 2: SECURITY ENFORCEMENT
    # =====================================================================

    def execution_policies(self, tool_name: str, tool_params: dict, approval_hook: Callable) -> PolicyResult:
        """Shell execution policies using unified security configuration system"""

        if tool_name != "execute" or 'command' not in tool_params:
            return PolicyResult.ALLOW()

        command = tool_params['command']
        current_dir = self.state.current_directory
        cmd_name = command.split()[0] if command.strip() else ''

        # Resolve security decision with priority system
        security_action = self._resolve_security_action(command, cmd_name)

        if security_action == "block":
            return PolicyResult.BLOCK(f"Command '{cmd_name}' blocked by security policy")
        elif security_action == "allow":
            # Still apply rm_to_mv conversion if enabled (silent safety)
            if self.state.rm_to_mv and cmd_name == 'rm':
                safe_command = self._convert_to_safe_delete(command)
                tool_params['command'] = safe_command
                return PolicyResult.MODIFY("Shell: Converted rm to mv for file recovery")
            return PolicyResult.ALLOW()
        elif security_action == "prompt":
            # Check if command was previously approved and remembered
            if self._is_command_remembered(cmd_name, current_dir):
                # Auto-approve based on learned policy
                if self.state.rm_to_mv and cmd_name == 'rm':
                    safe_command = self._convert_to_safe_delete(command)
                    tool_params['command'] = safe_command
                    return PolicyResult.MODIFY("Shell: Auto-approved + converted rm to mv (policy learned)")
                return PolicyResult.ALLOW()

            # Request approval for this command
            approval_result = approval_hook(
                title="Shell Security Approval",
                message=f"Allow dangerous command '{cmd_name}'?",
                command=command,
                context={
                    'current_directory': current_dir,
                    'rm_to_mv': self.state.rm_to_mv,
                    'platform': self.state.platform_name,
                    'tool_name': tool_name
                }
            )

            # If approved with remember flag, store the approval
            if approval_result.approved and hasattr(approval_result, 'remember') and approval_result.remember:
                self._remember_approval(cmd_name, current_dir)

            return PolicyResult.APPROVAL_REQUIRED(approval_result, f"Command '{cmd_name}' requires approval")

        # Default fallback (should not reach here)
        return PolicyResult.ALLOW()

    def _resolve_security_action(self, command: str, cmd_name: str) -> str:
        """Resolve security action for command using priority system."""
        config = self.state.security_config

        # Priority 1: Pattern rules (highest priority)
        if "pattern_rules" in config:
            for rule in config["pattern_rules"]:
                pattern = rule.get("pattern", "")
                if re.search(pattern, command, re.IGNORECASE):
                    return rule.get("action", "prompt")

        # Priority 2: Block patterns (security level patterns)
        for pattern in config.get("block_patterns", []):
            if re.search(pattern, command, re.IGNORECASE):
                return "block"

        # Priority 3: Direct command lists
        if cmd_name in config.get("block_commands", []):
            return "block"
        elif cmd_name in config.get("allow_commands", []):
            return "allow"
        elif cmd_name in config.get("prompt_commands", []):
            return "prompt"

        # Priority 4: Default action from security level
        return config.get("default_action", "prompt")

    def _is_safe_directory(self, directory: str) -> bool:
        """Check if directory is in safe directories list"""
        safe_dirs = getattr(self.state, 'safe_directories', ['/tmp', '/home/user/sandbox'])
        return any(directory.startswith(safe_dir) for safe_dir in safe_dirs)

    def _is_command_remembered(self, command: str, directory: str) -> bool:
        """Check if command was previously approved and remembered for this directory"""
        if not hasattr(self.state, 'remembered_approvals'):
            return False
        return directory in self.state.remembered_approvals.get(command, [])

    def _remember_approval(self, command: str, directory: str):
        """Remember approval choice for this command in this directory"""
        if not hasattr(self.state, 'remembered_approvals'):
            self.state.remembered_approvals = {}
        if command not in self.state.remembered_approvals:
            self.state.remembered_approvals[command] = []
        if directory not in self.state.remembered_approvals[command]:
            self.state.remembered_approvals[command].append(directory)

    def _convert_to_safe_delete(self, command: str) -> str:
        """Convert rm command to safe move to recovery directory"""
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        temp_suffix = f"{timestamp}_{uuid.uuid4().hex[:8]}"

        # Parse rm command to extract files
        # Simple implementation: converts "rm file.txt" to "mv file.txt .temp/file.txt_TIMESTAMP"
        parts = command.split()
        if len(parts) < 2:
            return command  # Invalid rm command, return as-is

        cmd_name = parts[0]
        files = parts[1:]  # All args after rm

        # Filter out flags (arguments starting with -)
        actual_files = [f for f in files if not f.startswith('-')]

        if not actual_files:
            return command  # No files to delete

        # Create recovery directory if it doesn't exist
        recovery_path = Path(self.state.recovery_dir)
        if not recovery_path.exists():
            recovery_path.mkdir(parents=True)

        # Build mv command for each file
        mv_commands = []
        for file in actual_files:
            # Strip quotes if present
            clean_file = file.strip('"').strip("'")
            recovery_file = f"{recovery_path}/{clean_file}_{temp_suffix}"
            mv_commands.append(f"mv '{clean_file}' '{recovery_file}' > /dev/null 2>&1 || true")

        # Join all mv commands with &&
        return " && ".join(mv_commands)
