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 UserAgentReviewTravelPolicyMixin:

    def _build_travel_policy_precheck_briefs(
        self,
        payload: UserAgentRequest,
        *,
        document_cards: list[UserAgentReviewDocumentCard],
        claim_groups: list[UserAgentReviewClaimGroup],
    ) -> list[UserAgentReviewRiskBrief]:
        if not document_cards or not self._is_travel_review_context(payload, document_cards, claim_groups):
            return []

        rule_catalog = ExpenseRuleRuntimeService(self.db).load_catalog()
        policy = rule_catalog.travel_policy
        if policy is None:
            return []

        employee = self._resolve_employee_profile(payload)
        grade = self._resolve_review_employee_grade(payload, employee=employee)
        grade_band = ExpenseClaimService._resolve_travel_policy_band(grade)
        band_label = policy.band_labels.get(grade_band or "", grade or "当前职级")
        declared_city = self._resolve_declared_travel_city(payload, policy)
        reason_corpus = self._build_review_reason_corpus(payload)
        has_exception_note = self._text_contains_any(reason_corpus, policy.standard_exception_keywords)
        standard_rule_name = str(getattr(policy, "standard_rule_name", "") or policy.rule_name)
        standard_rule_version = str(getattr(policy, "standard_rule_version", "") or policy.rule_version)

        briefs: list[UserAgentReviewRiskBrief] = []
        amount_measurement_lines: list[str] = []
        seen_keys: set[str] = set()

        def append_once(key: str, brief: UserAgentReviewRiskBrief) -> None:
            if key in seen_keys:
                return
            seen_keys.add(key)
            briefs.append(brief)

        for card in document_cards:
            document_type = str(card.document_type or "").strip().lower()
            suggested_type = str(card.suggested_expense_type or "").strip().lower()
            card_text = self._build_review_document_card_text(card)
            document_type_label = resolve_document_type_label(document_type)
            amount = self._extract_amount_decimal_from_card(card)

            if self._is_review_hotel_card(card):
                hotel_city = self._extract_policy_city_from_text(card_text, policy) or declared_city
                city_tier = policy.city_tiers.get(hotel_city, "tier_3")
                city_tier_label = self._format_travel_city_tier(city_tier)

                if amount is None:
                    amount_measurement_lines.append(
                        f"{card.filename}：识别为{document_type_label}，但未识别到可核算金额，无法完成住宿差标测算。"
                    )
                    append_once(
                        f"hotel-amount-missing-{card.index}",
                        UserAgentReviewRiskBrief(
                            title="住宿金额待补充",
                            level="warning",
                            content=f"{card.filename} 已识别为{document_type_label}，但未识别到可核算的住宿金额。",
                            detail=(
                                f"依据《{standard_rule_name}》（{standard_rule_version}），住宿票据需要按员工职级、城市级别和每晚金额进行差标核算。"
                                "当前票据缺少金额，系统无法判断是否超出差旅标准。"
                            ),
                            suggestion="请在票据识别结果中补充或更正住宿金额，再继续核对报销单。",
                        ),
                    )
                    continue

                if grade_band is None:
                    amount_measurement_lines.append(
                        f"{card.filename}：识别住宿金额 {amount:.2f} 元，但缺少员工职级，无法匹配住宿标准。"
                    )
                    append_once(
                        f"hotel-grade-missing-{card.index}",
                        UserAgentReviewRiskBrief(
                            title="职级信息待确认",
                            level="warning",
                            content=f"{card.filename} 已识别住宿金额 {amount:.2f} 元，但当前员工职级缺失，无法匹配住宿标准。",
                            detail=(
                                f"依据《{standard_rule_name}》（{standard_rule_version}），住宿标准按职级档位和城市级别配置。"
                                "当前未能识别员工职级，因此无法完成创建前差标核算。"
                            ),
                            suggestion="请确认员工档案或页面上下文中的职级信息，再重新进行差旅规则预检。",
                        ),
                    )
                    continue

                cap = self._resolve_review_hotel_cap(
                    policy,
                    grade_band=grade_band,
                    city=hotel_city,
                    city_tier=city_tier,
                )
                if cap <= Decimal("0.00"):
                    continue
                night_count = self._extract_review_hotel_night_count(card)
                nightly_amount = (amount / Decimal(max(night_count, 1))).quantize(Decimal("0.01"))
                if nightly_amount <= cap:
                    continue
                amount_measurement_lines.append(
                    f"{card.filename}：识别为{document_type_label}，金额 {amount:.2f} 元，"
                    f"按 {night_count} 晚折算 {nightly_amount:.2f} 元/晚；"
                    f"适用标准为 {band_label}{city_tier_label} {cap:.2f} 元/晚，超出标准。"
                )

                basis = (
                    f"依据《{standard_rule_name}》（{standard_rule_version}），{band_label} 在{city_tier_label}"
                    f"住宿标准为 {cap:.2f} 元/晚；{card.filename} 识别为{document_type_label}，"
                    f"金额 {amount:.2f} 元，按 {night_count} 晚折算约 {nightly_amount:.2f} 元/晚。"
                )
                append_once(
                    f"hotel-over-limit-{card.index}",
                    UserAgentReviewRiskBrief(
                        title="住宿超标待说明" if not has_exception_note else "住宿超标提醒",
                        level="high",
                        content=(
                            f"{card.filename} 住宿金额约 {nightly_amount:.2f} 元/晚，"
                            f"超过 {band_label} {city_tier_label}标准 {cap:.2f} 元/晚。"
                        ),
                        detail=(
                            basis
                            + (
                                "当前未识别到超标说明，创建单据前需要先补充原因。"
                                if not has_exception_note
                                else "当前已识别到例外说明，后续仍需审批人重点复核。"
                            )
                        ),
                        suggestion="补充超标说明、协议酒店满房/会议高峰等原因，或调整住宿金额后再继续。",
                    ),
                )
                continue

            if document_type == "meal_receipt":
                allowance = self._resolve_review_travel_allowance_standard(
                    policy,
                    declared_city=declared_city,
                    card_text=card_text,
                )
                if allowance is not None:
                    region_label, standard_amount = allowance
                    if amount is None:
                        amount_measurement_lines.append(
                            f"{card.filename}：识别为{document_type_label}，但未识别到可核算金额，无法按{region_label}伙食补助标准测算。"
                        )
                        append_once(
                            f"travel-meal-amount-missing-{card.index}",
                            UserAgentReviewRiskBrief(
                                title="差旅餐饮金额待补充",
                                level="high",
                                content=f"{card.filename} 已识别为{document_type_label}，但未识别到可核算金额。",
                                detail=(
                                    f"依据《{standard_rule_name}》（{standard_rule_version}），差旅餐饮票据优先按出差补助标准中的伙食补助进行测算。"
                                    f"当前匹配区域为{region_label}，但票据缺少金额，系统无法判断是否超出补助标准。"
                                ),
                                suggestion="请在票据识别结果中补充或更正餐饮金额，再继续创建报销单。",
                            ),
                        )
                        continue

                    if amount > standard_amount:
                        amount_measurement_lines.append(
                            f"{card.filename}：识别为{document_type_label}，金额 {amount:.2f} 元；"
                            f"适用《{standard_rule_name}》{region_label}伙食补助标准 {standard_amount:.2f} 元/天，超出标准。"
                        )
                        append_once(
                            f"travel-meal-allowance-over-limit-{card.index}",
                            UserAgentReviewRiskBrief(
                                title="差旅餐饮金额超出伙食补助标准",
                                level="high",
                                content=(
                                    f"{card.filename} 识别金额 {amount:.2f} 元，"
                                    f"超过{region_label}伙食补助标准 {standard_amount:.2f} 元/天。"
                                ),
                                detail=(
                                    f"依据《{standard_rule_name}》（{standard_rule_version}）的出差补助标准，"
                                    f"{region_label}伙食补助为 {standard_amount:.2f} 元/天；"
                                    f"当前票据类型识别为{document_type_label}，识别金额 {amount:.2f} 元。"
                                    "首轮上传阶段按单张票据先行测算，后续可结合出差天数和实际餐补口径复核。"
                                ),
                                suggestion="如该票据属于差旅餐补，请调整金额或补充超标/拆分说明；如属于业务招待或普通餐费，请改为对应费用类型后再提交。",
                            ),
                        )
                    continue

            scene_code = self._resolve_review_amount_scene_code(card, payload)
            scene_policy = rule_catalog.get_scene_policy(scene_code)
            scene_limit = self._resolve_review_scene_amount_limit(scene_policy)
            if scene_policy is not None and scene_limit is not None:
                metric_label = str(getattr(scene_limit, "metric_label", "") or scene_policy.label or "金额").strip()
                standard_amount = self._resolve_scene_standard_amount(scene_limit)
                if amount is None:
                    amount_measurement_lines.append(
                        f"{card.filename}：识别为{document_type_label}，但未识别到可核算金额，无法按{metric_label}测算。"
                    )
                    append_once(
                        f"{scene_code}-amount-missing-{card.index}",
                        UserAgentReviewRiskBrief(
                            title=f"{scene_policy.label}金额待补充",
                            level="warning",
                            content=f"{card.filename} 已识别为{document_type_label}，但未识别到可核算金额。",
                            detail=(
                                f"依据《{scene_policy.rule_name}》（{scene_policy.rule_version}），"
                                f"{scene_policy.label}需要按{metric_label}进行金额审核。当前票据缺少金额，系统无法判断是否合规。"
                            ),
                            suggestion="请在票据识别结果中补充或更正金额，再继续核对报销单。",
                        ),
                    )
                    continue

                amount_risk = self._evaluate_review_scene_amount(
                    amount=amount,
                    limit_config=scene_limit,
                    reason_text=reason_corpus,
                )
                if amount_risk is not None:
                    severity, threshold = amount_risk
                    if standard_amount is not None:
                        amount_measurement_lines.append(
                            f"{card.filename}：识别为{document_type_label}，金额 {amount:.2f} 元；"
                            f"适用《{scene_policy.rule_name}》{metric_label}标准 {standard_amount:.2f} 元，超出标准。"
                        )
                    append_once(
                        f"{scene_code}-amount-over-limit-{card.index}",
                        UserAgentReviewRiskBrief(
                            title=f"{scene_policy.label}金额超标待说明",
                            level="high" if severity == "high" else "warning",
                            content=(
                                f"{card.filename} 识别金额 {amount:.2f} 元，"
                                f"超过{metric_label}标准 {threshold:.2f} 元。"
                            ),
                            detail=(
                                f"依据《{scene_policy.rule_name}》（{scene_policy.rule_version}），"
                                f"{scene_policy.label}按{metric_label}审核，当前票据类型识别为{document_type_label}，"
                                f"识别金额 {amount:.2f} 元，标准阈值 {threshold:.2f} 元。"
                            ),
                            suggestion="请补充超标原因或拆分到更准确的费用类型；如属于例外场景，请在事由中写明业务背景。",
                        ),
                    )
                    continue

            transport_class = self._detect_review_transport_class(card, policy)
            if transport_class and grade_band is not None:
                transport_kind, class_label, class_level = transport_class
                allowed_level = policy.transport_limits.get(grade_band, {}).get(transport_kind)
                if allowed_level is not None and class_level > allowed_level:
                    append_once(
                        f"transport-class-over-limit-{card.index}-{class_label}",
                        UserAgentReviewRiskBrief(
                            title="交通舱位超标待说明" if not has_exception_note else "交通舱位超标提醒",
                            level="warning",
                            content=f"{card.filename} 识别为 {class_label}，{band_label} 当前默认不可报销该舱位/席别。",
                            detail=(
                                f"依据《{standard_rule_name}》（{standard_rule_version}），{band_label} 的交通席别标准"
                                f"未覆盖 {class_label}；票据类型识别为{document_type_label}。"
                                + (
                                    "当前未识别到例外说明，创建单据前需要补充原因。"
                                    if not has_exception_note
                                    else "当前已识别到例外说明，后续仍需审批人重点复核。"
                                )
                            ),
                            suggestion="补充无直达、临时改签、行程变更等例外说明，或更换为符合标准的票据。",
                        ),
                    )
                continue

            if document_type == "meal_receipt" and self._is_travel_review_context(payload, document_cards, claim_groups):
                if amount is not None:
                    amount_measurement_lines.append(
                        f"{card.filename}：识别为{document_type_label}，金额 {amount:.2f} 元；需确认按餐补、餐费或业务招待口径归口。"
                    )
                append_once(
                    f"travel-meal-card-{card.index}",
                    UserAgentReviewRiskBrief(
                        title="差旅餐饮票据待归口",
                        level="warning",
                        content=f"{card.filename} 已识别为餐饮票据，当前差旅报销单需要确认是否允许并入差旅费用。",
                        detail=(
                            f"依据《{standard_rule_name}》（{standard_rule_version}）的差旅票据预检口径，系统优先核算交通、住宿等差旅核心票据。"
                            "餐饮票据可能需要按餐费或业务招待场景拆分，并补充同行人员或客户信息。"
                        ),
                        suggestion="如属于差旅餐补，请补充制度允许口径；如属于招待或普通餐费，建议拆成对应费用类型单据。",
                    ),
                )
                continue

            if suggested_type in {"travel", "hotel", "transport"} and document_type in {"other", "travel_ticket"}:
                append_once(
                    f"travel-type-uncertain-{card.index}",
                    UserAgentReviewRiskBrief(
                        title="差旅票据类型待确认",
                        level="warning",
                        content=f"{card.filename} 归入差旅场景，但票据类型仍需确认。",
                        detail=(
                            f"依据《{standard_rule_name}》（{standard_rule_version}），差旅预检需要先明确票据是机票、火车票、住宿票据、打车票等，"
                            "再匹配对应的金额或舱位规则。当前类型识别不够稳定。"
                        ),
                        suggestion="请在附件识别结果中更正票据类型，或重新上传更清晰的附件后再继续。",
                    ),
                )

        if amount_measurement_lines:
            briefs.insert(
                0,
                UserAgentReviewRiskBrief(
                    title="附件金额测算异常",
                    level="warning",
                    content="系统根据首轮上传附件识别金额后，发现有需要进一步核查或说明的测算结果。",
                    detail="；".join(dict.fromkeys(amount_measurement_lines)),
                    suggestion="请补充超标说明、调整金额或更正票据类型后再继续。",
                ),
            )

        return briefs

