"""Shell command execution tool."""

from __future__ import annotations

import asyncio
import logging
import os
import sys
from typing import TYPE_CHECKING, Any

from .security import (
    check_blocked_path,
    check_custom_patterns,
    check_network_command,
    check_package_install,
    sanitize_command,
)

if TYPE_CHECKING:
    from ..config import BashSandboxConfig

import re
import shutil
import tempfile

logger = logging.getLogger(__name__)
security_logger = logging.getLogger("anteroom.security")

_MAX_OUTPUT = 100_000
_DEFAULT_TIMEOUT = 120

# Regex to detect `python -c "..."` or `python3 -c '...'` with multiline content
_PYTHON_C_RE = re.compile(
    r"""^(python3?)\s+-c\s+(['"])(.*)\2\s*$""",
    re.DOTALL,
)

_working_dir: str = os.getcwd()

DEFINITION: dict[str, Any] = {
    "name": "bash",
    "description": (
        "Execute a shell command and return stdout, stderr, and exit code. "
        "Commands run in the working directory. Default timeout is 120 seconds."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "command": {"type": "string", "description": "The shell command to execute"},
            "timeout": {
                "type": "integer",
                "description": "Timeout in seconds (default 120, max 600)",
                "default": 120,
            },
        },
        "required": ["command"],
    },
}


def set_working_dir(d: str) -> None:
    global _working_dir
    _working_dir = d


def _check_sandbox(command: str, config: BashSandboxConfig) -> str | None:
    """Run all sandbox checks. Returns error message if blocked, None if allowed."""
    if not config.allow_network:
        desc = check_network_command(command)
        if desc:
            return f"Network commands are blocked ({desc})"

    if not config.allow_package_install:
        desc = check_package_install(command)
        if desc:
            return f"Package installation is blocked ({desc})"

    if config.blocked_paths:
        desc = check_blocked_path(command, config.blocked_paths)
        if desc:
            return f"Blocked: {desc}"

    if config.blocked_commands:
        desc = check_custom_patterns(command, config.blocked_commands)
        if desc:
            return f"Blocked: {desc}"

    return None


def _rewrite_python_c_for_windows(command: str) -> tuple[str, str | None]:
    """On Windows, rewrite multiline ``python -c`` to a temp .py file.

    Returns (possibly rewritten command, temp_file_path or None).
    The caller is responsible for cleaning up the temp file.
    """
    m = _PYTHON_C_RE.match(command.strip())
    if not m:
        return command, None

    python_bin = m.group(1)
    script_body = m.group(3)

    # Only rewrite if the script is multiline (contains actual newlines)
    if "\n" not in script_body:
        return command, None

    # Write to a temp file (suffix .py so Windows knows it's Python)
    fd, tmp_path = tempfile.mkstemp(suffix=".py", prefix="anteroom_")
    try:
        os.write(fd, script_body.encode("utf-8"))
    finally:
        os.close(fd)

    return f'{python_bin} "{tmp_path}"', tmp_path


def _resolve_python_binary(command: str) -> str:
    """On Windows, rewrite ``python3`` to ``python`` if python3 is not on PATH."""
    if sys.platform != "win32":
        return command
    if not command.lstrip().startswith("python3"):
        return command
    if shutil.which("python3") is not None:
        return command
    if shutil.which("python") is None:
        return command
    return re.sub(r"\bpython3\b", "python", command, count=1)


async def handle(
    command: str,
    timeout: int = _DEFAULT_TIMEOUT,
    _bypass_hard_block: bool = False,
    _sandbox_config: BashSandboxConfig | None = None,
    **_: Any,
) -> dict[str, Any]:
    # Null byte check runs unconditionally — never bypassable.
    if "\x00" in command:
        return {"error": "Command contains null bytes", "exit_code": -1}
    if not _bypass_hard_block:
        command, error = sanitize_command(command)
        if error:
            return {"error": error, "exit_code": -1}

    # Apply sandbox restrictions (runs even when hard-block is bypassed)
    if _sandbox_config is not None:
        sandbox_error = _check_sandbox(command, _sandbox_config)
        if sandbox_error:
            security_logger.warning("Sandbox blocked: %s — %s", sandbox_error, command[:100])
            return {"error": sandbox_error, "exit_code": -1}
        max_timeout = _sandbox_config.timeout
        max_output = _sandbox_config.max_output_chars
    else:
        max_timeout = 600
        max_output = _MAX_OUTPUT

    timeout = min(max(1, timeout), max_timeout)

    # Log command if configured
    if _sandbox_config is not None and _sandbox_config.log_all_commands:
        security_logger.info("bash command: %s", command[:500])

    # Windows: rewrite python3 → python if needed, and multiline python -c to temp file
    tmp_script: str | None = None
    if sys.platform == "win32":
        command = _resolve_python_binary(command)
        command, tmp_script = _rewrite_python_c_for_windows(command)

    # Set up OS-level sandbox (Win32 Job Object) if configured
    use_os_sandbox = sys.platform == "win32" and _sandbox_config is not None and _sandbox_config.sandbox.is_enabled

    try:
        proc = await asyncio.create_subprocess_shell(
            command,
            stdin=asyncio.subprocess.DEVNULL,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=_working_dir,
        )

        # Assign process to Job Object for kernel-level resource limits
        job_handle: int | None = None
        if use_os_sandbox and proc.pid is not None:
            from .sandbox_win32 import setup_job_for_process

            job_handle = setup_job_for_process(_sandbox_config.sandbox, proc.pid)  # type: ignore[union-attr]
            if job_handle is None:
                security_logger.warning("Job Object setup failed, running without OS sandbox")

        try:
            stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
        except asyncio.TimeoutError:
            if job_handle is not None:
                from .sandbox_win32 import terminate_job

                terminate_job(job_handle)
            proc.kill()
            await proc.wait()
            return {"error": f"Command timed out after {timeout}s", "exit_code": -1}
        finally:
            if job_handle is not None:
                from .sandbox_win32 import close_job

                close_job(job_handle)

        stdout_str = stdout.decode("utf-8", errors="replace")
        stderr_str = stderr.decode("utf-8", errors="replace")

        if len(stdout_str) > max_output:
            stdout_str = stdout_str[:max_output] + "\n... (truncated)"
        if len(stderr_str) > max_output:
            stderr_str = stderr_str[:max_output] + "\n... (truncated)"

        return {
            "stdout": stdout_str,
            "stderr": stderr_str,
            "exit_code": proc.returncode or 0,
        }
    except OSError as e:
        return {"error": str(e), "exit_code": -1}
    finally:
        if tmp_script is not None:
            try:
                os.unlink(tmp_script)
            except OSError:
                pass
