from __future__ import annotations

import json
import re
from datetime import UTC, datetime, timedelta
from decimal import Decimal, InvalidOperation
from typing import Any

from sqlalchemy import or_, select
from sqlalchemy.orm import selectinload

from app.api.deps import CurrentUserContext
from app.core.agent_enums import AgentAssetStatus, AgentAssetType
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.schemas.agent_asset import AgentAssetListItem
from app.schemas.reimbursement import TravelReimbursementCalculatorRequest
from app.schemas.user_agent import (
    UserAgentCitation,
    UserAgentDraftPayload,
    UserAgentExpenseQueryRecord,
    UserAgentQueryPayload,
    UserAgentQueryStatusGroup,
    UserAgentReviewAction,
    UserAgentReviewClaimGroup,
    UserAgentReviewDocumentCard,
    UserAgentReviewDocumentField,
    UserAgentReviewEditField,
    UserAgentReviewPayload,
    UserAgentReviewRiskBrief,
    UserAgentReviewSlotCard,
    UserAgentRequest,
    UserAgentSuggestedAction,
)
from app.services.agent_assets import AgentAssetService
from app.services.expense_claims import ExpenseClaimService
from app.services.expense_rule_runtime import ExpenseRuleRuntimeService, RuntimeTravelPolicy, resolve_document_type_label
from app.services.risk_ontology_bridge import resolve_rule_codes_for_risk_check
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
from app.services.user_agent_constants import *


