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_type_keywords import resolve_expense_type_code_from_text
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.ontology_field_registry import normalize_ontology_form_values
from app.services.ocr import OcrService


class ExpenseClaimOntologyResolverMixin:
    def _resolve_employee(
        self,
        *,
        ontology: OntologyParseResult,
        context_json: dict[str, Any],
        user_id: str | None,
    ) -> Employee | None:
        normalized_user_id = str(user_id or "").strip()
        if normalized_user_id:
            stmt = (
                select(Employee)
                .options(selectinload(Employee.organization_unit), selectinload(Employee.manager))
                .where(func.lower(Employee.email) == normalized_user_id.lower())
                .limit(1)
            )
            employee = self.db.scalar(stmt)
            if employee is not None:
                return employee

        employee_name = self._resolve_employee_name(
            ontology=ontology,
            context_json=context_json,
            user_id=None,
        )
        if not employee_name:
            return None

        stmt = (
            select(Employee)
            .options(selectinload(Employee.organization_unit), selectinload(Employee.manager))
            .where(Employee.name == employee_name)
            .limit(1)
        )
        return self.db.scalar(stmt)

    @staticmethod
    def _resolve_employee_name(
        *,
        ontology: OntologyParseResult,
        context_json: dict[str, Any],
        user_id: str | None,
        fallback: str = "待补充",
    ) -> str:
        review_form_values = context_json.get("review_form_values")
        if isinstance(review_form_values, dict):
            for key in ("reporter_name", "employee_name", "claimant_name"):
                value = str(review_form_values.get(key) or "").strip()
                if value:
                    return value
        for item in ontology.entities:
            if item.type == "employee" and item.value.strip():
                return item.value.strip()
        for key in ("name", "user_name", "employee_name"):
            value = str(context_json.get(key) or "").strip()
            if value:
                return value
        return str(user_id or fallback).strip() or fallback

    @staticmethod
    def _resolve_department_name(
        *,
        employee: Employee | None,
        context_json: dict[str, Any],
        fallback: str = "待补充",
    ) -> str:
        if employee is not None and employee.organization_unit is not None:
            return employee.organization_unit.name

        request_context = context_json.get("request_context")
        if isinstance(request_context, dict):
            for key in ("department", "department_name", "deptName"):
                value = str(request_context.get(key) or "").strip()
                if value:
                    return value

        for key in ("department_name", "department"):
            value = str(context_json.get(key) or "").strip()
            if value:
                return value
        return fallback

    @staticmethod
    def _resolve_project_code(entities: list[OntologyEntity]) -> str | None:
        for item in entities:
            if item.type == "project" and item.normalized_value.strip():
                return item.normalized_value.strip()
        return None

    @staticmethod
    def _resolve_explicit_review_expense_type(context_json: dict[str, Any]) -> str | None:
        review_form_values = context_json.get("review_form_values")
        if isinstance(review_form_values, dict):
            review_form_values = normalize_ontology_form_values(review_form_values)
            compact = str(review_form_values.get("expense_type") or "").replace(" ", "")
            if compact:
                return resolve_expense_type_code_from_text(compact)
        return None

    @staticmethod
    def _resolve_expense_type(
        entities: list[OntologyEntity],
        *,
        context_json: dict[str, Any],
    ) -> str | None:
        explicit_expense_type = ExpenseClaimOntologyResolverMixin._resolve_explicit_review_expense_type(context_json)
        if explicit_expense_type:
            return explicit_expense_type
        for item in entities:
            if item.type == "expense_type":
                normalized = item.normalized_value.strip()
                if normalized:
                    return normalized
        return None

    @staticmethod
    def _resolve_reason(
        *,
        message: str,
        context_json: dict[str, Any],
        allow_message_fallback: bool,
    ) -> str | None:
        review_form_values = context_json.get("review_form_values")
        if isinstance(review_form_values, dict):
            review_form_values = normalize_ontology_form_values(review_form_values)
            value = str(review_form_values.get("reason") or "").strip()
            if value:
                return ExpenseClaimOntologyResolverMixin._strip_leading_time_from_reason(value)

        explicit_text = context_json.get("user_input_text")
        if isinstance(explicit_text, str):
            normalized_explicit_text = explicit_text.strip()
            if normalized_explicit_text:
                return ExpenseClaimOntologyResolverMixin._strip_leading_time_from_reason(normalized_explicit_text)[:500] or None
            return None

        request_context = context_json.get("request_context")
        if (
            isinstance(request_context, dict)
            and str(context_json.get("entry_source") or "").strip() == "detail"
        ):
            for key in ("reason", "title"):
                value = str(request_context.get(key) or "").strip()
                if value:
                    return value
        if not allow_message_fallback:
            return None

        normalized_message = str(message or "").strip()
        compact_message = re.sub(r"\s+", "", normalized_message)
        if compact_message.startswith(SYSTEM_GENERATED_REASON_PREFIXES):
            return None
        return ExpenseClaimOntologyResolverMixin._strip_leading_time_from_reason(normalized_message)[:500] or None

    @staticmethod
    def _strip_leading_time_from_reason(value: str) -> str:
        reason = str(value or "").strip()
        for pattern in LEADING_REASON_TIME_PATTERNS:
            next_reason = pattern.sub("", reason).strip()
            if next_reason != reason:
                return next_reason
        return reason

    @staticmethod
    def _resolve_location(*, message: str, context_json: dict[str, Any]) -> str | None:
        review_form_values = context_json.get("review_form_values")
        if isinstance(review_form_values, dict):
            review_form_values = normalize_ontology_form_values(review_form_values)
            value = str(review_form_values.get("location") or "").strip()
            if value:
                return value

        request_context = context_json.get("request_context")
        if (
            isinstance(request_context, dict)
            and str(context_json.get("entry_source") or "").strip() == "detail"
        ):
            for key in ("city", "location"):
                value = str(request_context.get(key) or "").strip()
                if value:
                    return value
        compact = str(message or "").replace(" ", "")
        city_match = re.search(
            r"去(?P<city>[\u4e00-\u9fa5]{2,8}?)(?:出差|拜访|参会|见客户|客户现场|支撑|支持|部署|实施|处理|协助)",
            compact,
        )
        if city_match:
            return city_match.group("city").strip()
        if "客户现场" in compact:
            return "客户现场"
        return None

    @staticmethod
    def _resolve_occurred_at(
        ontology: OntologyParseResult,
        *,
        context_json: dict[str, Any],
    ) -> datetime | None:
        review_form_values = context_json.get("review_form_values")
        if isinstance(review_form_values, dict):
            review_form_values = normalize_ontology_form_values(review_form_values)
            value = str(review_form_values.get("time_range") or "").strip()
            if value:
                try:
                    parsed = date.fromisoformat(value)
                    return datetime(parsed.year, parsed.month, parsed.day, tzinfo=UTC)
                except ValueError:
                    parsed = ExpenseClaimOntologyResolverMixin._resolve_first_date_from_text(value)
                    if parsed is not None:
                        return datetime(parsed.year, parsed.month, parsed.day, tzinfo=UTC)

        start_date = ontology.time_range.start_date
        if start_date:
            try:
                parsed = date.fromisoformat(start_date)
                return datetime(parsed.year, parsed.month, parsed.day, tzinfo=UTC)
            except ValueError:
                pass
        return None

    @staticmethod
    def _resolve_first_date_from_text(value: str) -> date | None:
        match = re.search(r"20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}", str(value or ""))
        if not match:
            return None
        normalized = match.group(0).replace("/", "-").replace(".", "-")
        parts = [part for part in normalized.split("-") if part]
        if len(parts) != 3:
            return None
        try:
            year, month, day = (int(part) for part in parts)
            return date(year, month, day)
        except ValueError:
            return None

    @staticmethod
    def _resolve_amount(
        entities: list[OntologyEntity],
        *,
        context_json: dict[str, Any],
    ) -> Decimal | None:
        review_form_values = context_json.get("review_form_values")
        if isinstance(review_form_values, dict):
            raw_value = str(review_form_values.get("amount") or "").strip()
            if raw_value:
                compact = raw_value.replace("元", "").replace(",", "").strip()
                try:
                    return Decimal(compact).quantize(Decimal("0.01"))
                except (InvalidOperation, ValueError):
                    pass
        for item in entities:
            if item.type != "amount" or item.role == "threshold":
                continue
            try:
                return Decimal(item.normalized_value).quantize(Decimal("0.01"))
            except (InvalidOperation, ValueError):
                continue
        return None

    @staticmethod
    def _resolve_attachment_names(context_json: dict[str, Any]) -> list[str]:
        names = context_json.get("attachment_names")
        if not isinstance(names, list):
            return []
        return [str(name).strip() for name in names if str(name).strip()]

    def _resolve_attachment_count(self, context_json: dict[str, Any]) -> int:
        names = self._resolve_attachment_names(context_json)
        if names:
            return len(names)
        try:
            return max(0, int(context_json.get("attachment_count") or 0))
        except (TypeError, ValueError):
            return 0
