from __future__ import annotations

import json
import re
import shutil
import uuid
from collections import defaultdict
from datetime import UTC, date, datetime, timedelta
from decimal import Decimal, InvalidOperation
from pathlib import Path
from types import SimpleNamespace
from typing import Any

from sqlalchemy import func, or_, select
from sqlalchemy import inspect as sqlalchemy_inspect
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, selectinload

from app.api.deps import CurrentUserContext
from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentAssetType
from app.models.agent_asset import AgentAsset
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.schemas.ontology import OntologyEntity, OntologyParseResult
from app.schemas.reimbursement import (
    ExpenseClaimItemCreate,
    ExpenseClaimItemUpdate,
    ExpenseClaimUpdate,
    TravelReimbursementCalculatorRequest,
)
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
from app.services.agent_foundation import AgentFoundationService
from app.services.audit import AuditLogService
from app.services.document_intelligence import build_document_insight
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_attachment_presentation import ExpenseClaimAttachmentPresentation
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
from app.services.expense_claim_constants import (
    EXPENSE_TYPE_LABELS,
    MAX_DRAFT_CLAIMS_PER_USER,
    EDITABLE_CLAIM_STATUSES,
    SYSTEM_GENERATED_ITEM_TYPES,
    TRAVEL_DETAIL_ITEM_TYPES,
    TRAVEL_ALLOWANCE_TRIGGER_ITEM_TYPES,
    DOCUMENT_TYPE_ITEM_TYPE_MAP,
    DOCUMENT_TYPE_SCENE_MAP,
    DOCUMENT_FACT_ITEM_TYPES,
    ROUTE_DESCRIPTION_ITEM_TYPES,
    DOCUMENT_TRIP_DATE_LABELS,
    DOCUMENT_TRIP_DATE_REQUIREMENT_LABELS,
    DOCUMENT_TRIP_DATE_KEYS,
    DOCUMENT_GENERIC_DATE_KEYS,
    DOCUMENT_INVOICE_DATE_KEYS,
    DOCUMENT_TRIP_DATE_LABEL_TOKENS,
    DOCUMENT_GENERIC_DATE_LABEL_TOKENS,
    DOCUMENT_INVOICE_DATE_LABEL_TOKENS,
    DOCUMENT_ROUTE_FORMAT_PATTERN,
    DOCUMENT_ROUTE_TEXT_PATTERN,
    DOCUMENT_ROUTE_ORIGIN_LABELS,
    DOCUMENT_ROUTE_DESTINATION_LABELS,
    GENERIC_ATTACHMENT_BACKFILL_ITEM_TYPES,
    LOCATION_REQUIRED_EXPENSE_TYPES,
    EXPENSE_SCENE_KEYWORDS,
    EXPENSE_TYPE_ALLOWED_DOCUMENT_SCENES,
    DOCUMENT_SCENE_LABELS,
    DOCUMENT_ASSOCIATION_REVIEW_ACTIONS,
    PERSISTENT_EXPENSE_REVIEW_ACTIONS,
    RETURN_REASON_OPTIONS,
    MAX_CLAIM_NO_RETRY_ATTEMPTS,
    DOCUMENT_DATE_PATTERN,
    SYSTEM_GENERATED_REASON_PREFIXES,
    LEADING_REASON_TIME_PATTERNS,
    AI_REVIEW_LOOKBACK_DAYS,
    AI_REVIEW_REPEAT_RISK_WARNING_COUNT,
    AI_REVIEW_REPEAT_RISK_BLOCK_COUNT,
    TRAVEL_REVIEW_RELEVANT_EXPENSE_TYPES,
    TRAVEL_REVIEW_LONG_DISTANCE_DOCUMENT_TYPES,
    TRAVEL_POLICY_CITY_TIERS,
    TRAVEL_POLICY_CITY_MATCH_ORDER,
    TRAVEL_POLICY_BAND_LABELS,
    TRAVEL_POLICY_HOTEL_LIMITS,
    TRAVEL_POLICY_ALLOWED_TRANSPORT_LEVELS,
    TRAVEL_POLICY_ROUTE_EXCEPTION_KEYWORDS,
    TRAVEL_POLICY_STANDARD_EXCEPTION_KEYWORDS,
    TRAVEL_POLICY_FLIGHT_CLASS_PATTERNS,
    TRAVEL_POLICY_TRAIN_CLASS_PATTERNS,
    TRAVEL_POLICY_HOTEL_NIGHT_PATTERN,
)
from app.services.expense_claim_risk_review import ExpenseClaimRiskReviewMixin
from app.services.expense_amounts import (
    extract_amount_candidates,
    format_decimal_amount,
    is_amount_match_date_fragment,
    is_date_like_amount_candidate,
    is_probable_year_amount,
    parse_document_amount_value,
    parse_plain_document_amount_value,
    resolve_document_field_amount,
    resolve_document_item_amount,
    resolve_document_text_amount,
)
from app.services.expense_rule_runtime import (
    DEFAULT_SCENE_RULE_ASSET_CODE,
    ExpenseRuleRuntimeService,
    RuntimeTravelPolicy,
    build_default_expense_rule_catalog,
    resolve_document_type_label,
)
from app.services.ocr import OcrService


