"""智保通 Agent 可以调用的受控业务工具。

第二阶段在第一阶段业务工具之上新增四类能力：

- Agentic RAG：允许模型拆分、改写问题并进行最多三轮知识检索；
- 长期记忆：按当前 H5 用户保存、召回和删除保障偏好；
- 运营分析：把模型选择的指标转换成后端白名单查询；
- 证据约束：回答只能引用工具实际返回的知识片段。

大模型负责决定“何时调用哪个工具”，真实查询、权限和数据范围仍由后端执行。
"""

from typing import Any, Literal, Protocol

from pydantic import BaseModel, ConfigDict, Field

from zbt.core.errors import AppError
from zbt.domains.catalog.service import ProductCatalogService
from zbt.domains.enrollment.service import EnrollmentService
from zbt.harness.policy import ToolPolicy
from zbt.harness.schemas import (
    AgentAction,
    BusinessListBlock,
    ChartBlock,
    HarnessToolResult,
    KnowledgeSourcesBlock,
    MetricItem,
    MetricsBlock,
    QuoteBlock,
    QuoteView,
)
from zbt.harness.tooling import ToolDefinition, ToolExecutionContext, ToolRegistry


class AttributionMetricsProvider(Protocol):
    def performance(self, admin_user_id: str | None = None) -> dict[str, Any]: ...

    def order_ids(self, admin_user_id: str | None = None) -> set[str]: ...


class KnowledgeSearchProvider(Protocol):
    """面向 Agent 的知识检索接口，实际实现是 ``KnowledgeService``。"""

    def search(
        self,
        query: str,
        *,
        limit: int = 5,
        product_code: str | None = None,
        document_types: tuple[str, ...] = (),
    ) -> dict[str, Any]: ...


class OperationalAnalyticsProvider(Protocol):
    """受控运营指标查询接口，不接受模型生成的原始 SQL。"""

    def query(
        self,
        *,
        metric: str,
        group_by: str,
        days: int,
        admin_user_id: str | None = None,
    ) -> dict[str, Any]: ...


class CustomerMemoryProvider(Protocol):
    """长期记忆接口；所有操作都必须显式携带当前用户编号。"""

    def save(
        self,
        *,
        owner_id: str,
        category: str,
        content: str,
    ) -> dict[str, object]: ...

    def search(
        self,
        *,
        owner_id: str,
        query: str,
        limit: int = 3,
    ) -> dict[str, object]: ...

    def delete(self, *, owner_id: str, memory_id: str) -> dict[str, object]: ...


class EmptyAttributionMetrics:
    def performance(self, admin_user_id: str | None = None) -> dict[str, Any]:
        del admin_user_id
        return {
            "salesperson_count": 0,
            "visit_count": 0,
            "lead_count": 0,
            "order_count": 0,
            "premium_cents": 0,
            "items": [],
        }

    def order_ids(self, admin_user_id: str | None = None) -> set[str]:
        del admin_user_id
        return set()


class ToolArguments(BaseModel):
    model_config = ConfigDict(extra="forbid")


class ListProductsArgs(ToolArguments):
    category: Literal["MEDICAL", "ACCIDENT"] | None = None


class CalculateQuoteArgs(ToolArguments):
    product_code: str = Field(min_length=2, max_length=32)
    plan_code: str = Field(min_length=2, max_length=32)
    age: int = Field(ge=0, le=120)
    region_code: str = Field(default="510100", pattern=r"^\d{6}$")
    occupation_code: str = Field(default="GENERAL", min_length=2, max_length=32)
    relationship: Literal["SELF", "PARENT", "SPOUSE", "CHILD"]


class PrepareEnrollmentArgs(ToolArguments):
    product_code: str = Field(min_length=2, max_length=32)
    plan_code: str = Field(min_length=2, max_length=32)
    age: int = Field(ge=0, le=120)
    relationship: Literal["SELF", "PARENT", "SPOUSE", "CHILD"]


class EmptyArgs(ToolArguments):
    pass


class RecentItemsArgs(ToolArguments):
    limit: int = Field(default=5, ge=1, le=20)


