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 ExpenseClaimAttachmentDocumentMixin:
    def _build_attachment_payload(
        self,
        item: ExpenseClaimItem,
        *,
        current_user: CurrentUserContext | None = None,
    ) -> dict[str, Any]:
        file_path, media_type, filename = self._resolve_item_attachment_content(item)
        metadata = self._attachment_storage.read_meta(file_path)
        metadata = self._repair_attachment_metadata_from_source_receipt_if_needed(
            file_path=file_path,
            metadata=metadata,
            item=item,
            current_user=current_user,
        )
        metadata = self._repair_pdf_text_layer_metadata_if_needed(
            file_path=file_path,
            metadata=metadata,
            item=item,
        )
        metadata = self._refresh_pdf_attachment_preview_meta_if_needed(
            file_path=file_path,
            metadata=metadata,
        )
        if self._attachment_metadata_needs_analysis_refresh(metadata):
            self._refresh_item_attachment_analysis(item)
            metadata = self._attachment_storage.read_meta(file_path)
        uploaded_at_value = metadata.get("uploaded_at")
        uploaded_at = None
        if isinstance(uploaded_at_value, str) and uploaded_at_value.strip():
            try:
                uploaded_at = datetime.fromisoformat(uploaded_at_value)
            except ValueError:
                uploaded_at = None

        analysis = metadata.get("analysis")
        if not isinstance(analysis, dict):
            analysis = None

        document_info = metadata.get("document_info")
        if not isinstance(document_info, dict):
            document_info = None

        requirement_check = metadata.get("requirement_check")
        if not isinstance(requirement_check, dict):
            requirement_check = None

        preview_kind = str(metadata.get("preview_kind") or "").strip()
        previewable = bool(metadata.get("previewable", self._attachment_presentation.is_previewable_media_type(media_type, filename)))
        preview_url = self._attachment_presentation.build_preview_client_path(item.claim_id, item.id) if previewable else ""

        return {
            "file_name": str(metadata.get("file_name") or filename),
            "storage_key": str(item.invoice_id or ""),
            "media_type": str(metadata.get("media_type") or media_type),
            "size_bytes": int(metadata.get("size_bytes") or file_path.stat().st_size),
            "uploaded_at": uploaded_at,
            "previewable": previewable,
            "preview_kind": preview_kind or self._attachment_presentation.resolve_preview_kind(media_type, filename),
            "preview_url": preview_url,
            "analysis": analysis,
            "document_info": document_info,
            "requirement_check": requirement_check,
        }

    def _repair_attachment_metadata_from_source_receipt_if_needed(
        self,
        *,
        file_path: Path,
        metadata: dict[str, Any],
        item: ExpenseClaimItem,
        current_user: CurrentUserContext | None,
    ) -> dict[str, Any]:
        if not metadata or current_user is None:
            return metadata

        source_receipt_id = str(metadata.get("source_receipt_id") or "").strip()
        if not source_receipt_id:
            return metadata
        if not self._attachment_metadata_needs_source_receipt_repair(metadata):
            return metadata

        source_document = self._resolve_source_receipt_document(
            source_receipt_id=source_receipt_id,
            current_user=current_user,
            fallback_filename=str(metadata.get("file_name") or file_path.name),
            fallback_media_type=str(metadata.get("media_type") or ""),
        )
        if source_document is None:
            return metadata

        document_info = self._build_attachment_document_info(source_document)
        requirement_check = self._build_attachment_requirement_check(
            item=item,
            document_info=document_info,
        )
        preview_meta = self._attachment_presentation.build_preview_meta(
            file_path=file_path,
            media_type=str(
                metadata.get("media_type")
                or self._attachment_presentation.resolve_media_type(file_path.name)
            ),
            ocr_document=source_document,
        )

        metadata.update(
            {
                "previewable": bool(preview_meta["previewable"]),
                "preview_kind": str(preview_meta["preview_kind"]),
                "preview_storage_key": str(preview_meta["preview_storage_key"]),
                "preview_media_type": str(preview_meta["preview_media_type"]),
                "preview_file_name": str(preview_meta["preview_file_name"]),
                "preview_rendered_with": str(preview_meta.get("preview_rendered_with") or ""),
                "analysis": self._build_attachment_analysis(
                    document=source_document,
                    item=item,
                    claim=getattr(item, "claim", None),
                    document_info=document_info,
                    requirement_check=requirement_check,
                ),
                "document_info": document_info,
                "requirement_check": requirement_check,
                "ocr_status": "recognized",
                "ocr_error": "",
                "ocr_text": str(getattr(source_document, "text", "") or ""),
                "ocr_summary": str(getattr(source_document, "summary", "") or ""),
                "ocr_avg_score": float(getattr(source_document, "avg_score", 0.0) or 0.0),
                "ocr_line_count": int(getattr(source_document, "line_count", 0) or 0),
                "ocr_classification_source": str(
                    getattr(source_document, "classification_source", "") or ""
                ),
                "ocr_classification_confidence": float(
                    getattr(source_document, "classification_confidence", 0.0) or 0.0
                ),
                "ocr_classification_evidence": [
                    str(value)
                    for value in list(getattr(source_document, "classification_evidence", []) or [])
                    if str(value).strip()
                ],
                "ocr_warnings": [
                    str(value)
                    for value in list(getattr(source_document, "warnings", []) or [])
                    if str(value).strip()
                ],
            }
        )
        self._attachment_storage.write_meta(file_path, metadata)
        return metadata

    @classmethod
    def _attachment_metadata_needs_source_receipt_repair(cls, metadata: dict[str, Any]) -> bool:
        document_info = metadata.get("document_info")
        document_type = ""
        fields: list[Any] = []
        if isinstance(document_info, dict):
            document_type = str(document_info.get("document_type") or "").strip()
            fields = list(document_info.get("fields") or [])

        return (
            str(metadata.get("preview_kind") or "").strip() != "image"
            or document_type in {"", "other"}
            or not any(
                isinstance(field, dict) and str(field.get("value") or "").strip()
                for field in fields
            )
        )

    @classmethod
    def _attachment_metadata_needs_analysis_refresh(cls, metadata: dict[str, Any]) -> bool:
        analysis = metadata.get("analysis")
        if not isinstance(analysis, dict):
            return cls._attachment_metadata_has_ocr_signal(metadata)

        points = [
            str(point or "").strip()
            for point in list(analysis.get("points") or [])
            if str(point or "").strip()
        ]
        if not points:
            return False

        if any("未识别到有效文字" in point for point in points):
            return cls._attachment_metadata_has_readable_signal(metadata)

        if any("未识别到列车出发时间" in point or "未识别到开票日期" in point for point in points):
            return cls._attachment_metadata_has_date_field(metadata)

        return False

    @classmethod
    def _attachment_metadata_has_ocr_signal(cls, metadata: dict[str, Any]) -> bool:
        return bool(
            str(metadata.get("ocr_text") or "").strip()
            or str(metadata.get("ocr_summary") or "").strip()
            or int(metadata.get("ocr_line_count") or 0) > 0
            or cls._attachment_metadata_document_fields(metadata)
        )

    @classmethod
    def _attachment_metadata_has_readable_signal(cls, metadata: dict[str, Any]) -> bool:
        return bool(
            str(metadata.get("ocr_text") or "").strip()
            or str(metadata.get("ocr_summary") or "").strip()
            or int(metadata.get("ocr_line_count") or 0) > 0
            or cls._attachment_metadata_document_fields(metadata)
        )

    @staticmethod
    def _attachment_metadata_document_fields(metadata: dict[str, Any]) -> list[dict[str, Any]]:
        document_info = metadata.get("document_info")
        if not isinstance(document_info, dict):
            return []
        return [
            field
            for field in list(document_info.get("fields") or [])
            if isinstance(field, dict) and str(field.get("value") or "").strip()
        ]

    @classmethod
    def _attachment_metadata_has_date_field(cls, metadata: dict[str, Any]) -> bool:
        for field in cls._attachment_metadata_document_fields(metadata):
            key = str(field.get("key") or "").strip().lower().replace("_", "")
            label = str(field.get("label") or "").replace(" ", "")
            if key in {"date", "tripdate", "departuredate", "invoicedate"}:
                return True
            if any(token in label for token in ("日期", "时间", "出发")):
                return True
        return False

    def _build_attachment_document_info(self, document: Any) -> dict[str, Any]:
        insight = build_document_insight(
            filename=str(getattr(document, "filename", "") or ""),
            summary=str(getattr(document, "summary", "") or ""),
            text=str(getattr(document, "text", "") or ""),
        )
        document_type = str(getattr(document, "document_type", "") or "").strip()
        if document_type in {"", "other"}:
            document_type = insight.document_type

        document_type_label = str(getattr(document, "document_type_label", "") or "").strip()
        if not document_type_label or document_type_label == "其他单据":
            document_type_label = insight.document_type_label

        scene_code = str(getattr(document, "scene_code", "") or "").strip()
        if scene_code in {"", "other"}:
            scene_code = insight.scene_code

        scene_label = str(getattr(document, "scene_label", "") or "").strip()
        if not scene_label or scene_label == "其他票据":
            scene_label = insight.scene_label

        raw_fields = list(getattr(document, "document_fields", []) or [])
        normalized_fields: list[dict[str, str]] = []
        for item in raw_fields:
            key = ""
            label = ""
            value = ""
            if isinstance(item, dict):
                key = str(item.get("key") or "").strip()
                label = str(item.get("label") or "").strip()
                value = str(item.get("value") or "").strip()
            else:
                key = str(getattr(item, "key", "") or "").strip()
                label = str(getattr(item, "label", "") or "").strip()
                value = str(getattr(item, "value", "") or "").strip()
            if key and label and value:
                label = self._resolve_document_field_display_label(
                    document_type=document_type,
                    key=key,
                    label=label,
                )
                normalized_fields.append(
                    {
                        "key": key,
                        "label": label,
                        "value": value,
                    }
                )

        if not normalized_fields:
            normalized_fields = [
                {
                    "key": field.key,
                    "label": field.label,
                    "value": field.value,
                }
                for field in insight.fields
                if field.value
            ]

        return {
            "document_type": document_type,
            "document_type_label": document_type_label,
            "scene_code": scene_code,
            "scene_label": scene_label,
            "fields": normalized_fields,
        }

    @staticmethod
    def _resolve_document_field_display_label(
        *,
        document_type: str,
        key: str,
        label: str,
    ) -> str:
        trip_label = DOCUMENT_TRIP_DATE_LABELS.get(
            str(document_type or "").strip().lower()
        )
        if not trip_label:
            return label

        normalized_key = str(key or "").strip().lower().replace("_", "")
        normalized_label = str(label or "").replace(" ", "")
        if normalized_key in DOCUMENT_INVOICE_DATE_KEYS or any(
            token in normalized_label for token in DOCUMENT_INVOICE_DATE_LABEL_TOKENS
        ):
            return label

        is_date_field = (
            normalized_key
            in DOCUMENT_TRIP_DATE_KEYS
            | DOCUMENT_GENERIC_DATE_KEYS
            or any(
                token in normalized_label
                for token in (
                    *DOCUMENT_TRIP_DATE_LABEL_TOKENS,
                    *DOCUMENT_GENERIC_DATE_LABEL_TOKENS,
                )
            )
        )
        return trip_label if is_date_field else label

    def _backfill_item_type_from_attachment(
        self,
        *,
        item: ExpenseClaimItem,
        document_info: dict[str, Any],
    ) -> None:
        current_type = str(item.item_type or "").strip().lower()
        if current_type not in GENERIC_ATTACHMENT_BACKFILL_ITEM_TYPES:
            return

        document_type = str(document_info.get("document_type") or "").strip()
        mapped_type = DOCUMENT_TYPE_ITEM_TYPE_MAP.get(document_type)
        if mapped_type:
            item.item_type = mapped_type

    def _backfill_item_amount_from_attachment(
        self,
        *,
        item: ExpenseClaimItem,
        document: Any,
        document_info: dict[str, Any],
    ) -> None:
        current_amount = Decimal(item.item_amount or Decimal("0.00")).quantize(Decimal("0.01"))
        if current_amount > Decimal("0.00"):
            return

        amount = self._resolve_document_item_amount(
            {
                "document_fields": document_info.get("fields") or [],
                "summary": str(getattr(document, "summary", "") or ""),
                "text": str(getattr(document, "text", "") or ""),
            }
        )
        if amount is not None and amount > Decimal("0.00"):
            item.item_amount = amount

    def _backfill_item_date_from_attachment(
        self,
        *,
        item: ExpenseClaimItem,
        document: Any,
        document_info: dict[str, Any],
    ) -> None:
        document_payload = {
            "document_type": str(document_info.get("document_type") or "").strip(),
            "scene_code": str(document_info.get("scene_code") or "").strip(),
            "summary": str(getattr(document, "summary", "") or "").strip(),
            "text": str(getattr(document, "text", "") or "").strip(),
            "document_fields": list(document_info.get("fields") or []),
        }
        parsed = self._resolve_document_item_date_candidate(document_payload)
        if parsed is not None:
            item.item_date = parsed

    def _backfill_item_reason_from_attachment(
        self,
        *,
        item: ExpenseClaimItem,
        document: Any,
        document_info: dict[str, Any],
    ) -> None:
        reason = self._resolve_document_item_reason(
            {
                "document_type": str(document_info.get("document_type") or "").strip(),
                "scene_code": str(document_info.get("scene_code") or "").strip(),
                "scene_label": str(document_info.get("scene_label") or "").strip(),
                "document_fields": document_info.get("fields") or [],
                "summary": str(getattr(document, "summary", "") or ""),
                "text": str(getattr(document, "text", "") or ""),
            },
            fallback=str(item.item_reason or "").strip(),
        )
        if reason:
            item.item_reason = reason