class ExpenseClaimAttachmentAnalysisMixin:
    def _build_attachment_expense_audit_points(
        self,
        *,
        document: Any,
        item: ExpenseClaimItem,
        document_info: dict[str, Any],
    ) -> list[str]:
        text = " ".join(
            [
                str(getattr(document, "summary", "") or "").strip(),
                str(getattr(document, "text", "") or "").strip(),
            ]
        ).strip()
        document_payload = {
            "document_fields": document_info.get("fields") or [],
            "summary": str(getattr(document, "summary", "") or ""),
            "text": str(getattr(document, "text", "") or ""),
        }
        field_amount = self._resolve_document_field_amount(document_payload)
        audited_amount = self._resolve_document_item_amount(document_payload)
        item_amount = Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01"))

        points: list[str] = []
        if (
            field_amount is not None
            and audited_amount is not None
            and self._is_date_like_amount_candidate(field_amount, text)
            and abs(field_amount - audited_amount) > Decimal("1.00")
        ):
            points.append(
                "费用核算：OCR 金额疑似误取日期"
                f" {self._format_decimal_amount(field_amount)}，"
                f"已按票据文本中的总费用 {self._format_decimal_amount(audited_amount)} 元回填，"
                "请核对酒店或票据原文总额。"
            )

        if (
            audited_amount is not None
            and item_amount > Decimal("0.00")
            and abs(audited_amount - item_amount) > Decimal("1.00")
        ):
            points.append(
                f"费用核算：票据文本复核金额为 {self._format_decimal_amount(audited_amount)} 元，"
                f"当前明细金额为 {self._format_decimal_amount(item_amount)} 元，请确认是否需要调整。"
            )

        return points

    def _build_attachment_travel_policy_audit(
        self,
        *,
        document: Any,
        item: ExpenseClaimItem,
        document_info: dict[str, Any],
        claim: ExpenseClaim | None = None,
    ) -> dict[str, Any]:
        policy = self._get_expense_rule_catalog().travel_policy
        if policy is None:
            return {"points": [], "rule_basis": [], "has_high_risk": False}

        item_type = str(item.item_type or "").strip().lower()
        document_type = str(document_info.get("document_type") or "").strip().lower()
        scene_code = str(document_info.get("scene_code") or "").strip().lower()
        if not (
            item_type in {"hotel", "hotel_ticket"}
            or document_type == "hotel_invoice"
            or scene_code == "hotel"
        ):
            return {"points": [], "rule_basis": [], "has_high_risk": False}

        item_amount = Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01"))
        if item_amount <= Decimal("0.00"):
            return {"points": [], "rule_basis": [], "has_high_risk": False}

        claim = claim or getattr(item, "claim", None)
        grade_band = self._resolve_travel_policy_band(getattr(claim, "employee_grade", None))
        rule_name = str(policy.standard_rule_name or policy.rule_name or "公司差旅费报销规则").strip()
        rule_version = str(policy.standard_rule_version or policy.rule_version or "").strip()
        version_text = f"（{rule_version}）" if rule_version else ""
        rule_basis = [
            f"依据《{rule_name}》{version_text}，住宿费按员工职级、出差城市和每晚金额进行差标核算。"
        ]
        if grade_band is None:
            return {
                "points": ["住宿标准：当前员工职级缺失，无法匹配规则中心的住宿报销标准。"],
                "rule_basis": rule_basis,
                "has_high_risk": False,
            }

        text = " ".join(
            [
                str(getattr(document, "summary", "") or "").strip(),
                str(getattr(document, "text", "") or "").strip(),
            ]
        ).strip()
        context = {
            "item": item,
            "document_info": document_info,
            "ocr_summary": str(getattr(document, "summary", "") or "").strip(),
            "ocr_text": str(getattr(document, "text", "") or "").strip(),
        }
        hotel_city = self._extract_hotel_city(context, policy)
        claim_city = self._extract_city_from_text(str(getattr(claim, "location", "") or ""), policy) if claim else ""
        reason_city = self._extract_city_from_text(str(getattr(claim, "reason", "") or ""), policy) if claim else ""
        baseline_city = hotel_city or claim_city or reason_city
        if not baseline_city:
            baseline_city = self._extract_city_from_text(text, policy)
        if not baseline_city:
            return {
                "points": ["住宿标准：未能从酒店名称、出差地点或票据内容匹配到规则中心城市，无法核算住宿差标。"],
                "rule_basis": rule_basis,
                "has_high_risk": False,
            }

        standard = self._resolve_travel_policy_hotel_standard(
            policy=policy,
            grade_band=grade_band,
            city=baseline_city,
        )
        if standard is None:
            return {"points": [], "rule_basis": rule_basis, "has_high_risk": False}

        cap, standard_label = standard
        night_count = self._extract_hotel_night_count(context)
        nightly_amount = (item_amount / Decimal(max(night_count, 1))).quantize(Decimal("0.01"))
        if nightly_amount <= cap:
            return {"points": [], "rule_basis": rule_basis, "has_high_risk": False}

        band_label = policy.band_labels.get(grade_band, str(getattr(claim, "employee_grade", "") or "当前职级").strip())
        over_amount = (nightly_amount - cap).quantize(Decimal("0.01"))
        return {
            "points": [
                (
                    f"住宿标准：{band_label}在{standard_label}的住宿标准为 "
                    f"{self._format_decimal_amount(cap)} 元/晚，票据识别金额 "
                    f"{self._format_decimal_amount(item_amount)} 元 / {night_count} 晚，"
                    f"约 {self._format_decimal_amount(nightly_amount)} 元/晚，"
                    f"超出 {self._format_decimal_amount(over_amount)} 元/晚。"
                )
            ],
            "rule_basis": rule_basis,
            "has_high_risk": True,
        }

    def _build_attachment_requirement_check(
        self,
        *,
        item: ExpenseClaimItem,
        document_info: dict[str, Any],
    ) -> dict[str, Any]:
        expense_type = str(item.item_type or "").strip().lower() or "other"
        policy = self._get_expense_scene_policy(expense_type)
        expense_label = policy.label if policy is not None else self._resolve_expense_type_label(expense_type)
        allowed_scenes = set(policy.allowed_scene_codes) if policy is not None else set()
        allowed_document_types = set(policy.allowed_document_types) if policy is not None else set()
        allowed_scene_labels = [self._resolve_document_scene_label(code) for code in sorted(allowed_scenes)]
        allowed_document_type_labels = [
            resolve_document_type_label(document_type)
            for document_type in sorted(allowed_document_types)
        ]
        recognized_scene_code = str(document_info.get("scene_code") or "other").strip() or "other"
        recognized_scene_label = str(
            document_info.get("scene_label") or self._resolve_document_scene_label(recognized_scene_code)
        ).strip()
        recognized_document_type = str(document_info.get("document_type") or "other").strip() or "other"
        recognized_document_type_label = str(document_info.get("document_type_label") or "其他单据").strip() or "其他单据"
        matches = (
            (not allowed_scenes and not allowed_document_types)
            or recognized_scene_code in allowed_scenes
            or recognized_document_type in allowed_document_types
        )

        if matches:
            if allowed_scene_labels or allowed_document_type_labels:
                message = (
                    f"当前费用项目为{expense_label}，已识别为{recognized_document_type_label}，"
                    f"符合当前{expense_label}场景的附件要求。"
                )
            else:
                message = f"当前费用项目为{expense_label}，已识别为{recognized_document_type_label}。"
        else:
            expected_parts = [label + "相关票据" for label in allowed_scene_labels]
            expected_parts.extend(allowed_document_type_labels)
            expected_text = "、".join(dict.fromkeys(part for part in expected_parts if part)) or "对应场景票据"
            message = (
                f"当前费用项目为{expense_label}，要求上传{expected_text}；"
                f"当前识别为{recognized_document_type_label}，不符合当前场景，建议过滤或更换附件。"
            )

        return {
            "matches": matches,
            "current_expense_type": expense_type,
            "current_expense_type_label": expense_label,
            "allowed_scene_labels": allowed_scene_labels,
            "allowed_document_type_labels": allowed_document_type_labels,
            "recognized_scene_code": recognized_scene_code,
            "recognized_scene_label": recognized_scene_label,
            "recognized_document_type": recognized_document_type,
            "recognized_document_type_label": recognized_document_type_label,
            "mismatch_severity": policy.attachment_mismatch_severity if policy is not None else "high",
            "rule_code": policy.rule_code if policy is not None else DEFAULT_SCENE_RULE_ASSET_CODE,
            "rule_name": policy.rule_name if policy is not None else "报销场景提交与附件标准",
            "message": message,
        }

    @staticmethod
    def _resolve_document_scene_label(scene_code: str) -> str:
        normalized = str(scene_code or "").strip().lower()
        return DOCUMENT_SCENE_LABELS.get(normalized, "其他票据")

    @staticmethod
    def _extract_amount_candidates(text: str) -> list[Decimal]:
        return extract_amount_candidates(text)

    @staticmethod
    def _is_amount_match_date_fragment(
        amount: Decimal,
        text: str,
        start: int,
        end: int,
    ) -> bool:
        return is_amount_match_date_fragment(amount, text, start, end)

    @staticmethod
    def _has_date_like_text(text: str) -> bool:
        return bool(re.search(r"(20\d{2}(?:[年/\-.]|\s+)\d{1,2}(?:[月/\-.]|\s+)\d{1,2}日?)", text))

    @staticmethod
    def _has_document_date_field(document_info: dict[str, Any]) -> bool:
        date_keys = DOCUMENT_TRIP_DATE_KEYS | DOCUMENT_GENERIC_DATE_KEYS | DOCUMENT_INVOICE_DATE_KEYS
        date_label_tokens = (
            *DOCUMENT_TRIP_DATE_LABEL_TOKENS,
            *DOCUMENT_GENERIC_DATE_LABEL_TOKENS,
            *DOCUMENT_INVOICE_DATE_LABEL_TOKENS,
        )
        for field in list(document_info.get("fields") or []):
            if not isinstance(field, dict):
                continue
            value = str(field.get("value") or "").strip()
            if not value:
                continue
            key = str(field.get("key") or "").strip().lower().replace("_", "")
            label = str(field.get("label") or "").replace(" ", "")
            if key in date_keys or any(token in label for token in date_label_tokens):
                return True
        return False

    @staticmethod
    def _normalize_match_text(text: str) -> str:
        return re.sub(r"\s+", "", str(text or "")).lower()

    @staticmethod
    def _resolve_expense_type_label(expense_type: str | None) -> str:
        normalized = str(expense_type or "").strip().lower()
        return EXPENSE_TYPE_LABELS.get(normalized, "其他")

    def _resolve_allowed_document_scenes(self, expense_type: str | None) -> set[str]:
        normalized = str(expense_type or "").strip().lower()
        policy = self._get_expense_scene_policy(normalized)
        allowed_scenes = set(policy.allowed_scene_codes) if policy is not None else set()
        allowed_scenes.update(EXPENSE_TYPE_ALLOWED_DOCUMENT_SCENES.get(normalized, set()))
        return allowed_scenes

    def _resolve_document_analysis_scenes(self, document_info: dict[str, Any], text: str) -> set[str]:
        scenes: set[str] = set()
        recognized_scene_code = str(document_info.get("scene_code") or "").strip().lower()
        if recognized_scene_code and recognized_scene_code != "other":
            scenes.add(recognized_scene_code)

        recognized_document_type = str(document_info.get("document_type") or "").strip().lower()
        mapped_scene = DOCUMENT_TYPE_SCENE_MAP.get(recognized_document_type)
        if mapped_scene:
            scenes.add(mapped_scene)

        if scenes:
            return scenes
        return set(self._detect_expense_scenes(text).keys())

    def _detect_expense_scenes(self, text: str) -> dict[str, list[str]]:
        normalized = self._normalize_match_text(text)
        if not normalized:
            return {}

        matches: dict[str, list[str]] = {}
        for scene, keywords in EXPENSE_SCENE_KEYWORDS.items():
            matched = [keyword for keyword in keywords if keyword in normalized]
            if matched:
                matches[scene] = matched[:3]
        return matches

    def _format_scene_labels(self, scene_codes: set[str]) -> str:
        labels = [self._resolve_expense_type_label(code) for code in scene_codes]
        unique_labels = list(dict.fromkeys(label for label in labels if label))
        return "、".join(unique_labels) if unique_labels else "其他"

    def _build_purpose_mismatch_point(
        self,
        *,
        item: ExpenseClaimItem,
        document_scenes: set[str],
    ) -> str | None:
        if not document_scenes:
            return None

        allowed_scenes = self._resolve_allowed_document_scenes(item.item_type)
        document_scene_labels = self._format_scene_labels(document_scenes)

        if allowed_scenes and document_scenes.isdisjoint(allowed_scenes):
            expense_label = self._resolve_expense_type_label(item.item_type)
            return f"附件类型：当前费用项目为{expense_label}，但附件内容更像{document_scene_labels}相关票据。"

        return None

    @staticmethod
    def _is_valid_route_description(value: str) -> bool:
        text = str(value or "").strip()
        if not text:
            return False
        if DOCUMENT_DATE_PATTERN.search(text):
            return False
        return bool(DOCUMENT_ROUTE_FORMAT_PATTERN.match(text))

    def _build_route_format_point(
        self,
        *,
        item: ExpenseClaimItem,
        document_info: dict[str, Any],
    ) -> str | None:
        item_type = str(item.item_type or "").strip().lower()
        document_type = str(document_info.get("document_type") or "").strip().lower()
        route_required = item_type in ROUTE_DESCRIPTION_ITEM_TYPES or document_type in {
            "train_ticket",
            "flight_itinerary",
            "taxi_receipt",
            "transport_receipt",
        }
        if not route_required:
            return None

        reason = str(item.item_reason or "").strip()
        if self._is_valid_route_description(reason):
            return None

        example = "广州南-北京南" if item_type != "ride_ticket" else "深圳北站-腾讯滨海大厦"
        current = f"当前为“{reason[:30]}”，" if reason else ""
        return (
            f"行程说明：{current}格式应为“起始地-目的地”，"
            f"例如“{example}”，请按票据行程补充。"
        )

    def _build_fallback_attachment_analysis(
        self,
        *,
        media_type: str | None,
        item: ExpenseClaimItem,
    ) -> dict[str, Any]:
        return {
            "severity": "medium",
            "label": "中风险",
            "headline": "AI提示：附件已上传，待识别结果",
            "summary": "附件已成功保存，但当前尚未拿到有效识别结果，建议人工先核对票据内容。",
            "points": [
                f"附件格式：{self._attachment_presentation.resolve_media_type('attachment', fallback=media_type)}",
                f"费用金额：当前明细金额为 {item.item_amount} 元",
            ],
            "suggestion": "建议打开附件确认金额、日期和票据类型是否完整，再继续提交审批。",
        }

    def _build_failed_ocr_attachment_analysis(
        self,
        *,
        media_type: str | None,
        error_message: str,
        item: ExpenseClaimItem,
    ) -> dict[str, Any]:
        return {
            "severity": "medium",
            "label": "中风险",
            "headline": "AI提示：附件已上传，但识别失败",
            "summary": "文件已经保存成功，但本次 AI 识别未完成，因此无法给出完整票据核验结论。",
            "points": [
                f"识别异常：{error_message or 'OCR 服务暂不可用'}",
                f"费用金额：当前明细金额为 {item.item_amount} 元",
                f"附件格式：{self._attachment_presentation.resolve_media_type('attachment', fallback=media_type)}",
            ],
            "suggestion": "建议重新上传更清晰的票据图片，或稍后重试识别后再提交。",
        }

    def _build_attachment_analysis(
        self,
        *,
        document: Any,
        item: ExpenseClaimItem,
        claim: ExpenseClaim | None = None,
        document_info: dict[str, Any] | None = None,
        requirement_check: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        warnings = [str(value).strip() for value in list(getattr(document, "warnings", []) or []) if str(value).strip()]
        text = " ".join(
            [
                str(getattr(document, "summary", "") or "").strip(),
                str(getattr(document, "text", "") or "").strip(),
            ]
        ).strip()
        compact_text = text.replace(" ", "")
        avg_score = float(getattr(document, "avg_score", 0.0) or 0.0)
        line_count = int(getattr(document, "line_count", 0) or 0)
        document_info = document_info or self._build_attachment_document_info(document)
        requirement_check = requirement_check or self._build_attachment_requirement_check(
            item=item,
            document_info=document_info,
        )
        document_scenes = self._resolve_document_analysis_scenes(document_info, text)
        purpose_mismatch_point = self._build_purpose_mismatch_point(
            item=item,
            document_scenes=document_scenes,
        )
        route_format_point = self._build_route_format_point(
            item=item,
            document_info=document_info,
        )
        expense_audit_points = self._build_attachment_expense_audit_points(
            document=document,
            item=item,
            document_info=document_info,
        )
        travel_policy_audit = self._build_attachment_travel_policy_audit(
            document=document,
            item=item,
            claim=claim,
            document_info=document_info,
        )
        travel_policy_points = [
            str(point).strip()
            for point in list(travel_policy_audit.get("points") or [])
            if str(point).strip()
        ]
        travel_policy_rule_basis = [
            str(point).strip()
            for point in list(travel_policy_audit.get("rule_basis") or [])
            if str(point).strip()
        ]
        travel_policy_high_risk = bool(travel_policy_audit.get("has_high_risk"))
        recognized_document_type = str(document_info.get("document_type") or "other").strip().lower() or "other"
        recognized_document_label = str(document_info.get("document_type_label") or "其他单据").strip() or "其他单据"
        requirement_matches = bool(requirement_check.get("matches"))
        mismatch_severity = str(requirement_check.get("mismatch_severity") or "high").strip().lower() or "high"
        document_fields = [
            field
            for field in list(document_info.get("fields") or [])
            if isinstance(field, dict) and str(field.get("value") or "").strip()
        ]
        has_readable_content = bool(line_count > 0 or compact_text or document_fields)

        has_ticket_keyword = any(
            keyword in compact_text
            for keyword in (
                "发票",
                "票据",
                "增值税",
                "电子行程单",
                "购买方",
                "销售方",
                "税额",
                "价税",
                "票号",
                "发票代码",
                "凭证",
            )
        )
        amount_candidates = self._extract_amount_candidates(text)
        field_amount = self._resolve_document_field_amount({"document_fields": document_fields})
        if field_amount is not None and field_amount not in amount_candidates:
            amount_candidates.insert(0, field_amount)
        item_amount = Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01"))
        has_matching_amount = any(abs(candidate - item_amount) <= Decimal("1.00") for candidate in amount_candidates)
        has_date_text = self._has_date_like_text(text) or self._has_document_date_field(document_info)
        amount_mismatch = bool(amount_candidates) and item_amount > Decimal("0.00") and not has_matching_amount

        points: list[str] = []
        if warnings:
            points.append(f"识别提示：{warnings[0]}")
        if not has_readable_content:
            points.append("附件内容：未识别到有效文字，当前附件更像普通图片或内容过于模糊。")
        if recognized_document_type == "other" and not has_ticket_keyword:
            points.append("票据类型：未识别到发票、票据、电子行程单等关键字，暂无法判断票据类型。")
        if not amount_candidates:
            points.append("金额字段：未识别到可用于核对的金额。")
        elif amount_mismatch:
            candidate_text = "、".join(str(candidate) for candidate in amount_candidates[:3])
            points.append(f"金额字段：附件识别金额 {candidate_text} 元与报销金额 {item_amount} 元不一致。")
        if not has_date_text:
            date_requirement = DOCUMENT_TRIP_DATE_REQUIREMENT_LABELS.get(
                recognized_document_type,
                "开票日期或业务发生日期",
            )
            points.append(f"日期字段：未识别到{date_requirement}。")
        if not requirement_matches:
            points.append(f"附件类型要求：{requirement_check.get('message')}")
        points.extend(expense_audit_points)
        points.extend(travel_policy_points)
        if purpose_mismatch_point:
            points.append(purpose_mismatch_point)
        if route_format_point:
            points.append(route_format_point)
        if avg_score and avg_score < 0.72:
            points.append(f"识别质量：OCR 置信度偏低（{avg_score:.0%}），可能影响票据核验准确性。")

        issue_count = len(points)
        if issue_count == 0:
            return {
                "severity": "pass",
                "label": "AI提示符合条件",
                "headline": "AI提示：附件符合基础校验条件",
                "summary": "已识别到票据类型和关键字段，且符合当前费用场景的附件要求。",
                "points": [
                    f"票据类型：已识别为{recognized_document_label}。",
                    f"附件类型要求：{requirement_check.get('message')}",
                    f"金额字段：已识别到与当前明细接近的金额 {item_amount} 元。",
                ],
                "rule_basis": travel_policy_rule_basis,
                "suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。",
            }

        severity = "low"
        label = "低风险"
        headline = "AI提示：附件存在轻微待核对项"
        summary = "当前附件已识别出部分票据要素，但仍建议人工继续复核。"

        if travel_policy_high_risk:
            severity = "high"
            label = "高风险"
            headline = "AI提示：住宿金额超出报销标准"
            summary = "当前住宿票据金额超过规则中心差旅住宿标准，已作为风险项保留在单据中；如需按特殊情况提交，请补充超标原因。"
        elif (
            not has_readable_content
            or (recognized_document_type == "other" and not has_ticket_keyword and issue_count >= 2)
            or (not requirement_matches and mismatch_severity == "high")
            or (purpose_mismatch_point and amount_mismatch)
        ):
            severity = "high"
            label = "高风险"
            headline = "AI提示：附件不符合票据校验条件"
            summary = "当前附件存在明显异常，票据类型与当前费用场景不匹配，或无法作为有效报销材料。"
        elif (
            purpose_mismatch_point
            or route_format_point
            or expense_audit_points
            or travel_policy_points
            or amount_mismatch
            or issue_count >= 2
            or warnings
            or (avg_score and avg_score < 0.72)
            or (not requirement_matches and mismatch_severity in {"medium", "low"})
        ):
            severity = "medium"
            label = "中风险"
            headline = "AI提示：附件存在明显待整改项"
            summary = "当前附件可见部分内容，但金额、用途、日期或附件类型仍有缺失或不一致。"
            if route_format_point and issue_count == 1:
                summary = "票据行程已识别，但费用明细说明未按“起始地-目的地”格式填写。"
            elif expense_audit_points and issue_count == len(expense_audit_points):
                summary = "OCR 金额已完成二次核算，请按票据原文总额复核。"
            elif travel_policy_points and issue_count == len(travel_policy_points):
                summary = "住宿票据已识别，但当前缺少职级或城市信息，无法完成差旅住宿标准核算。"

        suggestion = {
            "high": "建议过滤当前不匹配的票据，重新上传符合当前费用场景的清晰原件。",
            "medium": "建议根据风险点补齐清晰票据，或修正金额、日期、费用说明后再提交。",
            "low": "建议人工再次核对金额和业务说明，确认后可继续流转。",
        }[severity]
        if travel_policy_high_risk:
            suggestion = "请核对住宿发票金额、晚数和出差城市；如确需超标，需在附加说明中补充超标说明并提交审批重点复核。"

        return {
            "severity": severity,
            "label": label,
            "headline": headline,
            "summary": summary,
            "points": points,
            "rule_basis": list(dict.fromkeys(travel_policy_rule_basis)),
            "suggestion": suggestion,
        }
