"""Bash command execution tool for Promethian.

Provides shell command execution with safety controls.
This is a built-in tool that's always available to the agent.
"""

from __future__ import annotations

import asyncio
import os
import shlex
import shutil
import sys
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Any


@lru_cache(maxsize=1)
def get_python_executable() -> str:
    """
    Get the correct Python executable name for this system.

    Returns 'python3' if available, otherwise 'python', or the full path
    to sys.executable as fallback.
    """
    # First check if python3 is available
    if shutil.which("python3"):
        return "python3"
    # Then check python
    if shutil.which("python"):
        return "python"
    # Fallback to current interpreter
    return sys.executable


@dataclass
class BashResult:
    """Result from a bash command."""
    success: bool
    exit_code: int
    stdout: str
    stderr: str
    command: str
    timed_out: bool = False


class BashTool:
    """
    Bash command execution tool.

    Provides:
    - run(command) - Execute a shell command
    - run_in_dir(command, directory) - Execute in specific directory

    Safety features:
    - Timeout protection (default 120s)
    - Output truncation (default 100KB)
    - Optional command allowlist/blocklist
    - Working directory restrictions
    """

    def __init__(
        self,
        timeout: int = 120,
        max_output: int = 100_000,
        working_dir: str | Path | None = None,
        blocked_commands: list[str] | None = None,
    ):
        self.timeout = timeout
        self.max_output = max_output
        self.working_dir = Path(working_dir) if working_dir else Path.cwd()
        self.blocked_commands = set(blocked_commands or [
            "rm -rf /",
            "rm -rf /*",
            "mkfs",
            "dd if=/dev/zero",
            ":(){:|:&};:",  # Fork bomb
        ])

    def _is_blocked(self, command: str) -> str | None:
        """Check if command is blocked. Returns reason if blocked, None otherwise."""
        command_lower = command.lower().strip()

        for blocked in self.blocked_commands:
            if blocked.lower() in command_lower:
                return f"Command contains blocked pattern: {blocked}"

        return None

    def _truncate_output(self, output: str) -> str:
        """Truncate output if too long."""
        if len(output) > self.max_output:
            return output[:self.max_output] + f"\n\n... (truncated, {len(output)} total bytes)"
        return output

    def _normalize_python_command(self, command: str) -> str:
        """
        Normalize Python commands to use the correct executable.

        Replaces standalone 'python ' with the detected python executable
        (usually 'python3' on macOS/Linux).
        """
        python_exe = get_python_executable()
        if python_exe == "python":
            return command  # No change needed

        # Replace 'python ' at start of command or after && or ; or |
        import re
        # Match 'python ' at word boundary but not 'python3' or 'pythonw'
        patterns = [
            (r'^python\s', f'{python_exe} '),  # Start of command
            (r'(&&\s*)python\s', rf'\1{python_exe} '),  # After &&
            (r'(;\s*)python\s', rf'\1{python_exe} '),  # After ;
            (r'(\|\s*)python\s', rf'\1{python_exe} '),  # After |
            (r'(\$\()python\s', rf'\1{python_exe} '),  # In subshell $(python ...)
        ]

        result = command
        for pattern, replacement in patterns:
            result = re.sub(pattern, replacement, result)

        return result

    async def run(
        self,
        command: str,
        timeout: int | None = None,
        working_dir: str | Path | None = None,
        env: dict[str, str] | None = None,
    ) -> BashResult:
        """
        Execute a bash command.

        Args:
            command: The command to execute
            timeout: Optional timeout override in seconds
            working_dir: Optional working directory override
            env: Optional environment variables to add

        Returns:
            BashResult with output and status
        """
        # Normalize Python commands (python -> python3 on systems where needed)
        command = self._normalize_python_command(command)

        # Check for blocked commands
        blocked_reason = self._is_blocked(command)
        if blocked_reason:
            return BashResult(
                success=False,
                exit_code=-1,
                stdout="",
                stderr=f"Command blocked: {blocked_reason}",
                command=command,
            )

        # Set up environment
        run_env = os.environ.copy()
        if env:
            run_env.update(env)

        # Set up working directory
        cwd = Path(working_dir) if working_dir else self.working_dir

        try:
            proc = await asyncio.create_subprocess_shell(
                command,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                cwd=str(cwd),
                env=run_env,
            )

            try:
                stdout_bytes, stderr_bytes = await asyncio.wait_for(
                    proc.communicate(),
                    timeout=timeout or self.timeout,
                )
                timed_out = False
            except asyncio.TimeoutError:
                proc.kill()
                await proc.wait()
                return BashResult(
                    success=False,
                    exit_code=-1,
                    stdout="",
                    stderr=f"Command timed out after {timeout or self.timeout}s",
                    command=command,
                    timed_out=True,
                )

            stdout = self._truncate_output(stdout_bytes.decode(errors="replace"))
            stderr = self._truncate_output(stderr_bytes.decode(errors="replace"))

            return BashResult(
                success=proc.returncode == 0,
                exit_code=proc.returncode or 0,
                stdout=stdout,
                stderr=stderr,
                command=command,
                timed_out=timed_out,
            )

        except Exception as e:
            return BashResult(
                success=False,
                exit_code=-1,
                stdout="",
                stderr=f"Failed to execute command: {e}",
                command=command,
            )

    async def run_script(
        self,
        script: str,
        timeout: int | None = None,
        working_dir: str | Path | None = None,
    ) -> BashResult:
        """
        Execute a multi-line bash script.

        Args:
            script: The script content (multiple lines)
            timeout: Optional timeout in seconds
            working_dir: Optional working directory

        Returns:
            BashResult with output and status
        """
        # Use bash -c with the script
        # Escape for safe passing
        escaped_script = script.replace("'", "'\"'\"'")
        command = f"bash -c '{escaped_script}'"

        return await self.run(
            command=command,
            timeout=timeout,
            working_dir=working_dir,
        )

    def get_tool_schemas(self) -> list[dict]:
        """Get Claude tool schemas for bash actions."""
        return [
            {
                "name": "bash",
                "description": (
                    "Execute a bash shell command. Use for running system commands, "
                    "git operations, file operations, installing packages, running tests, "
                    "and any other shell tasks. Commands run in a subprocess with timeout protection."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "command": {
                            "type": "string",
                            "description": "The bash command to execute"
                        },
                        "working_dir": {
                            "type": "string",
                            "description": "Optional working directory for the command"
                        },
                        "timeout": {
                            "type": "integer",
                            "description": "Optional timeout in seconds (default 120)"
                        }
                    },
                    "required": ["command"]
                }
            },
            {
                "name": "bash_script",
                "description": (
                    "Execute a multi-line bash script. Use for complex operations "
                    "that require multiple commands, loops, or conditionals."
                ),
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "script": {
                            "type": "string",
                            "description": "The bash script content (can be multiple lines)"
                        },
                        "working_dir": {
                            "type": "string",
                            "description": "Optional working directory for the script"
                        },
                        "timeout": {
                            "type": "integer",
                            "description": "Optional timeout in seconds (default 120)"
                        }
                    },
                    "required": ["script"]
                }
            },
        ]

    async def execute_tool(self, name: str, params: dict) -> BashResult:
        """Execute a bash tool by name."""
        if name == "bash":
            return await self.run(
                command=params["command"],
                timeout=params.get("timeout"),
                working_dir=params.get("working_dir"),
            )
        elif name == "bash_script":
            return await self.run_script(
                script=params["script"],
                timeout=params.get("timeout"),
                working_dir=params.get("working_dir"),
            )
        else:
            return BashResult(
                success=False,
                exit_code=-1,
                stdout="",
                stderr=f"Unknown bash tool: {name}",
                command="",
            )


# Singleton bash tool instance
_bash: BashTool | None = None


def get_bash(
    timeout: int = 120,
    max_output: int = 100_000,
    working_dir: str | Path | None = None,
) -> BashTool:
    """Get the global bash tool instance."""
    global _bash
    if _bash is None:
        _bash = BashTool(
            timeout=timeout,
            max_output=max_output,
            working_dir=working_dir,
        )
    return _bash


def reset_bash() -> None:
    """Reset the global bash tool instance."""
    global _bash
    _bash = None
