"""Shell execution tool."""

import logging
import os
import re
import subprocess
from typing import Annotated

from arcade_tdk import ToolContext, tool

from cade_mcp_local.config import DEFAULT_TIMEOUT, get_project_root
from cade_mcp_local.errors import (
    CommandExecutionError,
    CommandTimeoutError,
    PathNotFoundError,
)
from cade_mcp_local.utils import resolve_path

log = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Dangerous command patterns
# ---------------------------------------------------------------------------

_DANGEROUS_PATTERNS: list[tuple[re.Pattern[str], str]] = [
    # rm — catch short flags, long flags, and separate flag groups
    (re.compile(r"\brm\s+(-\w*f\w*\s+)?/\b"), "rm on root path"),
    (re.compile(r"\brm\s+(-\w+\s+)*-\w*r\w*\s"), "recursive delete"),
    (re.compile(r"\brm\s+--recursive\b"), "recursive delete"),
    (re.compile(r"\brm\s+--force\b"), "force delete"),
    # Filesystem destruction
    (re.compile(r"\bmkfs\b"), "filesystem format"),
    (re.compile(r"\bdd\b.*\bof=/dev/"), "raw disk write"),
    (re.compile(r">\s*/dev/[sh]d"), "write to raw device"),
    # Git destructive operations
    (re.compile(r"\bgit\s+push\s+.*--force\b"), "force push"),
    (re.compile(r"\bgit\s+push\s+-f\b"), "force push"),
    (re.compile(r"\bgit\s+push\b.*\+\w"), "refspec force push"),
    (re.compile(r"\bgit\s+reset\s+--hard\b"), "hard reset"),
    (re.compile(r"\bgit\s+clean\s+-\w*f"), "git clean force"),
    # Fork bomb variants
    (re.compile(r":\(\)\s*\{"), "fork bomb"),
    # Permissions
    (re.compile(r"\bchmod\s+-R\s+777\b"), "world-writable recursive chmod"),
    # Pipe URL to shell — any interpreter
    (
        re.compile(r"\bcurl\b.*\|\s*(?:sudo\s+)?(?:bash|sh|python[23]?|perl|ruby)\b"),
        "pipe URL to shell",
    ),
    (
        re.compile(r"\bwget\b.*\|\s*(?:sudo\s+)?(?:bash|sh|python[23]?|perl|ruby)\b"),
        "pipe URL to shell",
    ),
    # Process substitution to shell
    (re.compile(r"(?:bash|sh)\s+<\("), "process substitution to shell"),
    # Privilege escalation
    (re.compile(r"\bsudo\b"), "sudo command"),
    (re.compile(r"\bsu\s+-c\b"), "su command execution"),
    (re.compile(r"\bdoas\b"), "doas command"),
    (re.compile(r"\bpkexec\b"), "pkexec command"),
]


def _check_dangerous(script: str) -> str | None:
    """Return a reason string if the script contains a dangerous pattern."""
    for pattern, reason in _DANGEROUS_PATTERNS:
        if pattern.search(script):
            return reason
    return None


@tool(
    name="Bash",
    desc=(
        "Run a shell command and return its stdout, stderr, and exit code. "
        "Only use for running builds, tests, git status, grep, or other "
        "read-only / non-destructive commands. "
        "Do NOT use Bash to edit files — use Edit or WriteFile instead."
    ),
)
def bash_tool(
    context: ToolContext,
    script: Annotated[
        str,
        "The bash command to run (e.g. 'python -m pytest tests/' or 'git status').",
    ],
    cwd: Annotated[
        str,
        "Working directory (relative to project root). Defaults to project root.",
    ] = "",
    timeout: Annotated[int, "Timeout in seconds."] = DEFAULT_TIMEOUT,
) -> Annotated[dict, "Command output with stdout, stderr, exit_code, and success."]:
    """Execute a bash command using /bin/bash -c."""
    # Guard against None leaking through arcade-tdk's parameter handling
    script = script or ""
    if not script.strip():
        raise CommandExecutionError("(empty)", "script cannot be empty")

    # Block dangerous commands
    danger = _check_dangerous(script)
    if danger:
        raise CommandExecutionError(
            script[:50],
            f"Blocked: {danger}. This command could cause irreversible damage.",
        )

    cwd = cwd or ""
    if cwd.strip():
        try:
            working_dir = resolve_path(cwd)
        except Exception as e:
            raise CommandExecutionError(
                script[:50],
                f"Failed to resolve working directory '{cwd}': {e}",
            )
        if not working_dir.is_dir():
            raise PathNotFoundError(cwd, f"Working directory not found: {cwd}")
    else:
        working_dir = get_project_root()

    full_env = os.environ.copy()

    try:
        result = subprocess.run(
            ["/bin/bash", "-c", script],
            cwd=str(working_dir),
            capture_output=True,
            text=True,
            timeout=timeout if timeout > 0 else None,
            env=full_env,
        )

        output: dict = {
            "stdout": result.stdout or "",
            "stderr": result.stderr or "",
            "exit_code": result.returncode,
            "success": result.returncode == 0,
        }

        # Include cwd in output for transparency on failure
        if result.returncode != 0 and result.stderr:
            output["cwd"] = str(working_dir)

        return output

    except subprocess.TimeoutExpired:
        raise CommandTimeoutError(script[:50], timeout)
    except FileNotFoundError:
        raise CommandExecutionError("bash", "Bash not found at /bin/bash")
    except TypeError as e:
        raise CommandExecutionError(
            script[:50],
            f"Invalid parameter type: {e} (cwd={working_dir!r})",
        )
    except Exception as e:
        log.error(f"Bash execution failed: {e}")
        raise CommandExecutionError(script[:50], str(e))