class SearchKnowledgeArgs(ToolArguments):
    """Agentic RAG 的单轮检索计划。

    模型可给出原始问题和最多两个改写问题，后端会分别检索后再合并结果。
    """

    query: str = Field(min_length=2, max_length=500)
    alternate_queries: list[str] = Field(default_factory=list, max_length=2)
    product_code: str | None = Field(default=None, min_length=2, max_length=64)
    document_types: list[
        Literal["INSURANCE_TERMS", "PRODUCT_GUIDE", "SERVICE_RULES", "FAQ"]
    ] = Field(default_factory=list, max_length=4)
    limit: int = Field(default=5, ge=1, le=8)


class OperationAnalyticsArgs(ToolArguments):
    metric: Literal[
        "ORDER_COUNT",
        "ORDER_PREMIUM_SUM",
        "ACTIVE_POLICY_COUNT",
        "ACTIVE_POLICY_PREMIUM_SUM",
    ]
    group_by: Literal["DAY", "PRODUCT"] = "DAY"
    days: Literal[7, 30, 90] = 30


class RememberCustomerArgs(ToolArguments):
    category: Literal["PREFERENCE", "FAMILY_CONTEXT", "CONSTRAINT"]
    content: str = Field(min_length=2, max_length=300)


class RecallCustomerMemoryArgs(ToolArguments):
    query: str = Field(min_length=2, max_length=300)
    limit: int = Field(default=3, ge=1, le=5)


class ForgetCustomerMemoryArgs(ToolArguments):
    memory_id: str = Field(min_length=16, max_length=64)