class UserAgentReviewMessageMixin:

    def _build_review_confirmation_actions(
        self,
        payload: UserAgentRequest,
        *,
        can_proceed: bool,
        claim_groups: list[UserAgentReviewClaimGroup],
        draft_payload: UserAgentDraftPayload | None,
        missing_slot_keys: set[str] | None = None,
    ) -> list[UserAgentReviewAction]:
        missing_slot_keys = set(missing_slot_keys or set())
        if self._is_review_association_choice_pending(payload):
            claim_no = str(payload.tool_payload.get("association_candidate_claim_no") or "").strip()
            link_label = f"关联到草稿 {claim_no}" if claim_no else "关联到现有草稿"
            return [
                UserAgentReviewAction(
                    label=link_label,
                    action_type="link_to_existing_draft",
                    description=(
                        f"把本次上传票据并入现有草稿 {claim_no}。"
                        if claim_no
                        else "把本次上传票据并入现有草稿。"
                    ),
                    emphasis="primary",
                ),
                UserAgentReviewAction(
                    label="单独建立报销单",
                    action_type="create_new_claim_from_documents",
                    description="基于当前上传的多张票据，新建一张独立的报销草稿。",
                    emphasis="secondary",
                ),
            ]

        review_action = str(payload.context_json.get("review_action") or "").strip()
        if "expense_type" in missing_slot_keys and not review_action:
            return [
                UserAgentReviewAction(
                    label="保存为草稿",
                    action_type="save_draft",
                    description="先暂存当前已识别信息，稍后仍可从个人报销继续补充或提交。",
                    emphasis="primary",
                ),
            ]

        primary_action = UserAgentReviewAction(
            label="继续下一步" if can_proceed else "保存为草稿",
            action_type="next_step" if can_proceed else "save_draft",
            description=(
                "当前识别信息已满足继续处理条件，确认后进入下一步。"
                if can_proceed
                else "暂存当前识别结果，后续可以继续补充或修改。"
            ),
            emphasis="primary",
        )
        if len(claim_groups) > 1 and can_proceed:
            primary_action.description = f"系统建议拆分为 {len(claim_groups)} 张报销单，确认后继续下一步。"
        if draft_payload is not None and draft_payload.claim_no and not can_proceed:
            primary_action.description = f"保存后会生成草稿 {draft_payload.claim_no}，后续仍可继续补充。"

        actions = []
        if can_proceed:
            actions.append(
                UserAgentReviewAction(
                    label="保存为草稿",
                    action_type="save_draft",
                    description="先暂存当前已识别信息，稍后仍可从个人报销继续补充或提交。",
                    emphasis="secondary",
                )
            )
        actions.append(primary_action)
        return actions


    def _build_review_intent_summary(
        self,
        payload: UserAgentRequest,
        *,
        slot_cards: list[UserAgentReviewSlotCard],
        claim_groups: list[UserAgentReviewClaimGroup],
    ) -> str:
        slots = {item.key: item for item in slot_cards}
        expense_type = slots.get("expense_type")
        amount = slots.get("amount")
        time_range = slots.get("time_range")
        location = slots.get("location")
        customer = slots.get("customer_name")

        summary = "我先根据您当前提供的信息整理出一笔报销："
        if expense_type and expense_type.value:
            summary = f"识别到您希望报销一笔“{expense_type.value}”费用："
        details: list[str] = []
        if customer and customer.value:
            details.append(f"客户：{customer.value}")
        if time_range and time_range.value:
            details.append(f"时间：{time_range.value}")
        if location and location.value:
            details.append(f"地点：{location.value}")
        if amount and amount.value:
            details.append(f"金额：{amount.value}")
        reason = slots.get("reason")
        if reason and reason.value:
            details.append(f"事由：{reason.value}")
        if details:
            return "\n\n".join([summary, "基础信息识别结果：", "\n".join(details)])
        return summary


    def _build_review_body_answer(
        self,
        payload: UserAgentRequest,
        *,
        review_payload: UserAgentReviewPayload | None,
        draft_payload: UserAgentDraftPayload | None,
    ) -> str | None:
        if review_payload is None:
            return None
        if payload.ontology.scenario != "expense":
            return None
        if payload.ontology.intent not in {"draft", "operate"}:
            return None
        if payload.tool_payload.get("draft_limit_reached"):
            return (
                str(payload.tool_payload.get("message") or "").strip()
                or "您当前已保存 3 个草稿，请先完成已保存的草稿，才能再次新建草稿。"
            )

        review_action = str(payload.context_json.get("review_action") or "").strip()
        if payload.tool_payload.get("preview_only") and not review_action:
            return review_payload.body_message or self._build_review_intent_summary(
                payload,
                slot_cards=review_payload.slot_cards,
                claim_groups=review_payload.claim_groups,
            )
        if payload.tool_payload.get("duplicate_attachment_blocked") or payload.tool_payload.get("duplicate_invoice_blocked"):
            return (
                str(payload.tool_payload.get("message") or "").strip()
                or "检测到本次上传票据与当前单据已有票据重复，请重新上传不同的票据后再归集。"
            )
        if review_action == "save_draft":
            if draft_payload is not None and draft_payload.claim_no:
                return (
                    f"已按您当前确认的信息保存为草稿 {draft_payload.claim_no}。"
                    "系统已完成草稿规则校验，风险与异常可在单据详情查看。"
                    "如果还有其他票据，可以继续在当前对话上传，我会归集到这张草稿。"
                )
            return (
                "已按您当前确认的信息保存为草稿。"
                "系统已完成草稿规则校验，风险与异常可在单据详情查看。"
                "如果还有其他票据，可以继续在当前对话上传，我会归集到这张草稿。"
            )
        if review_action == "link_to_existing_draft":
            document_count = self._resolve_review_document_count(payload)
            followup_copy = self._build_review_action_followup_copy(review_payload)
            if draft_payload is not None and draft_payload.claim_no:
                return (
                    f"已将本次上传的 {document_count} 张票据关联到草稿 {draft_payload.claim_no}。"
                    f"{followup_copy or '您可以继续补充识别字段，确认无误后再提交审批。'}"
                )
            return f"已将本次上传的票据关联到现有草稿。{followup_copy or '您可以继续补充识别字段，确认无误后再提交审批。'}"
        if review_action == "create_new_claim_from_documents":
            document_count = self._resolve_review_document_count(payload)
            followup_copy = self._build_review_action_followup_copy(review_payload)
            if draft_payload is not None and draft_payload.claim_no:
                return (
                    f"已按当前上传的 {document_count} 张票据新建报销草稿 {draft_payload.claim_no}。"
                    f"{followup_copy or '您可以继续补充识别字段，确认无误后再提交审批。'}"
                )
            return f"已按当前上传票据新建报销草稿。{followup_copy or '您可以继续补充识别字段，确认无误后再提交审批。'}"
        if review_action == "next_step":
            if draft_payload is not None and draft_payload.status == "submitted":
                stage_text = draft_payload.approval_stage or "审批中"
                return f"报销单 {draft_payload.claim_no or ''} 已提交，当前节点为 {stage_text}。".strip()
            if payload.tool_payload.get("submission_blocked"):
                reasons = self._resolve_submission_blocked_reasons(payload)
                if reasons:
                    reason_lines = "\n".join(
                        f"{index}. {reason}" for index, reason in enumerate(reasons, start=1)
                    )
                    if all(self._is_submission_exception_explanation_reason(reason) for reason in reasons):
                        return (
                            "检测到当前单据存在需要说明的超标风险，但票据和费用明细会继续保留在单据中。\n"
                            f"{reason_lines}\n"
                            "如果确有特殊情况，请先在附加说明中补充原因；补充后可以继续提交给审批人重点复核。"
                        )
                    return (
                        "自动检测暂未通过，所以还没有提交到审批人。\n"
                        f"{reason_lines}\n"
                        "请先处理以上项目；处理完成后再点继续下一步。"
                    )
                return str(payload.tool_payload.get("message") or "").strip() or "当前报销单暂时还不能提交审批。"
            return (
                f"{self._build_review_intent_summary(payload, slot_cards=review_payload.slot_cards, claim_groups=review_payload.claim_groups)}\n\n"
                "当前关键信息已基本齐全，您确认无误后可以继续下一步。"
            )
        return review_payload.body_message or None


    def _build_review_body_message(
        self,
        payload: UserAgentRequest,
        *,
        slot_cards: list[UserAgentReviewSlotCard],
        risk_briefs: list[UserAgentReviewRiskBrief],
        can_proceed: bool,
        document_cards: list[UserAgentReviewDocumentCard],
        travel_receipt_state: dict[str, Any] | None = None,
    ) -> str:
        if self._is_review_association_choice_pending(payload):
            claim_no = str(payload.tool_payload.get("association_candidate_claim_no") or "").strip()
            document_count = len(document_cards) or self._resolve_review_document_count(payload)
            if claim_no:
                return (
                    f"已识别出本次上传的 {document_count} 张票据。"
                    f"系统检测到您已有草稿 {claim_no}，请选择关联到该草稿，或单独新建一张报销单。"
                )
            return (
                f"已识别出本次上传的 {document_count} 张票据。"
                "系统检测到您已有可用草稿，请先选择关联到现有草稿，或单独新建一张报销单。"
            )

        blocked_reasons = self._resolve_submission_blocked_reasons(payload)
        if blocked_reasons:
            reason_text = "；".join(dict.fromkeys(reason.strip("。；;") for reason in blocked_reasons if reason))
            if all(self._is_submission_exception_explanation_reason(reason) for reason in blocked_reasons):
                return (
                    f"检测到当前单据存在需要说明的超标风险：{reason_text}。"
                    "票据会先正常归集到单据中，并在费用明细前标记风险；"
                    "如确有特殊情况，请在附加说明中补充原因后继续提交审批。"
                )
            return (
                f"自动检测未通过：{reason_text}。"
                "请先根据风险提示补充原因、调整金额或更换附件，整改后再继续提交。"
            )

        travel_message = self._build_travel_receipt_guidance_message(
            payload,
            travel_receipt_state=travel_receipt_state or {},
            can_proceed=can_proceed,
        )
        if travel_message:
            return travel_message

        missing_labels = self._resolve_review_missing_slot_labels(slot_cards)
        if travel_receipt_state:
            missing_labels.extend(
                str(item)
                for item in travel_receipt_state.get("required_missing_labels", [])
                if str(item).strip()
            )
            missing_labels = list(dict.fromkeys(missing_labels))

        expense_type_slot = next((item for item in slot_cards if item.key == "expense_type"), None)
        if expense_type_slot is not None and not str(expense_type_slot.value or "").strip():
            return (
                f"{self._build_review_intent_summary(payload, slot_cards=slot_cards, claim_groups=[])}\n\n"
                "我已经先保留了当前识别出的时间、地点和事由，但还不能确定这张单据应该走哪类报销流程。"
                "请先点击“选择报销类型”，在差旅费、交通费、住宿费等选项中选定；"
                "选定后，后续上传的票据都会作为这张单据的补充继续核对，不会重新改判报销类型。"
            )

        review_payload = UserAgentReviewPayload(
            intent_summary="",
            body_message="",
            scenario=payload.ontology.scenario,
            intent=payload.ontology.intent,
            can_proceed=can_proceed,
            missing_slots=missing_labels,
            risk_briefs=risk_briefs,
            slot_cards=slot_cards,
            document_cards=[],
            claim_groups=[],
            confirmation_actions=[],
            edit_fields=[],
        )
        return "\n\n".join(
            item
            for item in [
                self._build_review_intent_summary(payload, slot_cards=slot_cards, claim_groups=[]),
                self._build_review_standard_calculation_copy(payload, slot_cards),
                self._build_review_guidance_copy(review_payload, mention_save_draft=not can_proceed),
            ]
            if item
        )


    def _build_review_standard_calculation_copy(
        self,
        payload: UserAgentRequest,
        slot_cards: list[UserAgentReviewSlotCard],
    ) -> str:
        slots = {item.key: item for item in slot_cards}
        expense_type = str(slots.get("expense_type").value if slots.get("expense_type") else "").strip()
        if "差旅" in expense_type:
            return self._build_review_travel_calculation_table(payload, slots)
        if "交通" in expense_type:
            return (
                "报销测算参考：交通费通常以实际票据金额为基础，结合出行地点、业务事由和票据合规性复核；"
                "如果它属于差旅行程的一部分，后续也会并入差旅费测算。"
            )
        if "住宿" in expense_type:
            return (
                "报销测算参考：住宿费通常按“实际住宿金额”和“目的地住宿标准 × 住宿天数”取合规口径；"
                "补齐酒店票据后再核对是否超标。"
            )
        return (
            "报销测算参考：先以用户填写金额或票据识别金额为基础，"
            "再结合费用类型、发生地点、业务事由和规则中心限额进行复核。"
        )


    def _build_review_travel_calculation_table(
        self,
        payload: UserAgentRequest,
        slots: dict[str, UserAgentReviewSlotCard],
    ) -> str:
        destination = self._resolve_slot_text(slots, "location")
        days = self._resolve_review_travel_days(payload, slots)
        ticket_amount = self._resolve_slot_money(slots, "amount")
        employee = self._resolve_employee_profile(payload)
        grade = self._resolve_review_employee_grade(payload, employee=employee)

        if not destination or not grade:
            return "\n".join(
                [
                    "报销测算参考：",
                    "",
                    "| 项目 | 当前信息 | 测算说明 |",
                    "| --- | --- | --- |",
                    f"| 出差地点 | {destination or '待确认'} | 用于匹配城市住宿标准和补贴区域 |",
                    f"| 出差天数 | {days} 天 | 来自业务发生时间或用户描述 |",
                    f"| 职级 | {grade or '待确认'} | 补齐后才能匹配住宿标准和补贴档位 |",
                    f"| 交通票据 | {self._format_decimal_money(ticket_amount)} 元 | 上传票据后会按真实金额重新复核 |",
                ]
            )

        current_user = CurrentUserContext(
            username=str(payload.user_id or payload.context_json.get("name") or "anonymous").strip() or "anonymous",
            name=str(payload.context_json.get("name") or payload.user_id or "anonymous").strip() or "anonymous",
            role_codes=[
                str(item).strip()
                for item in list(payload.context_json.get("role_codes") or [])
                if str(item).strip()
            ],
            is_admin=bool(payload.context_json.get("is_admin")),
            tenant_id=str(
                payload.context_json.get("tenant_id")
                or payload.context_json.get("tenantId")
                or "default"
            ).strip()
            or "default",
            department_name=str(payload.context_json.get("department_name") or payload.context_json.get("department") or "").strip(),
        )
        try:
            calculation = TravelReimbursementCalculatorService(self.db).calculate(
                TravelReimbursementCalculatorRequest(days=days, location=destination, grade=grade),
                current_user,
            )
        except Exception:
            return "\n".join(
                [
                    "报销测算参考：",
                    "",
                    "| 项目 | 当前信息 | 测算说明 |",
                    "| --- | --- | --- |",
                    f"| 出差地点 | {destination} | 暂时未能匹配规则中心地点 |",
                    f"| 出差天数 | {days} 天 | 来自业务发生时间或用户描述 |",
                    f"| 职级 | {grade} | 暂时无法自动匹配差旅标准 |",
                    f"| 交通票据 | {self._format_decimal_money(ticket_amount)} 元 | 上传票据后会按真实金额重新复核 |",
                ]
            )

        total_amount = (
            ticket_amount
            + self._coerce_decimal_money(calculation.hotel_amount)
            + self._coerce_decimal_money(calculation.allowance_amount)
        ).quantize(Decimal("0.01"))
        ticket_basis = "当前未上传交通票据，先按 0.00 元占位" if ticket_amount <= Decimal("0.00") else "已识别或填写的交通票据金额"
        return "\n".join(
            [
                "报销测算参考：",
                "",
                (
                    f"职级 {calculation.grade}，目的地 {destination}，匹配城市 {calculation.matched_city}；"
                    "补齐交通、酒店等票据后，我会按真实票据金额和规则中心标准重新复核。"
                ),
                "",
                "| 项目 | 测算口径 | 金额 |",
                "| --- | --- | ---: |",
                f"| 交通票据 | {ticket_basis} | {self._format_decimal_money(ticket_amount)} 元 |",
                f"| 住宿标准 | {self._format_decimal_money(calculation.hotel_rate)} 元/天 × {calculation.days} 天 | {self._format_decimal_money(calculation.hotel_amount)} 元 |",
                f"| 出差补贴 | {self._format_decimal_money(calculation.total_allowance_rate)} 元/天 × {calculation.days} 天 | {self._format_decimal_money(calculation.allowance_amount)} 元 |",
                f"| 参考合计 | 交通票据 + 住宿标准 + 出差补贴 | {self._format_decimal_money(total_amount)} 元 |",
            ]
        )


    @staticmethod
    def _resolve_slot_text(slots: dict[str, UserAgentReviewSlotCard], key: str) -> str:
        item = slots.get(key)
        return str(getattr(item, "value", "") or getattr(item, "raw_value", "") or "").strip()


    def _resolve_review_travel_days(
        self,
        payload: UserAgentRequest,
        slots: dict[str, UserAgentReviewSlotCard],
    ) -> int:
        text = " ".join(
            [
                str(payload.message or ""),
                str(payload.context_json.get("user_input_text") or ""),
                self._resolve_slot_text(slots, "reason"),
                self._resolve_slot_text(slots, "time_range"),
            ]
        )
        explicit_match = re.search(r"(?<!\d)(\d{1,2})\s*天", text)
        if explicit_match:
            return max(1, int(explicit_match.group(1)))

        dates = self._extract_dates_from_text(self._resolve_slot_text(slots, "time_range"))
        if len(dates) >= 2:
            return max(1, (max(dates).date() - min(dates).date()).days)
        return 1


    def _resolve_slot_money(
        self,
        slots: dict[str, UserAgentReviewSlotCard],
        key: str,
    ) -> Decimal:
        text = self._resolve_slot_text(slots, key).replace(",", "")
        match = re.search(r"([0-9]+(?:\.[0-9]{1,2})?)", text)
        if not match:
            return Decimal("0.00")
        return self._coerce_decimal_money(match.group(1))


    @staticmethod
    def _build_review_action_followup_copy(review_payload: UserAgentReviewPayload) -> str:
        missing_slots = [str(item).strip() for item in review_payload.missing_slots if str(item).strip()]
        receipt_briefs = [
            item
            for item in review_payload.risk_briefs
            if "差旅票据待补充" in str(item.title or "")
        ]
        if missing_slots:
            return f"当前仍有 {'、'.join(missing_slots)}，暂时只能保存为草稿，补齐后再继续下一步。"
        if receipt_briefs:
            return "当前仍有必需票据待补充，暂时只能保存为草稿；补齐后再继续下一步。"
        if review_payload.can_proceed:
            return "当前信息已较完整，您可以继续下一步，也可以先保存为草稿。"
        return ""


    def _build_travel_receipt_guidance_message(
        self,
        payload: UserAgentRequest,
        *,
        travel_receipt_state: dict[str, Any],
        can_proceed: bool,
    ) -> str:
        review_action = str(payload.context_json.get("review_action") or "").strip()
        if review_action or not travel_receipt_state.get("has_long_distance_ticket"):
            return ""

        employee = self._resolve_employee_profile(payload)
        user_name = (
            str(employee.name).strip()
            if employee is not None and employee.name
            else str(payload.context_json.get("name") or payload.user_id or "同事").strip()
        )
        destination = str(travel_receipt_state.get("destination") or "待确认").strip()
        days = max(1, int(travel_receipt_state.get("days") or 1))
        ticket_type_label = str(travel_receipt_state.get("ticket_type_label") or "交通").strip()
        ticket_amount = self._coerce_decimal_money(travel_receipt_state.get("ticket_amount"))

        required_labels = [
            str(item).strip()
            for item in travel_receipt_state.get("required_missing_labels", [])
            if str(item).strip()
        ]
        provide_items: list[str] = []
        if required_labels:
            provide_items.append("1. 酒店住宿发票/住宿清单（必须，当前待上传）")

        sections = [
            f"您好，{user_name}。我先按票据信息做一次差旅预检。",
            "\n".join(
                [
                    "已识别信息：",
                    f"1. 出差地点：{destination}",
                    f"2. 预计天数：{days} 天",
                    f"3. 票据类型：{ticket_type_label}票",
                    f"4. 票据金额：{self._format_decimal_money(ticket_amount)} 元",
                ]
            ),
        ]

        if provide_items:
            sections.append("还需补充：\n" + "\n".join(provide_items))
        else:
            sections.append("票据完整性：当前核心票据已较完整，无需继续上传票据。")

        if required_labels:
            sections.append(
                "处理建议：酒店票据仍缺失，暂时不能继续下一步。"
                "您可以先保存为草稿，补齐后再提交。"
            )
        elif can_proceed:
            sections.append(
                "处理建议：当前信息已较完整，确认无误后可以继续下一步；"
                "暂时不提交时，也可以先保存为草稿。"
            )

        estimate_copy = self._build_travel_receipt_estimate_copy(
            payload,
            travel_receipt_state=travel_receipt_state,
        )
        if estimate_copy:
            sections.append(estimate_copy)
        return "\n\n".join(section for section in sections if section)


    def _build_travel_receipt_estimate_copy(
        self,
        payload: UserAgentRequest,
        *,
        travel_receipt_state: dict[str, Any],
    ) -> str:
        destination = str(travel_receipt_state.get("destination") or "").strip()
        days = max(1, int(travel_receipt_state.get("days") or 1))
        ticket_type_label = str(travel_receipt_state.get("ticket_type_label") or "交通").strip()
        ticket_amount = self._coerce_decimal_money(travel_receipt_state.get("ticket_amount"))
        employee = self._resolve_employee_profile(payload)
        grade = self._resolve_review_employee_grade(payload, employee=employee)

        if not destination or not grade:
            return (
                "差旅费测算：\n"
                f"1. 职级：{grade or '待确认'}\n"
                f"2. 目的地：{destination or '出差地点待确认'}\n"
                f"3. 已提交{ticket_type_label}：{self._format_decimal_money(ticket_amount)} 元\n"
                "4. 住宿和补贴金额：需补齐职级或地点后再核算。"
            )

        current_user = CurrentUserContext(
            username=str(payload.user_id or payload.context_json.get("name") or "anonymous").strip() or "anonymous",
            name=str(payload.context_json.get("name") or payload.user_id or "anonymous").strip() or "anonymous",
            role_codes=[
                str(item).strip()
                for item in list(payload.context_json.get("role_codes") or [])
                if str(item).strip()
            ],
            is_admin=bool(payload.context_json.get("is_admin")),
            tenant_id=str(
                payload.context_json.get("tenant_id")
                or payload.context_json.get("tenantId")
                or "default"
            ).strip()
            or "default",
            department_name=str(payload.context_json.get("department_name") or payload.context_json.get("department") or "").strip(),
        )
        try:
            calculation = TravelReimbursementCalculatorService(self.db).calculate(
                TravelReimbursementCalculatorRequest(days=days, location=destination, grade=grade),
                current_user,
            )
        except Exception:
            return (
                "差旅费测算：\n"
                f"1. 职级：{grade}\n"
                f"2. 目的地：{destination}\n"
                f"3. 已提交{ticket_type_label}：{self._format_decimal_money(ticket_amount)} 元\n"
                "4. 住宿和补贴标准：暂时无法自动测算，请以规则中心最新差旅标准为准。"
            )

        total_amount = (
            ticket_amount
            + self._coerce_decimal_money(calculation.hotel_amount)
            + self._coerce_decimal_money(calculation.allowance_amount)
        ).quantize(Decimal("0.01"))
        return (
            "差旅费测算：\n"
            f"1. 职级：{calculation.grade}\n"
            f"2. 目的地：{calculation.matched_city or destination}\n"
            f"3. 已提交{ticket_type_label}：{self._format_decimal_money(ticket_amount)} 元\n"
            f"4. 住宿标准：{self._format_decimal_money(calculation.hotel_rate)} 元/天 × {calculation.days} 天\n"
            f"5. 出差补贴：{self._format_decimal_money(calculation.total_allowance_rate)} 元/天 × {calculation.days} 天\n"
            f"6. 参考合计：{self._format_decimal_money(total_amount)} 元"
        )


    @staticmethod
    def _coerce_decimal_money(value: Any) -> Decimal:
        try:
            return Decimal(str(value or "0")).quantize(Decimal("0.01"))
        except (InvalidOperation, ValueError):
            return Decimal("0.00")


    @staticmethod
    def _format_decimal_money(value: Any) -> str:
        return f"{UserAgentReviewMessageMixin._coerce_decimal_money(value):.2f}"


    @staticmethod
    def _resolve_review_missing_slot_labels(
        slot_cards: list[UserAgentReviewSlotCard],
    ) -> list[str]:
        return [item.label for item in slot_cards if item.status == "missing"]


    @staticmethod
    def _build_review_guidance_copy(
        review_payload: UserAgentReviewPayload,
        *,
        mention_save_draft: bool,
    ) -> str:
        reminder_count = len(review_payload.risk_briefs)

        if review_payload.can_proceed:
            if reminder_count:
                return (
                    f"当前关键信息已基本齐全，但还有 {reminder_count} 条提醒。"
                    "请核查对话中的文字说明，确认无误后继续下一步。"
                )
            return "当前关键信息已基本齐全，您确认无误后可以继续下一步。"

        return ""


    @staticmethod
    def _can_proceed_review(
        payload: UserAgentRequest,
        *,
        missing_slot_keys: list[str],
        claim_groups: list[UserAgentReviewClaimGroup],
    ) -> bool:
        if payload.ontology.ambiguity:
            return False
        if missing_slot_keys:
            return False
        if not claim_groups:
            return False
        return True
