"""Beaver 工具系统的统一契约。

这一层的目标不是实现具体工具，而是把 runtime 真正依赖的最小接口定死。

我们需要统一回答 4 个问题：
1. 一个工具长什么样
2. tool schema 怎么导出给 provider
3. 工具执行结果长什么样
4. tool loop 执行时，可以把哪些运行时依赖传给工具

这层故意保持很薄：
- 不绑定 MCP
- 不绑定 memory/session
- 不绑定具体 provider

这样内建工具、MCP 工具、未来插件工具都可以收敛到同一套契约上。
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
import inspect
import json
from typing import Any


@dataclass(slots=True)
class ToolSpec:
    """单个工具对外暴露的描述信息。

    这份信息主要服务两个场景：
    1. 以 MCP-style descriptor 作为统一事实来源
    2. 导出给 provider 的 function schema
    3. 在 registry 中做列出、查找、调试与 embedding 召回
    """

    name: str
    description: str
    input_schema: dict[str, Any]
    toolset: str = "core"
    always_available: bool = False
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_mcp_descriptor(self) -> dict[str, Any]:
        """导出 MCP ListTools 风格的工具描述。

        MCP 的基础字段是 `name`、`description`、`inputSchema`。
        Beaver 内部额外的 toolset/always_available 不塞进这个对象，
        避免未来对接真实 MCP server 时出现格式偏差。
        """

        return {
            "name": self.name,
            "description": self.description,
            "inputSchema": self.input_schema,
        }

    def to_provider_schema(self) -> dict[str, Any]:
        """导出为 OpenAI-compatible function tool schema。"""

        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": self.input_schema,
            },
        }

    def to_embedding_candidate(self) -> dict[str, str]:
        """导出给语义召回使用的轻量文本候选。"""

        return {
            "name": self.name,
            "description": self.description,
            "input_schema": json.dumps(self.input_schema, ensure_ascii=False, sort_keys=True),
        }


@dataclass(slots=True)
class ToolContext:
    """一次工具执行时可用的运行时上下文。

    这不是“所有系统对象的大杂烩”，而是当前工具执行阶段最常用的公共入口。
    后面主链接进来时，可以把 session manager / memory store / workspace 等从这里传入。
    """

    workspace: str | None = None
    session_id: str | None = None
    user_id: str | None = None
    services: dict[str, Any] = field(default_factory=dict)
    metadata: dict[str, Any] = field(default_factory=dict)

    def get(self, key: str, default: Any = None) -> Any:
        """优先从 services 中取依赖，方便工具侧少写样板代码。"""

        return self.services.get(key, default)


@dataclass(slots=True)
class ToolResult:
    """标准化工具执行结果。

    统一返回结构的意义是：
    1. tool loop 更容易记录日志和失败信息
    2. provider 回灌时可以稳定地拿到字符串内容
    3. 后面要做工具审计时，数据结构已经固定
    """

    success: bool
    content: str
    tool_name: str
    error: str | None = None
    raw_output: Any | None = None


class BaseTool(ABC):
    """所有工具实现都应遵守的抽象基类。"""

    @property
    @abstractmethod
    def spec(self) -> ToolSpec:
        """返回工具元数据。"""

    @abstractmethod
    async def invoke(self, arguments: dict[str, Any], context: ToolContext) -> ToolResult:
        """执行工具调用。"""


class ObjectBackedTool(BaseTool):
    """把现有“轻量对象工具”适配到统一 BaseTool 契约。

    目前 `MemoryTool` / `SessionSearchTool` 已经存在，但它们还不是统一的 BaseTool。
    这个适配器的作用就是避免重写业务逻辑，只做接口收口。
    """

    def __init__(self, backend: Any) -> None:
        self.backend = backend
        self._spec = ToolSpec(
            name=str(getattr(backend, "name")),
            description=str(getattr(backend, "description", "")),
            input_schema=dict(getattr(backend, "parameters", {"type": "object", "properties": {}})),
            toolset=str(getattr(backend, "toolset", "core")),
            always_available=bool(getattr(backend, "always_available", False)),
        )

    @property
    def spec(self) -> ToolSpec:
        return self._spec

    async def invoke(self, arguments: dict[str, Any], context: ToolContext) -> ToolResult:
        try:
            call_arguments = dict(arguments)
            self._inject_runtime_context(call_arguments, context)
            content = await self.backend.execute(**call_arguments)
            result = self._normalize_output(content)
            return ToolResult(
                success=result["success"],
                content=result["content"],
                tool_name=self.spec.name,
                error=result.get("error"),
                raw_output=content,
            )
        except Exception as exc:
            return ToolResult(
                success=False,
                content=f"Tool {self.spec.name} failed: {exc}",
                tool_name=self.spec.name,
                error=str(exc),
            )

    def _inject_runtime_context(self, arguments: dict[str, Any], context: ToolContext) -> None:
        """把少量 runtime 上下文注入到后端工具参数中。

        当前只做最小注入：
        - 只有当 backend 明确暴露对应字段时才注入
        - 避免把 ToolContext 整个对象直接塞给现有 builtin 工具
        """

        if "current_session_id" not in arguments and hasattr(self.backend, "current_session_id"):
            arguments["current_session_id"] = context.session_id
        if "workspace" not in arguments and (hasattr(self.backend, "workspace") or self._backend_accepts_argument("workspace")):
            arguments["workspace"] = context.workspace
        if "services" not in arguments and self._backend_accepts_argument("services"):
            arguments["services"] = context.services
        if "metadata" not in arguments and self._backend_accepts_argument("metadata"):
            arguments["metadata"] = context.metadata

    def _backend_accepts_argument(self, name: str) -> bool:
        try:
            signature = inspect.signature(self.backend.execute)
        except (TypeError, ValueError):
            return False
        for parameter in signature.parameters.values():
            if parameter.kind == inspect.Parameter.VAR_KEYWORD:
                return True
            if parameter.name == name:
                return True
        return False

    @staticmethod
    def _normalize_output(content: Any) -> dict[str, Any]:
        """把后端工具返回值转成统一 success/content/error 语义。

        对现有 builtin 工具最关键的是：
        - 若返回的是 JSON 字符串，且包含 `success` 字段，就尊重它
        - 否则默认视为普通成功文本
        """

        if isinstance(content, str):
            try:
                parsed = json.loads(content)
            except json.JSONDecodeError:
                return {"success": True, "content": content}
            if isinstance(parsed, dict) and "success" in parsed:
                return {
                    "success": bool(parsed.get("success")),
                    "content": content,
                    "error": parsed.get("error"),
                }
            return {"success": True, "content": content}
        return {"success": True, "content": str(content)}
