"""Tool 注册、上下文注入与执行审计。

模型看到的是 LangChain Tool，但真实业务函数还需要登录身份、权限和执行记录。
这个模块负责把普通 Python 函数包装成模型可调用、系统可审计的受控工具。
"""

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any

from langchain_core.tools import BaseTool, StructuredTool
from pydantic import BaseModel

from zbt.core.errors import AppError
from zbt.domains.identity.models import AdminUser, H5User
from zbt.harness.policy import HarnessPolicyEngine, ToolPolicy
from zbt.harness.schemas import HarnessToolResult

ToolHandler = Callable[["ToolExecutionContext", BaseModel], HarnessToolResult]


@dataclass(frozen=True)
class ToolDefinition:
    """一个工具的静态说明：名称、参数、权限策略和真正的处理函数。"""

    name: str
    description: str
    arguments: type[BaseModel]
    policy: ToolPolicy
    handler: ToolHandler


@dataclass(frozen=True)
class ToolExecution:
    """一次已经完成的工具调用，用于生成卡片、Action 和运行记录。"""

    name: str
    result: HarnessToolResult


@dataclass
class ToolExecutionContext:
    """一次 Agent 运行期间共享的工具上下文。

    模型无法修改这里的登录用户；它只能生成工具参数。
    """

    persona: str
    h5_user: H5User | None = None
    admin_user: AdminUser | None = None
    executions: list[ToolExecution] = field(default_factory=list)

    @property
    def principal_id(self) -> str:
        """返回当前调用者 ID；没有登录身份时拒绝继续执行。"""

        principal = self.h5_user or self.admin_user
        if principal is None:
            raise AppError("AGENT_PRINCIPAL_REQUIRED", "Agent缺少登录身份", 401)
        return principal.id

    @property
    def permissions(self) -> tuple[str, ...]:
        return self.admin_user.permissions if self.admin_user else ()


class ToolRegistry:
    """保存所有工具定义，并按人格白名单构建本次可见的工具列表。"""

    def __init__(self) -> None:
        self._items: dict[str, ToolDefinition] = {}

    def register(self, definition: ToolDefinition) -> None:
        """注册一个工具；名称重复通常意味着装配错误，因此直接报错。"""

        if definition.name in self._items:
            raise AppError(
                "HARNESS_TOOL_DUPLICATED",
                f"Tool标识重复：{definition.name}",
                500,
            )
        self._items[definition.name] = definition

    def names(self) -> tuple[str, ...]:
        return tuple(sorted(self._items))

    def build(
        self,
        *,
        names: tuple[str, ...],
        context: ToolExecutionContext,
        policy_engine: HarnessPolicyEngine,
    ) -> list[BaseTool]:
        """把本次人格有权使用的工具转换为 LangChain Tool。"""

        tools: list[BaseTool] = []
        for name in names:
            definition = self._items.get(name)
            if definition is None:
                raise AppError(
                    "HARNESS_TOOL_NOT_FOUND",
                    f"未找到Tool：{name}",
                    500,
                )
            # 构建阶段先过滤一次，模型根本看不到无权使用的工具。
            if not policy_engine.can_use(
                persona=context.persona,
                allowed_by_persona=names,
                tool_name=name,
                policy=definition.policy,
                permissions=context.permissions,
            ):
                continue
            tools.append(
                self._to_langchain_tool(
                    definition,
                    names,
                    context,
                    policy_engine,
                )
            )
        return tools

    @staticmethod
    def _to_langchain_tool(
        definition: ToolDefinition,
        allowed_names: tuple[str, ...],
        context: ToolExecutionContext,
        policy_engine: HarnessPolicyEngine,
    ) -> BaseTool:
        """创建真正交给模型的工具包装器。"""

        def invoke(**kwargs: Any) -> str:
            # 执行前再次鉴权，防止绕过“工具可见性”直接调用处理函数。
            policy_engine.authorize(
                persona=context.persona,
                allowed_by_persona=allowed_names,
                tool_name=definition.name,
                policy=definition.policy,
                permissions=context.permissions,
            )
            # 模型生成的字典是不可信输入，必须先按 Pydantic Schema 校验。
            arguments = definition.arguments.model_validate(kwargs)
            result = definition.handler(context, arguments)

            # 保存执行结果，Kernel 稍后会从中收集卡片、按钮和工具名称。
            context.executions.append(ToolExecution(name=definition.name, result=result))
            return result.model_dump_json(exclude_none=True)

        return StructuredTool.from_function(
            func=invoke,
            name=definition.name,
            description=definition.description,
            args_schema=definition.arguments,
        )
