"""Executor 接口:工具调用的总入口(DESIGN §7.5 落地清单 #5)。

`AgentLoop` 不直接调 `tool.execute`,而是 `executor.call_tool(name, args, ctx)`。
Backend 内部 dispatch:

- `HostExecutor`(本步引入):全部 tools in-process,沿用原 `Tool.execute` 行为
- `DockerExecutor`(Step 3 引入):`shell` / `run_python` 走 `docker exec`,
  其余按 §7.5 #6 信任域二分仍走 host —— 此时 DockerExecutor 内部组合 HostExecutor

接口形状刻意保持 backend 无关:`call_tool(name, args, ctx)` 不暴露 `docker exec` /
`docker cp` / `docker stats` 等 Docker 假设。未来切 gVisor / Firecracker / e2b 时
应用层零改动,只换 backend driver(§7.5 #5 / §7.9 升级触发表)。
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from uuid import UUID


@dataclass
class ExecCtx:
    """每次 tool 调用的执行上下文。

    带身份 / 范围 / 取消钩子;arg-irrelevant 信息从 args 剥离。
    host backend 当前只用 cancel_check;docker backend 会:
    - user_id → 找 / 起 per-user 容器
    - working_dir → 拼 `docker exec --workdir /workspace/<wd_name>`
    - task_id → 临时文件命名空间 `/tmp/zcbot/<task_id>/`
    - cancel_check → 轮询期间响应停止按钮(主动 `kill -- -PGID`)
    """
    user_id: UUID
    task_id: UUID
    working_dir: Path
    cancel_check: Optional[Callable[[], bool]] = None


@dataclass
class ToolResult:
    """工具调用统一返回。

    现状所有 `Tool.execute` 都返 str,docker backend 后续可能要带 stdout/stderr/
    exit_code 分离。这里先留单 content 字段(LLM 拿到的就是这串),exit_code 作
    backend 内部使用 hint(0=ok / 1=tool 抛异常 / 2=参数非法 / 124=timeout 等),
    不影响 LLM 接口。
    """
    content: str
    exit_code: int = 0


class Executor(ABC):
    """工具调度抽象 —— 见模块 docstring。"""

    @abstractmethod
    def call_tool(self, name: str, args: Dict[str, Any], ctx: ExecCtx) -> ToolResult:
        """执行单次 tool 调用。永远返 ToolResult,不抛异常(异常包成 exit_code=1)。"""

    @abstractmethod
    def schemas(self) -> List[Dict[str, Any]]:
        """暴露给 LLM 的 OpenAI tool schema 列表;`AgentLoop._stream_llm` 用。"""

    @abstractmethod
    def has_tool(self, name: str) -> bool:
        """schema 列表覆盖的 tool 名;主要给测试 / 诊断用。"""
