from __future__ import annotations

import asyncio
import os
import shlex
from dataclasses import dataclass
from pathlib import Path

from openclaw_lite.config import AppConfig
from openclaw_lite.schema import ToolContext, ToolResult, ToolSpec
from openclaw_lite.tools.base import fail, ok
from openclaw_lite.utils import shorten


@dataclass(slots=True)
class ShellExecution:
    command: str
    returncode: int
    stdout: str
    stderr: str
    mode: str
    cwd: str


def _is_safe_command(command: str, config: AppConfig) -> tuple[bool, str | None]:
    lowered = command.strip().lower()
    for forbidden in config.shell.safe_blocklist:
        if forbidden in lowered:
            return False, f"blocked by pattern: {forbidden}"
    try:
        argv = shlex.split(command)
    except ValueError as exc:
        return False, f"shlex parse error: {exc}"
    if not argv:
        return False, "empty command"
    executable = Path(argv[0]).name
    if executable not in config.shell.safe_allowlist:
        return False, f"command `{executable}` not in safe allowlist"
    return True, None


async def run_shell_command(
    config: AppConfig,
    command: str,
    mode: str,
    cwd: str | Path | None = None,
) -> ShellExecution:
    target_cwd = Path(cwd or config.paths.workspace).resolve()
    target_cwd.mkdir(parents=True, exist_ok=True)
    env = os.environ.copy()

    if mode == "safe":
        allowed, reason = _is_safe_command(command, config)
        if not allowed:
            return ShellExecution(command=command, returncode=126, stdout="", stderr=reason or "unsafe", mode=mode, cwd=str(target_cwd))
        argv = shlex.split(command)
        proc = await asyncio.create_subprocess_exec(
            *argv,
            cwd=str(target_cwd),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            env=env,
        )
    else:
        proc = await asyncio.create_subprocess_shell(
            command,
            cwd=str(target_cwd),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            env=env,
        )

    try:
        stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout=config.shell.timeout_sec)
    except asyncio.TimeoutError:
        proc.kill()
        await proc.wait()
        return ShellExecution(
            command=command,
            returncode=124,
            stdout="",
            stderr=f"timeout after {config.shell.timeout_sec}s",
            mode=mode,
            cwd=str(target_cwd),
        )

    stdout = shorten(stdout_b.decode("utf-8", errors="replace"), config.shell.max_output_chars)
    stderr = shorten(stderr_b.decode("utf-8", errors="replace"), config.shell.max_output_chars)
    return ShellExecution(
        command=command,
        returncode=proc.returncode,
        stdout=stdout,
        stderr=stderr,
        mode=mode,
        cwd=str(target_cwd),
    )


async def shell_tool(context: ToolContext, arguments: dict) -> ToolResult:
    command = str(arguments.get("command", "")).strip()
    cwd = arguments.get("cwd") or context.config.paths.workspace
    result = await run_shell_command(context.config, command=command, mode=context.session.mode, cwd=cwd)
    return ok(
        {
            "command": result.command,
            "returncode": result.returncode,
            "stdout": result.stdout,
            "stderr": result.stderr,
            "mode": result.mode,
            "cwd": result.cwd,
        }
    )


def build_tools() -> list[ToolSpec]:
    return [
        ToolSpec(
            name="shell_exec",
            description="Run a shell command in the current workspace. Mode depends on the active session security mode.",
            parameters={
                "type": "object",
                "properties": {
                    "command": {"type": "string", "description": "Shell command to execute."},
                    "cwd": {"type": "string", "description": "Optional working directory."},
                },
                "required": ["command"],
            },
            handler=shell_tool,
            dangerous=True,
        )
    ]
