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_errors import ExpenseClaimSubmissionBlockedError
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 ExpenseClaimReviewPreviewMixin:
    def save_or_submit_from_ontology(
        self,
        *,
        run_id: str,
        user_id: str | None,
        message: str,
        ontology: OntologyParseResult,
        context_json: dict[str, Any],
    ) -> dict[str, Any]:
        review_action = str(context_json.get("review_action") or "").strip()
        if review_action not in PERSISTENT_EXPENSE_REVIEW_ACTIONS:
            return self._build_expense_review_preview_result(
                user_id=user_id,
                message=message,
                ontology=ontology,
                context_json=context_json,
            )

        result = self.upsert_draft_from_ontology(
            run_id=run_id,
            user_id=user_id,
            message=message,
            ontology=ontology,
            context_json=context_json,
        )

        if review_action != "next_step":
            return result

        claim_id = str(result.get("claim_id") or "").strip()
        if not claim_id or result.get("draft_limit_reached"):
            return result

        current_user = CurrentUserContext(
            username=str(user_id or context_json.get("name") or "anonymous").strip() or "anonymous",
            name=str(context_json.get("name") or user_id or "anonymous").strip() or "anonymous",
            role_codes=[
                str(item).strip()
                for item in list(context_json.get("role_codes") or [])
                if str(item).strip()
            ],
            is_admin=bool(context_json.get("is_admin")),
            tenant_id=str(
                context_json.get("tenant_id")
                or context_json.get("tenantId")
                or "default"
            ).strip()
            or "default",
            department_name=str(context_json.get("department_name") or context_json.get("department") or "").strip(),
        )

        try:
            claim = self.submit_claim(claim_id, current_user)
        except ExpenseClaimSubmissionBlockedError as exc:
            return {
                **result,
                "message": self._format_submission_blocked_message(exc.issues),
                "submission_blocked": True,
                "submission_blocked_reasons": exc.issues,
                "missing_fields": exc.issues,
                "draft_only": False,
            }
        except ValueError as exc:
            message = str(exc)
            return {
                **result,
                "message": message,
                "submission_blocked": True,
                "submission_blocked_reasons": [message] if message else [],
                "missing_fields": [message] if message else [],
                "draft_only": False,
            }

        if claim is None:
            return {
                **result,
                "message": "未找到可提交的报销单，请刷新后重试。",
                "submission_blocked": True,
                "draft_only": False,
            }

        if str(claim.status or "").strip().lower() != "submitted":
            review_message = ""
            for flag in list(claim.risk_flags_json or []):
                if not isinstance(flag, dict):
                    continue
                if str(flag.get("source") or "").strip() != "submission_review":
                    continue
                review_message = str(flag.get("message") or "").strip()
                if review_message:
                    break
            return {
                "message": review_message or f"报销单 {claim.claim_no} 经自动检测后转为待补充，请先修正后再提交。",
                "submission_blocked": True,
                "draft_only": False,
                "claim_id": claim.id,
                "claim_no": claim.claim_no,
                "status": claim.status,
                "approval_stage": claim.approval_stage,
                "amount": float(claim.amount),
                "invoice_count": int(claim.invoice_count or 0),
            }

        return {
            "message": (
                f"报销单 {claim.claim_no} 已完成自动检测，"
                f"当前节点为 {claim.approval_stage or '审批中'}。"
            ),
            "draft_only": False,
            "claim_id": claim.id,
            "claim_no": claim.claim_no,
            "status": claim.status,
            "approval_stage": claim.approval_stage,
            "amount": float(claim.amount),
            "invoice_count": int(claim.invoice_count or 0),
        }

    def _build_expense_review_preview_result(
        self,
        *,
        user_id: str | None,
        message: str,
        ontology: OntologyParseResult,
        context_json: dict[str, Any],
    ) -> dict[str, Any]:
        attachment_count = self._resolve_attachment_count(context_json)
        calculation_copy = self._build_expense_review_preview_calculation_copy(
            user_id=user_id,
            message=message,
            ontology=ontology,
            context_json=context_json,
        )
        return {
            "message": "\n\n".join(
                item
                for item in [
                    "我已先整理出本次报销的待核对信息。下面是基于当前信息的制度测算，票据补齐后会按真实金额重新复核。",
                    calculation_copy,
                ]
                if item
            ),
            "draft_only": True,
            "preview_only": True,
            "status": "preview",
            "invoice_count": attachment_count,
        }

    def _build_expense_review_preview_calculation_copy(
        self,
        *,
        user_id: str | None,
        message: str,
        ontology: OntologyParseResult,
        context_json: dict[str, Any],
    ) -> str:
        expense_type = self._resolve_explicit_review_expense_type(context_json) or self._resolve_expense_type(
            ontology.entities,
            context_json=context_json,
        )
        if expense_type == "travel" or (
            (not expense_type or expense_type == "other")
            and self._should_preview_as_travel(message=message, context_json=context_json)
        ):
            return self._build_travel_review_preview_calculation_copy(
                user_id=user_id,
                message=message,
                ontology=ontology,
                context_json=context_json,
            )

        amount = self._resolve_amount(ontology.entities, context_json=context_json) or Decimal("0.00")
        expense_label = EXPENSE_TYPE_LABELS.get(str(expense_type or "").strip(), "当前费用")
        return "\n".join(
            [
                "报销测算参考：",
                "",
                "| 项目 | 当前信息 | 复核口径 |",
                "| --- | --- | --- |",
                f"| 费用类型 | {expense_label} | 匹配规则中心对应费用标准 |",
                f"| 票据金额 | {self._format_decimal_amount(amount)} 元 | 以真实票据识别金额和用户确认金额为准 |",
                "| 规则校验 | 待票据和关键信息补齐 | 按费用类型、发生地点、业务事由和审批口径复核 |",
            ]
        )

    def _build_travel_review_preview_calculation_copy(
        self,
        *,
        user_id: str | None,
        message: str,
        ontology: OntologyParseResult,
        context_json: dict[str, Any],
    ) -> str:
        location = self._resolve_location(message=message, context_json=context_json) or "待确认"
        occurred_at = self._resolve_occurred_at(ontology, context_json=context_json) or datetime.now(UTC)
        days, _, _ = self._resolve_travel_allowance_days(
            context_json=context_json,
            occurred_at=occurred_at,
        )
        amount = self._resolve_amount(ontology.entities, context_json=context_json) or Decimal("0.00")
        employee = self._resolve_employee(
            ontology=ontology,
            context_json=context_json,
            user_id=user_id,
        )
        grade = str(
            context_json.get("employee_grade")
            or context_json.get("grade")
            or context_json.get("user_grade")
            or (employee.grade if employee is not None else "")
            or ""
        ).strip()

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

        try:
            from app.services.travel_reimbursement_calculator import (
                TravelReimbursementCalculatorService,
            )

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

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

    @staticmethod
    def _should_preview_as_travel(*, message: str, context_json: dict[str, Any]) -> bool:
        text_parts = [message]
        review_form_values = context_json.get("review_form_values")
        if isinstance(review_form_values, dict):
            text_parts.extend(str(value or "") for value in review_form_values.values())
        text_parts.extend(str(context_json.get(key) or "") for key in ("user_input_text", "raw_text", "ocr_summary"))
        compact = "".join(text_parts)
        return any(keyword in compact for keyword in ("差旅", "出差", "火车票", "机票", "酒店", "住宿票"))
