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.ontology_field_registry import normalize_ontology_form_values
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 UserAgentReviewCoreMixin:

    def _should_prompt_expense_scene_selection(self, payload: UserAgentRequest) -> bool:
        if payload.ontology.scenario != "expense":
            return False
        if payload.ontology.intent not in {"draft", "operate"}:
            return False
        if str(payload.context_json.get("review_action") or "").strip():
            return False
        review_form_values = normalize_ontology_form_values(self._resolve_review_form_values(payload))
        if str(review_form_values.get("expense_type") or "").strip():
            return False
        if self._resolve_attachment_count(payload) > 0 or self._resolve_ocr_documents(payload):
            return False
        return not any(
            item.type == "expense_type" and str(item.normalized_value or item.value or "").strip()
            for item in payload.ontology.entities
        )


    @staticmethod
    def _build_expense_scene_selection_answer(payload: UserAgentRequest) -> str:
        has_time = bool(payload.ontology.time_range.start_date or payload.ontology.time_range.raw)
        context_hint = "我先识别到这是一次报销申请"
        if has_time:
            context_hint += "，并看到了业务发生时间"
        return (
            f"{context_hint}。但您还没有明确这笔单据属于哪类报销。"
            "请先在下面选择报销场景，我会按您选择的场景继续识别时间、地点、事由、金额和所需票据，"
            "避免系统先入为主把项目支持、部署等描述误判成差旅。"
        )


    def _build_review_payload(
        self,
        payload: UserAgentRequest,
        *,
        citations: list[UserAgentCitation],
        draft_payload: UserAgentDraftPayload | None,
    ) -> UserAgentReviewPayload | None:
        attachment_count = self._resolve_attachment_count(payload)
        ocr_documents = self._resolve_ocr_documents(payload)
        if payload.ontology.scenario != "expense":
            return None
        if payload.ontology.intent not in {"draft", "operate"} and attachment_count <= 0 and not ocr_documents:
            return None

        document_cards = self._build_review_document_cards(payload, ocr_documents=ocr_documents)
        claim_groups = self._build_review_claim_groups(
            payload,
            document_cards=document_cards,
        )
        slot_cards = self._build_review_slot_cards(
            payload,
            ocr_documents=ocr_documents,
            claim_groups=claim_groups,
        )
        travel_receipt_state = self._build_travel_receipt_state(
            payload,
            document_cards=document_cards,
            claim_groups=claim_groups,
        )
        missing_slot_keys = self._resolve_review_missing_slot_keys(
            payload,
            slot_cards=slot_cards,
        )
        submission_blocked = bool(payload.tool_payload.get("submission_blocked"))
        risk_briefs = self._build_review_risk_briefs(
            payload,
            citations=citations,
            document_cards=document_cards,
            claim_groups=claim_groups,
        )
        risk_briefs.extend(self._build_travel_receipt_briefs(travel_receipt_state))
        association_choice_pending = self._is_review_association_choice_pending(payload)
        can_proceed = (
            False
            if association_choice_pending or submission_blocked or travel_receipt_state.get("blocks_next_step")
            else self._can_proceed_review(
                payload,
                missing_slot_keys=missing_slot_keys,
                claim_groups=claim_groups,
            )
        )
        confirmation_actions = self._build_review_confirmation_actions(
            payload,
            can_proceed=can_proceed,
            claim_groups=claim_groups,
            draft_payload=draft_payload,
            missing_slot_keys=missing_slot_keys,
        )
        edit_fields = self._build_review_edit_fields(
            payload,
            draft_payload=draft_payload,
            slot_cards=slot_cards,
        )
        intent_summary = self._build_review_intent_summary(
            payload,
            slot_cards=slot_cards,
            claim_groups=claim_groups,
        )
        body_message = self._build_review_body_message(
            payload,
            slot_cards=slot_cards,
            risk_briefs=risk_briefs,
            can_proceed=can_proceed,
            document_cards=document_cards,
            travel_receipt_state=travel_receipt_state,
        )
        missing_slot_labels = [SLOT_LABELS.get(key, key) for key in missing_slot_keys]
        missing_slot_labels.extend(
            str(item)
            for item in travel_receipt_state.get("required_missing_labels", [])
            if str(item).strip()
        )
        missing_slot_labels = list(dict.fromkeys(missing_slot_labels))

        return UserAgentReviewPayload(
            intent_summary=intent_summary,
            body_message=body_message,
            scenario=payload.ontology.scenario,
            intent=payload.ontology.intent,
            can_proceed=can_proceed,
            missing_slots=missing_slot_labels,
            risk_briefs=risk_briefs,
            slot_cards=slot_cards,
            document_cards=document_cards,
            claim_groups=claim_groups,
            confirmation_actions=confirmation_actions,
            edit_fields=edit_fields,
        )


    def _build_review_slot_cards(
        self,
        payload: UserAgentRequest,
        *,
        ocr_documents: list[dict[str, object]],
        claim_groups: list[UserAgentReviewClaimGroup],
    ) -> list[UserAgentReviewSlotCard]:
        entity_map = self._collect_entity_values(payload)
        time_slot = self._build_time_slot(payload)
        location_slot = self._build_location_slot(payload)
        customer_slot = self._build_customer_slot(payload, entity_map=entity_map)
        participants_slot = self._build_participants_slot(payload, entity_map=entity_map)
        amount_slot = self._build_amount_slot(payload, entity_map=entity_map, ocr_documents=ocr_documents)
        expense_type_slot = self._build_expense_type_slot(
            payload,
            entity_map=entity_map,
            ocr_documents=ocr_documents,
        )
        merchant_slot = self._build_merchant_slot(payload, ocr_documents=ocr_documents)
        reason_slot = self._build_reason_slot(
            payload,
            claim_groups=claim_groups,
        )
        attachment_slot = self._build_attachment_slot(payload)
        required_keys = self._resolve_required_review_keys(
            payload,
            primary_expense_type=str(expense_type_slot["normalized_value"] or ""),
            claim_groups=claim_groups,
        )

        cards = [
            self._make_slot_card(
                key="expense_type",
                value=expense_type_slot["value"],
                raw_value=expense_type_slot["raw_value"],
                normalized_value=expense_type_slot["normalized_value"],
                source=expense_type_slot["source"],
                confidence=expense_type_slot["confidence"],
                evidence=expense_type_slot["evidence"],
                required="expense_type" in required_keys,
            ),
            self._make_slot_card(
                key="customer_name",
                value=customer_slot["value"],
                raw_value=customer_slot["raw_value"],
                normalized_value=customer_slot["normalized_value"],
                source=customer_slot["source"],
                confidence=customer_slot["confidence"],
                evidence=customer_slot["evidence"],
                required="customer_name" in required_keys,
            ),
            self._make_slot_card(
                key="time_range",
                value=time_slot["value"],
                raw_value=time_slot["raw_value"],
                normalized_value=time_slot["normalized_value"],
                source=time_slot["source"],
                confidence=time_slot["confidence"],
                evidence=time_slot["evidence"],
                required="time_range" in required_keys,
            ),
            self._make_slot_card(
                key="location",
                value=location_slot["value"],
                raw_value=location_slot["raw_value"],
                normalized_value=location_slot["normalized_value"],
                source=location_slot["source"],
                confidence=location_slot["confidence"],
                evidence=location_slot["evidence"],
                required="location" in required_keys,
            ),
            self._make_slot_card(
                key="merchant_name",
                value=merchant_slot["value"],
                raw_value=merchant_slot["raw_value"],
                normalized_value=merchant_slot["normalized_value"],
                source=merchant_slot["source"],
                confidence=merchant_slot["confidence"],
                evidence=merchant_slot["evidence"],
                required="merchant_name" in required_keys,
            ),
            self._make_slot_card(
                key="amount",
                value=amount_slot["value"],
                raw_value=amount_slot["raw_value"],
                normalized_value=amount_slot["normalized_value"],
                source=amount_slot["source"],
                confidence=amount_slot["confidence"],
                evidence=amount_slot["evidence"],
                required="amount" in required_keys,
            ),
            self._make_slot_card(
                key="reason",
                value=reason_slot["value"],
                raw_value=reason_slot["raw_value"],
                normalized_value=reason_slot["normalized_value"],
                source=reason_slot["source"],
                confidence=reason_slot["confidence"],
                evidence=reason_slot["evidence"],
                required="reason" in required_keys,
            ),
            self._make_slot_card(
                key="participants",
                value=participants_slot["value"],
                raw_value=participants_slot["raw_value"],
                normalized_value=participants_slot["normalized_value"],
                source=participants_slot["source"],
                confidence=participants_slot["confidence"],
                evidence=participants_slot["evidence"],
                required="participants" in required_keys,
            ),
            self._make_slot_card(
                key="attachments",
                value=attachment_slot["value"],
                raw_value=attachment_slot["raw_value"],
                normalized_value=attachment_slot["normalized_value"],
                source=attachment_slot["source"],
                confidence=attachment_slot["confidence"],
                evidence=attachment_slot["evidence"],
                required="attachments" in required_keys,
            ),
        ]
        return cards


    def _build_review_document_cards(
        self,
        payload: UserAgentRequest,
        *,
        ocr_documents: list[dict[str, object]],
    ) -> list[UserAgentReviewDocumentCard]:
        cards: list[UserAgentReviewDocumentCard] = []
        for index, item in enumerate(ocr_documents, start=1):
            classified = self._classify_document(item, payload)
            fields = self._extract_document_fields(item)
            cards.append(
                UserAgentReviewDocumentCard(
                    index=index,
                    filename=str(item.get("filename") or f"document-{index}"),
                    document_type=classified["document_type"],
                    suggested_expense_type=classified["expense_type"],
                    scene_label=self._resolve_review_document_scene_label(item, classified),
                    summary=str(item.get("summary") or item.get("text") or "").strip(),
                    avg_score=float(item.get("avg_score") or 0.0),
                    preview_kind=str(item.get("preview_kind") or "").strip(),
                    preview_data_url=str(item.get("preview_data_url") or "").strip(),
                    preview_url=str(item.get("preview_url") or "").strip(),
                    warnings=[str(warning) for warning in item.get("warnings", []) if str(warning).strip()],
                    fields=[
                        UserAgentReviewDocumentField(
                            label=label,
                            value=value,
                            source="ocr",
                        )
                        for label, value in fields.items()
                        if str(value).strip()
                    ],
                )
            )
        return cards


    @staticmethod
    def _resolve_review_document_scene_label(item: dict[str, object], classified: dict[str, str]) -> str:
        provided_label = str(item.get("document_type_label") or "").strip()
        if provided_label and provided_label != "其他单据":
            return provided_label

        classified_scene_label = str(classified.get("scene_label") or "").strip()
        if classified_scene_label:
            return classified_scene_label

        document_type = str(classified.get("document_type") or item.get("document_type") or "").strip()
        document_type_label = resolve_document_type_label(document_type)
        if document_type_label and document_type_label not in {"其他单据", document_type}:
            return document_type_label

        scene_label = str(item.get("scene_label") or "").strip()
        return scene_label or "其他票据"


    def _build_review_claim_groups(
        self,
        payload: UserAgentRequest,
        *,
        document_cards: list[UserAgentReviewDocumentCard],
    ) -> list[UserAgentReviewClaimGroup]:
        groups: dict[str, dict[str, object]] = {}
        for card in document_cards:
            group_code = self._normalize_group_code(card.suggested_expense_type)
            bucket = groups.setdefault(
                group_code,
                {
                    "document_indexes": [],
                    "amount_total": 0.0,
                    "expense_type": str(card.suggested_expense_type or group_code).strip() or group_code,
                    "scene_label": GROUP_SCENE_LABELS.get(
                        str(card.suggested_expense_type or group_code).strip() or group_code,
                        GROUP_SCENE_LABELS.get(group_code, "其他费用"),
                    ),
                    "reasons": [],
                },
            )
            bucket["document_indexes"].append(card.index)
            bucket["amount_total"] = float(bucket["amount_total"]) + self._extract_amount_from_card(card)
            bucket["reasons"].append(f"{card.filename} 识别为 {card.scene_label}")
            current_expense_type = str(bucket["expense_type"] or "").strip()
            current_card_type = str(card.suggested_expense_type or "").strip()
            if current_expense_type and current_card_type and current_expense_type != current_card_type:
                bucket["expense_type"] = group_code
                bucket["scene_label"] = GROUP_SCENE_LABELS.get(group_code, "其他费用")

        if not groups:
            expense_type_code = self._collect_entity_values(payload).get("expense_type_code", "other")
            group_code = self._normalize_group_code(expense_type_code)
            groups[group_code] = {
                "document_indexes": [],
                "amount_total": self._resolve_amount_value(payload),
                "expense_type": expense_type_code or "other",
                "scene_label": GROUP_SCENE_LABELS.get(group_code, "其他费用"),
                "reasons": ["当前主要依据用户文本和页面上下文进行分单建议。"],
            }

        claim_groups: list[UserAgentReviewClaimGroup] = []
        for index, (group_code, bucket) in enumerate(groups.items(), start=1):
            title = f"建议报销单 {index}：{bucket['scene_label']}"
            rationale = (
                "；".join(dict.fromkeys(str(item) for item in bucket["reasons"]))
                if bucket["reasons"]
                else "当前仅有单一场景，无需拆单。"
            )
            claim_groups.append(
                UserAgentReviewClaimGroup(
                    group_code=group_code,
                    title=title,
                    expense_type=str(bucket["expense_type"]),
                    scene_label=str(bucket["scene_label"]),
                    document_indexes=list(bucket["document_indexes"]),
                    amount_total=round(float(bucket["amount_total"]), 2),
                    rationale=rationale,
                )
            )
        return claim_groups


    def _build_review_risk_briefs(
        self,
        payload: UserAgentRequest,
        *,
        citations: list[UserAgentCitation],
        document_cards: list[UserAgentReviewDocumentCard],
        claim_groups: list[UserAgentReviewClaimGroup],
    ) -> list[UserAgentReviewRiskBrief]:
        briefs: list[UserAgentReviewRiskBrief] = []
        for reason in self._resolve_submission_blocked_reasons(payload):
            needs_exception_explanation = self._is_submission_exception_explanation_reason(reason)
            briefs.append(
                UserAgentReviewRiskBrief(
                    title="提交风险提示",
                    level=self._resolve_submission_blocked_risk_level(reason),
                    content=reason,
                    detail=(
                        "该项不是票据归集阻断条件，系统会保留费用明细并在详情中标记高风险；"
                        "继续提交前需要补充特殊情况说明，便于审批人判断例外原因。"
                        if needs_exception_explanation
                        else (
                            "该项属于提交审批前的阻断条件。系统会先要求补齐基础字段、附件或业务说明，"
                            "否则审批人无法判断成本归属、业务真实性或票据有效性。"
                        )
                    ),
                    suggestion=(
                        "请在附加说明中写清超标或例外原因；确认业务真实后可继续提交给审批人重点复核。"
                        if needs_exception_explanation
                        else "按提示补齐对应信息；如果业务场景本身合理，请补充说明或佐证附件后再提交。"
                    ),
                )
            )

        briefs.extend(
            self._build_travel_policy_precheck_briefs(
                payload,
                document_cards=document_cards,
                claim_groups=claim_groups,
            )
        )

        employee = self._resolve_employee_profile(payload)
        employee_name = (
            str(employee.name).strip()
            if employee is not None and employee.name
            else self._collect_entity_values(payload).get("employee_name")
            or str(payload.context_json.get("name") or "").strip()
        )
        current_amount = self._resolve_amount_value(payload) or sum(
            self._extract_amount_from_card(card) for card in document_cards
        )
        if employee_name and current_amount > 0:
            since = datetime.now(UTC) - timedelta(days=90)
            claim_identity_conditions = [ExpenseClaim.employee_name == employee_name]
            if employee is not None:
                employee_identifiers = {
                    str(employee.name or "").strip(),
                    str(employee.email or "").strip(),
                    str(employee.employee_no or "").strip(),
                }
                employee_identifiers.discard("")
                claim_identity_conditions = [
                    ExpenseClaim.employee_id == employee.id,
                    ExpenseClaim.employee_name.in_(list(employee_identifiers)),
                ]
            stmt = select(ExpenseClaim).where(or_(*claim_identity_conditions), ExpenseClaim.occurred_at >= since)
            recent_claims = list(self.db.scalars(stmt).all())
            if recent_claims:
                duplicate_count = sum(
                    1
                    for item in recent_claims
                    if abs(float(item.amount) - current_amount) < 0.01
                )
                if duplicate_count:
                    briefs.append(
                        UserAgentReviewRiskBrief(
                            title="金额重复预警",
                            level="warning",
                            content=(
                                f"近 90 天发现 {duplicate_count} 笔金额相同的报销记录，"
                                "提交前建议核对是否为重复报销或拆分不当。"
                            ),
                            detail=(
                                "系统将当前金额与近 90 天历史报销金额进行比对。金额完全一致不一定违规，"
                                "但在交通、餐饮、办公采购等场景中可能提示重复票据或拆分报销。"
                            ),
                            suggestion="核对历史单据与当前票据是否对应同一业务；如不是重复，请在事由中说明差异。",
                        )
                    )

        warning_count = sum(len(item.warnings) for item in document_cards)
        if warning_count:
            briefs.append(
                UserAgentReviewRiskBrief(
                    title="票据识别提醒",
                    level="warning",
                    content=f"当前共有 {warning_count} 条票据识别提示，建议逐张确认 OCR 识别字段。",
                    detail="票据 OCR 识别存在字段缺失、置信度偏低或类型判断不稳定时，会生成该提醒。",
                    suggestion="打开票据明细逐张核对日期、金额、商户和票据类型，必要时更正后再提交。",
                )
            )

        if len(claim_groups) > 1:
            briefs.append(
                UserAgentReviewRiskBrief(
                    title="建议拆单",
                    level="warning",
                    content=f"系统检测到 {len(claim_groups)} 类费用场景，建议拆成多张报销单后再提交。",
                    detail="同一批附件中包含多类费用场景时，混在一张报销单里会影响规则匹配、附件核验和审批归口。",
                    suggestion="按费用场景拆成多张报销单，分别确认金额、事由和附件归属。",
                )
            )

        return self._filter_deprecated_review_risk_briefs(briefs)


    @staticmethod
    def _resolve_submission_blocked_risk_level(reason: str) -> str:
        normalized = re.sub(r"\s+", "", str(reason or ""))
        amount_keywords = ("金额", "超标", "费用", "价款", "票面金额", "单价", "合计")
        return "high" if any(keyword in normalized for keyword in amount_keywords) else "warning"


    @staticmethod
    def _is_submission_exception_explanation_reason(reason: str) -> bool:
        normalized = re.sub(r"\s+", "", str(reason or ""))
        if not normalized:
            return False
        has_over_standard = any(keyword in normalized for keyword in ("超标", "超出", "超标准", "差标"))
        has_explanation = any(keyword in normalized for keyword in ("说明", "原因", "例外", "特殊"))
        return has_over_standard and has_explanation


    @staticmethod
    def _filter_deprecated_review_risk_briefs(
        briefs: list[UserAgentReviewRiskBrief],
    ) -> list[UserAgentReviewRiskBrief]:
        filtered: list[UserAgentReviewRiskBrief] = []
        for brief in briefs:
            title = str(brief.title or "").strip()
            if any(keyword in title for keyword in DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS):
                continue
            filtered.append(brief)
        return filtered