def build_agent_tool_registry(
    catalog: ProductCatalogService,
    enrollment: EnrollmentService,
    attribution: AttributionMetricsProvider | None = None,
    knowledge: KnowledgeSearchProvider | None = None,
    analytics: OperationalAnalyticsProvider | None = None,
    memory: CustomerMemoryProvider | None = None,
) -> ToolRegistry:
    """组装当前阶段全部工具，并把业务服务封装成模型可调用函数。"""

    registry = ToolRegistry()
    metrics = attribution or EmptyAttributionMetrics()

    def list_products(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        del context
        args = _arguments(arguments, ListProductsArgs)
        products = catalog.list_available(category=args.category)
        return HarnessToolResult(
            summary=f"当前共有{len(products)}款符合条件的在售产品",
            data={"items": products, "total": len(products)},
        )

    def calculate_quote(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        args = _arguments(arguments, CalculateQuoteArgs)
        user = context.h5_user
        if user is None:
            raise AppError("AGENT_CUSTOMER_REQUIRED", "保费测算需要H5用户身份", 401)
        product, plan = _resolve_product_plan(
            catalog,
            args.product_code,
            args.plan_code,
        )
        try:
            quote = enrollment.create_quote(
                user,
                product_id=str(product["product_id"]),
                plan_id=str(plan["id"]),
                age=args.age,
                region_code=args.region_code,
                occupation_code=args.occupation_code,
                relationship=args.relationship,
            )
        except AppError as error:
            if error.code != "ELIGIBILITY_REJECTED":
                raise
            block = QuoteBlock(
                quote=QuoteView(
                    eligible=False,
                    product_name=str(product["name"]),
                    plan_name=str(plan["name"]),
                    reason=error.message,
                )
            )
            return HarnessToolResult(
                summary=f"资格校验未通过：{error.message}",
                data={
                    "eligible": False,
                    "reason_code": (error.details or {}).get("reason_code"),
                },
                blocks=[block],
            )
        block = QuoteBlock(
            quote=QuoteView(
                eligible=True,
                quote_id=str(quote["quote_id"]),
                product_id=str(quote["product_id"]),
                product_name=str(product["name"]),
                plan_id=str(quote["plan_id"]),
                plan_name=str(plan["name"]),
                premium_cents=int(quote["premium_cents"]),
                currency=str(quote["currency"]),
                expires_at=str(quote["expires_at"]),
            )
        )
        return HarnessToolResult(
            summary=(
                f"{product['name']}{plan['name']}测算成功，"
                f"年保费{int(quote['premium_cents']) / 100:.2f}元"
            ),
            data=quote,
            blocks=[block],
            actions=[
                AgentAction(
                    type="open_enrollment",
                    label="填写投保信息",
                    payload={
                        "product_code": args.product_code,
                        "plan_code": args.plan_code,
                        "quote_id": quote["quote_id"],
                        "age": args.age,
                        "relationship": args.relationship,
                    },
                )
            ],
        )

    def prepare_enrollment(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        if context.h5_user is None:
            raise AppError("AGENT_CUSTOMER_REQUIRED", "投保引导需要H5用户身份", 401)
        args = _arguments(arguments, PrepareEnrollmentArgs)
        product, plan = _resolve_product_plan(
            catalog,
            args.product_code,
            args.plan_code,
        )
        action = AgentAction(
            type="open_enrollment",
            label="立即投保",
            payload={
                "product_id": product["product_id"],
                "product_code": args.product_code,
                "plan_id": plan["id"],
                "plan_code": args.plan_code,
                "age": args.age,
                "relationship": args.relationship,
            },
        )
        return HarnessToolResult(
            summary="已准备安全投保表单入口",
            data={"product": product, "plan": plan},
            actions=[action],
        )

    def list_my_orders(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        del arguments
        user = context.h5_user
        if user is None:
            raise AppError("AGENT_CUSTOMER_REQUIRED", "订单查询需要H5用户身份", 401)
        result = enrollment.list_orders(user.id)
        items = [_safe_order_item(item) for item in result["items"]]
        return HarnessToolResult(
            summary=f"查到{len(items)}笔本人投保订单",
            data={"items": items, "total": len(items)},
            blocks=[
                BusinessListBlock(
                    title="我的投保订单",
                    entity="order",
                    items=items[:5],
                    total=len(items),
                )
            ],
            actions=[AgentAction(type="open_orders", label="查看我的订单")],
        )

    def list_my_policies(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        del arguments
        user = context.h5_user
        if user is None:
            raise AppError("AGENT_CUSTOMER_REQUIRED", "保单查询需要H5用户身份", 401)
        result = enrollment.list_policies(user)
        items = [_safe_policy_item(item) for item in result["items"]]
        return HarnessToolResult(
            summary=f"查到{len(items)}张本人保单",
            data={"items": items, "total": len(items)},
            blocks=[
                BusinessListBlock(
                    title="我的保障",
                    entity="policy",
                    items=items[:5],
                    total=len(items),
                )
            ],
            actions=[AgentAction(type="open_policies", label="查看我的保障")],
        )

    def overview(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        del arguments
        orders = enrollment.list_orders()
        policies = enrollment.list_all_policies()
        if context.admin_user and context.admin_user.data_scope == "SELF":
            allowed = metrics.order_ids(context.admin_user.id)
            orders = _filter_business_result(orders, allowed, "order_id")
            policies = _filter_business_result(policies, allowed, "order_id")
        active = [item for item in policies["items"] if item["status"] == "ACTIVE"]
        premium_cents = sum(int(item["premium_cents"]) for item in active)
        items = [
            MetricItem(label="订单总量", value=int(orders["total"]), unit="笔"),
            MetricItem(label="有效保单", value=len(active), unit="张"),
            MetricItem(label="累计保费", value=premium_cents / 100, unit="元"),
        ]
        return HarnessToolResult(
            summary=(
                f"当前订单{orders['total']}笔、有效保单{len(active)}张、"
                f"累计保费{premium_cents / 100:.2f}元"
            ),
            data={
                "order_count": orders["total"],
                "active_policy_count": len(active),
                "premium_cents": premium_cents,
            },
            blocks=[MetricsBlock(title="经营总览", items=items)],
        )

    def recent_orders(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        args = _arguments(arguments, RecentItemsArgs)
        result = enrollment.list_orders()
        if context.admin_user and context.admin_user.data_scope == "SELF":
            result = _filter_business_result(
                result,
                metrics.order_ids(context.admin_user.id),
                "order_id",
            )
        items = [_safe_order_item(item) for item in result["items"][: args.limit]]
        return HarnessToolResult(
            summary=f"返回最近{len(items)}笔订单",
            data={"items": items, "total": result["total"]},
            blocks=[
                BusinessListBlock(
                    title="近期订单",
                    entity="order",
                    items=items,
                    total=int(result["total"]),
                )
            ],
            actions=[AgentAction(type="open_admin_orders", label="进入订单中心")],
        )

    def recent_policies(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        args = _arguments(arguments, RecentItemsArgs)
        result = enrollment.list_all_policies()
        if context.admin_user and context.admin_user.data_scope == "SELF":
            result = _filter_business_result(
                result,
                metrics.order_ids(context.admin_user.id),
                "order_id",
            )
        items = [_safe_policy_item(item) for item in result["items"][: args.limit]]
        return HarnessToolResult(
            summary=f"返回最近{len(items)}张保单",
            data={"items": items, "total": result["total"]},
            blocks=[
                BusinessListBlock(
                    title="近期保单",
                    entity="policy",
                    items=items,
                    total=int(result["total"]),
                )
            ],
            actions=[AgentAction(type="open_admin_policies", label="进入保单中心")],
        )

    def attribution_performance(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        del arguments
        admin = context.admin_user
        if admin is None:
            raise AppError("AGENT_ADMIN_REQUIRED", "推广分析需要后台身份", 401)
        owner_id = admin.id if admin.data_scope == "SELF" else None
        result = metrics.performance(owner_id)
        block = MetricsBlock(
            title="推广业绩",
            items=[
                MetricItem(
                    label="推广员",
                    value=int(result["salesperson_count"]),
                    unit="人",
                ),
                MetricItem(label="访问", value=int(result["visit_count"]), unit="次"),
                MetricItem(label="线索", value=int(result["lead_count"]), unit="人"),
                MetricItem(label="归因订单", value=int(result["order_count"]), unit="笔"),
                MetricItem(
                    label="归因保费",
                    value=int(result["premium_cents"]) / 100,
                    unit="元",
                ),
            ],
        )
        return HarnessToolResult(
            summary=(
                f"推广访问{result['visit_count']}次、线索{result['lead_count']}人、"
                f"归因订单{result['order_count']}笔"
            ),
            data=result,
            blocks=[block],
            actions=[AgentAction(type="open_attribution", label="查看推广明细")],
        )

    def search_insurance_knowledge(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        """执行一轮 Agentic RAG 检索并返回可追溯证据。

        同一次 Agent 运行最多调用三轮。模型可以根据上一轮证据不足的部分继续
        缩小问题，但到达上限后必须使用已有依据回答或明确拒绝推测。
        """

        args = _arguments(arguments, SearchKnowledgeArgs)
        queries = _normalize_knowledge_queries(args.query, args.alternate_queries)
        attempt = (
            sum(
                execution.name == "search_insurance_knowledge"
                for execution in context.executions
            )
            + 1
        )
        if attempt > 3:
            raise AppError(
                "AGENTIC_RETRIEVAL_LIMIT_REACHED",
                "知识检索最多执行3轮，请基于现有证据回答或明确说明无法确定",
                409,
            )
        retrieval_plan = {
            "original_query": args.query.strip(),
            "executed_queries": queries,
            "filters": {
                "product_code": args.product_code,
                "document_types": list(args.document_types),
            },
            "attempt": attempt,
        }
        if knowledge is None:
            return HarnessToolResult(
                summary="没有检索到已发布的可靠知识依据",
                data={
                    "items": [],
                    "total": 0,
                    "agentic_retrieval": {
                        **retrieval_plan,
                        "next_step": "refine_query",
                    },
                },
            )
        # 每个改写问题独立检索，再按片段编号融合，减少同义问题漏召回。
        rankings = [
            knowledge.search(
                query,
                limit=args.limit,
                product_code=args.product_code,
                document_types=tuple(args.document_types),
            )["items"]
            for query in queries
        ]
        items = _fuse_knowledge_rankings(rankings, limit=args.limit)
        result = {"items": items, "total": len(items)}
        if not items:
            return HarnessToolResult(
                summary="没有检索到已发布的可靠知识依据",
                data={
                    **result,
                    "agentic_retrieval": {
                        **retrieval_plan,
                        "next_step": "refine_query",
                    },
                },
            )
        best = items[0]
        excerpt = str(best["content"])[:180]
        # 这些约束会随工具结果返回给模型，明确禁止把相似内容扩展成确定结论。
        evidence_data = {
            **result,
            "agentic_retrieval": {
                **retrieval_plan,
                "next_step": "evaluate_evidence",
            },
            "answer_constraints": {
                "direct_evidence_only": True,
                "no_scenario_expansion": True,
                "no_numeric_inference": True,
                "unsupported_answer": "当前知识库未明确，无法确定",
            },
        }
        return HarnessToolResult(
            summary=(
                f"第{attempt}轮共执行{len(queries)}个检索问题，"
                f"合并得到{result['total']}条已发布依据。最相关内容：{excerpt}"
                f"（来源：{best['source_name']}，版本v{best['document_version']}）。"
                "请先判断证据是否覆盖问题各子项；不足时缩小范围再次检索。"
                "回答只能使用返回摘录中的直接事实；不得扩展具体场景或推断未出现的数字。"
            ),
            data=evidence_data,
            blocks=[
                KnowledgeSourcesBlock(
                    items=[
                        {
                            "document_id": str(item["document_id"]),
                            "document_version": int(item["document_version"]),
                            "title": str(item["title"]),
                            "source_name": str(item["source_name"]),
                            "excerpt": str(item["content"])[:180],
                            "score": float(item["score"]),
                        }
                        for item in items[:5]
                    ]
                )
            ],
        )

    def query_operation_analytics(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        """查询运营趋势；模型只能选择已登记的指标、维度和时间窗口。"""

        args = _arguments(arguments, OperationAnalyticsArgs)
        admin = context.admin_user
        if admin is None:
            raise AppError("AGENT_ADMIN_REQUIRED", "运营分析需要后台身份", 401)
        if analytics is None:
            raise AppError(
                "ANALYTICS_UNAVAILABLE",
                "运营分析服务尚未就绪",
                503,
                retryable=True,
            )
        result = analytics.query(
            metric=args.metric,
            group_by=args.group_by,
            days=args.days,
            admin_user_id=admin.id if admin.data_scope == "SELF" else None,
        )
        rows = list(result["rows"])
        return HarnessToolResult(
            summary=(
                f"已按{args.days}天、{result['group_by']}维度查询"
                f"{result['metric_name']}，合计{result['total']}{result['unit']}。"
                "查询由后端白名单语义层编译，只读取分析事实表。"
            ),
            data=result,
            blocks=[
                ChartBlock(
                    title=f"{args.days}天{result['metric_name']}趋势",
                    chart_type="line" if args.group_by == "DAY" else "bar",
                    unit=str(result["unit"]),
                    points=[
                        {
                            "label": str(item["label"]),
                            "value": float(item["value"]),
                        }
                        for item in rows
                    ],
                )
            ],
        )

    def remember_customer_preference(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        """仅在用户明确要求记住时，由 Agent 调用该工具保存长期偏好。"""

        args = _arguments(arguments, RememberCustomerArgs)
        user = context.h5_user
        if user is None:
            raise AppError("AGENT_CUSTOMER_REQUIRED", "长期记忆需要H5用户身份", 401)
        if memory is None:
            raise AppError("CUSTOMER_MEMORY_UNAVAILABLE", "长期记忆服务尚未就绪", 503)
        result = memory.save(
            owner_id=user.id,
            category=args.category,
            content=args.content,
        )
        return HarnessToolResult(
            summary=(
                f"已按用户明确要求保存长期记忆：{result['content']}。"
                f"记忆编号：{result['memory_id']}"
            ),
            data=dict(result),
        )

    def recall_customer_memories(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        """召回当前用户相关记忆，不能跨用户读取，也不能替代保险条款。"""

        args = _arguments(arguments, RecallCustomerMemoryArgs)
        user = context.h5_user
        if user is None:
            raise AppError("AGENT_CUSTOMER_REQUIRED", "长期记忆需要H5用户身份", 401)
        if memory is None:
            raise AppError("CUSTOMER_MEMORY_UNAVAILABLE", "长期记忆服务尚未就绪", 503)
        result = memory.search(
            owner_id=user.id,
            query=args.query,
            limit=args.limit,
        )
        return HarnessToolResult(
            summary=(
                f"检索到{result['total']}条当前用户的长期记忆。"
                "记忆只用于个性化沟通，不能替代保险条款和业务数据。"
            ),
            data=dict(result),
        )

    def forget_customer_memory(
        context: ToolExecutionContext,
        arguments: BaseModel,
    ) -> HarnessToolResult:
        """响应用户删除记忆的要求，并让该内容立即停止参与后续对话。"""

        args = _arguments(arguments, ForgetCustomerMemoryArgs)
        user = context.h5_user
        if user is None:
            raise AppError("AGENT_CUSTOMER_REQUIRED", "长期记忆需要H5用户身份", 401)
        if memory is None:
            raise AppError("CUSTOMER_MEMORY_UNAVAILABLE", "长期记忆服务尚未就绪", 503)
        result = memory.delete(owner_id=user.id, memory_id=args.memory_id)
        return HarnessToolResult(
            summary="已删除指定长期记忆，后续对话不再使用该内容。",
            data=dict(result),
        )

    definitions = (
        ToolDefinition(
            name="list_available_products",
            description="查询当前真实在售保险产品和计划；推荐产品前必须调用。",
            arguments=ListProductsArgs,
            policy=ToolPolicy(personas=("customer", "operation"), effect="read"),
            handler=list_products,
        ),
        ToolDefinition(
            name="calculate_insurance_quote",
            description="按真实产品、年龄、地区、职业和关系执行确定性资格校验与保费测算。",
            arguments=CalculateQuoteArgs,
            policy=ToolPolicy(personas=("customer",), effect="draft"),
            handler=calculate_quote,
        ),
        ToolDefinition(
            name="prepare_enrollment",
            description="为已明确的产品和计划生成安全投保表单入口，不采集身份证号。",
            arguments=PrepareEnrollmentArgs,
            policy=ToolPolicy(personas=("customer",), effect="draft"),
            handler=prepare_enrollment,
        ),
        ToolDefinition(
            name="list_my_orders",
            description="查询当前登录客户自己的投保订单。",
            arguments=EmptyArgs,
            policy=ToolPolicy(personas=("customer",), effect="read"),
            handler=list_my_orders,
        ),
        ToolDefinition(
            name="list_my_policies",
            description="查询当前登录客户自己的电子保单。",
            arguments=EmptyArgs,
            policy=ToolPolicy(personas=("customer",), effect="read"),
            handler=list_my_policies,
        ),
        ToolDefinition(
            name="get_operation_overview",
            description="查询订单、有效保单和累计保费经营指标。",
            arguments=EmptyArgs,
            policy=ToolPolicy(
                personas=("operation",),
                effect="read",
                required_permissions=("dashboard:read",),
            ),
            handler=overview,
        ),
        ToolDefinition(
            name="list_recent_orders",
            description="按当前管理员数据范围查询近期订单，只返回脱敏字段。",
            arguments=RecentItemsArgs,
            policy=ToolPolicy(
                personas=("operation",),
                effect="read",
                required_permissions=("order:read",),
            ),
            handler=recent_orders,
        ),
        ToolDefinition(
            name="list_recent_policies",
            description="按当前管理员数据范围查询近期保单，只返回脱敏字段。",
            arguments=RecentItemsArgs,
            policy=ToolPolicy(
                personas=("operation",),
                effect="read",
                required_permissions=("policy:read",),
            ),
            handler=recent_policies,
        ),
        ToolDefinition(
            name="get_attribution_performance",
            description="查询当前管理员数据范围内的推广访问、线索、订单和保费。",
            arguments=EmptyArgs,
            policy=ToolPolicy(
                personas=("operation",),
                effect="read",
                required_permissions=("attribution:read",),
            ),
            handler=attribution_performance,
        ),
        ToolDefinition(
            name="search_insurance_knowledge",
            description=(
                "执行受控Agentic RAG：可提交原问题、最多2个改写问题及产品、文档类型过滤，"
                "对已发布的保险条款、保障责任、免责范围和服务规则进行多路检索并合并证据。"
                "调用后需判断证据充分性；不足时可缩小问题再次调用，"
                "最终只引用直接支持结论的来源和版本。"
            ),
            arguments=SearchKnowledgeArgs,
            policy=ToolPolicy(personas=("customer", "operation"), effect="read"),
            handler=search_insurance_knowledge,
        ),
        ToolDefinition(
            name="query_operation_analytics",
            description=(
                "按白名单指标、时间窗口和维度查询分析库，返回确定性聚合结果和图表。"
                "需要趋势、分产品对比或一段时间内的经营分析时调用；"
                "不得生成或提交原始SQL。"
            ),
            arguments=OperationAnalyticsArgs,
            policy=ToolPolicy(
                personas=("operation",),
                effect="read",
                required_permissions=("orders:read", "policies:read"),
            ),
            handler=query_operation_analytics,
        ),
        ToolDefinition(
            name="remember_customer_preference",
            description=(
                "仅当客户明确说“记住、保存、以后按此偏好”时，"
                "保存不含手机号、身份证号和银行卡号的偏好或家庭保障上下文。"
            ),
            arguments=RememberCustomerArgs,
            policy=ToolPolicy(personas=("customer",), effect="draft"),
            handler=remember_customer_preference,
        ),
        ToolDefinition(
            name="recall_customer_memories",
            description=(
                "在需要跨会话个性化推荐时，语义检索当前登录客户自己的长期记忆。"
                "不得把记忆当作条款或业务事实。"
            ),
            arguments=RecallCustomerMemoryArgs,
            policy=ToolPolicy(personas=("customer",), effect="read"),
            handler=recall_customer_memories,
        ),
        ToolDefinition(
            name="forget_customer_memory",
            description="按客户明确要求删除当前账号自己的指定长期记忆。",
            arguments=ForgetCustomerMemoryArgs,
            policy=ToolPolicy(personas=("customer",), effect="draft"),
            handler=forget_customer_memory,
        ),
    )
    for definition in definitions:
        registry.register(definition)
    return registry


def _arguments(value: BaseModel, expected: type[ToolArguments]) -> Any:
    if not isinstance(value, expected):
        raise AppError("AGENT_TOOL_ARGUMENTS_INVALID", "工具参数类型错误", 500)
    return value


def _normalize_knowledge_queries(
    query: str,
    alternate_queries: list[str],
) -> list[str]:
    queries: list[str] = []
    for candidate in [query, *alternate_queries]:
        normalized = candidate.strip()
        if len(normalized) >= 2 and normalized not in queries:
            queries.append(normalized)
    return queries[:3]


def _fuse_knowledge_rankings(
    rankings: list[list[dict[str, Any]]],
    *,
    limit: int,
) -> list[dict[str, Any]]:
    fused_scores: dict[str, float] = {}
    items_by_chunk: dict[str, dict[str, Any]] = {}
    for ranking in rankings:
        for rank, item in enumerate(ranking, start=1):
            chunk_id = str(item["chunk_id"])
            items_by_chunk.setdefault(chunk_id, dict(item))
            fused_scores[chunk_id] = fused_scores.get(chunk_id, 0.0) + (
                1.0 / (60 + rank)
            )
    ordered_chunk_ids = sorted(
        fused_scores,
        key=lambda chunk_id: (-fused_scores[chunk_id], chunk_id),
    )
    results: list[dict[str, Any]] = []
    for chunk_id in ordered_chunk_ids[:limit]:
        item = dict(items_by_chunk[chunk_id])
        item["score"] = fused_scores[chunk_id]
        results.append(item)
    return results


def _resolve_product_plan(
    catalog: ProductCatalogService,
    product_code: str,
    plan_code: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
    product = next(
        (item for item in catalog.list_available() if item["product_code"] == product_code),
        None,
    )
    if product is None:
        raise AppError("PRODUCT_NOT_AVAILABLE", "产品当前不可投保", 404)
    plan = next(
        (item for item in product["plans"] if item["code"] == plan_code),
        None,
    )
    if plan is None:
        raise AppError("PLAN_NOT_AVAILABLE", "保障计划当前不可投保", 404)
    return product, plan


def _safe_order_item(item: dict[str, Any]) -> dict[str, Any]:
    return {
        "order_id": item["order_id"],
        "order_no": item["order_no"],
        "product_name": item["product_name"],
        "plan_name": item["plan_name"],
        "relationship": item["relationship"],
        "status": item["status"],
        "amount_cents": item["amount_cents"],
        "currency": item["currency"],
        "policy_no": item["policy_no"],
        "created_at": str(item["created_at"]),
    }


def _safe_policy_item(item: dict[str, Any]) -> dict[str, Any]:
    return {
        "policy_id": item["policy_id"],
        "policy_no": item["policy_no"],
        "order_no": item["order_no"],
        "product_name": item["product_name"],
        "plan_name": item["plan_name"],
        "relationship": item["relationship"],
        "status": item["status"],
        "premium_cents": item["premium_cents"],
        "currency": item["currency"],
        "coverage_start": str(item["coverage_start"]),
        "coverage_end": str(item["coverage_end"]),
        "issued_at": str(item["issued_at"]),
    }


def _filter_business_result(
    result: dict[str, Any],
    allowed_ids: set[str],
    id_field: str,
) -> dict[str, Any]:
    items = [item for item in result["items"] if str(item.get(id_field, "")) in allowed_ids]
    return {"items": items, "total": len(items)}
