from __future__ import annotations

from collections.abc import Callable
from datetime import UTC, datetime
from decimal import Decimal
from typing import Any

from sqlalchemy import delete, select
from sqlalchemy.orm import Session, selectinload

from app.api.deps import CurrentUserContext
from app.models.employee import Employee
from app.models.expense_case import BusinessEvent
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.hermes_report import HermesRiskReport
from app.models.risk_observation import RiskObservation, RiskObservationFeedback
from app.schemas.reimbursement import (
    ExpenseClaimItemCreate,
    ExpenseClaimItemUpdate,
    ExpenseClaimUpdate,
)
from app.services.audit import AuditLogService
from app.services.budget_types import BudgetControlError
from app.services.document_numbering import is_application_claim_no
from app.services.expense_cases import ExpenseCaseService
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_action_protocol import ExpenseClaimActionProtocolMixin
from app.services.expense_claim_application_handoff import ExpenseClaimApplicationHandoffMixin
from app.services.expense_claim_approval_flow import ExpenseClaimApprovalFlowMixin
from app.services.expense_claim_approval_routing import ExpenseClaimApprovalRoutingMixin
from app.services.expense_claim_attachment_analysis import ExpenseClaimAttachmentAnalysisMixin
from app.services.expense_claim_attachment_commercial import (
    stage_attachment_deletion,
    stage_claim_attachment_deletion,
)
from app.services.expense_claim_attachment_document import ExpenseClaimAttachmentDocumentMixin
from app.services.expense_claim_attachment_operations import ExpenseClaimAttachmentOperationsMixin
from app.services.expense_claim_attachment_presentation import ExpenseClaimAttachmentPresentation
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
from app.services.expense_claim_budget_flow import ExpenseClaimBudgetFlowMixin
from app.services.expense_claim_document_item_builder import ExpenseClaimDocumentItemBuilderMixin
from app.services.expense_claim_document_parsing import ExpenseClaimDocumentParsingMixin
from app.services.expense_claim_draft_flow import ExpenseClaimDraftFlowMixin
from app.services.expense_claim_draft_persistence import ExpenseClaimDraftPersistenceMixin
from app.services.expense_claim_errors import (
    ExpenseClaimPreReviewBlockedError,
    ExpenseClaimSubmissionBlockedError,
)
from app.services.expense_claim_ontology_resolvers import ExpenseClaimOntologyResolverMixin
from app.services.expense_claim_pagination import ExpenseClaimPaginationMixin
from app.services.expense_claim_pre_review import ExpenseClaimPreReviewMixin
from app.services.expense_claim_pre_review_decision import (
    pre_review_identity_matches,
    pre_review_public_payload,
)
from app.services.expense_claim_read_model import ExpenseClaimReadModelMixin
from app.services.expense_claim_return_flow import ExpenseClaimReturnFlowMixin
from app.services.expense_claim_review_preview import ExpenseClaimReviewPreviewMixin
from app.services.expense_claim_risk_flags import dedupe_claim_risk_flags
from app.services.expense_claim_risk_review import ExpenseClaimRiskReviewMixin
from app.services.expense_claim_risk_stage import with_risk_business_stage
from app.services.expense_claim_standard_adjustment import (
    ExpenseClaimStandardAdjustmentMixin,
)
from app.services.expense_claim_workflow_constants import DIRECT_MANAGER_APPROVAL_STAGE
from app.services.expense_claim_workflow_repair import ExpenseClaimWorkflowRepairMixin
from app.services.receipt_folder import ReceiptFolderService


class ExpenseClaimItemActionMixin:
    def update_claim_item(
        self,
        *,
        claim_id: str,
        item_id: str,
        payload: ExpenseClaimItemUpdate,
        current_user: CurrentUserContext,
    ) -> ExpenseClaim | None:
        claim = self.get_claim(claim_id, current_user)
        if claim is None:
            return None

        self._ensure_draft_claim(claim)
        item = next((entry for entry in claim.items if entry.id == item_id), None)
        if item is None:
            raise LookupError("Item not found")
        self._ensure_mutable_claim_item(item)

        before_json = self._serialize_claim(claim)

        if payload.item_date is not None:
            item.item_date = payload.item_date
        if payload.item_type is not None:
            item.item_type = (
                self._normalize_optional_text(payload.item_type, fallback=item.item_type)
                or item.item_type
            )
        if payload.item_reason is not None:
            item.item_reason = (
                self._normalize_optional_text(payload.item_reason, allow_empty=True) or ""
            )
        if payload.item_location is not None:
            item.item_location = (
                self._normalize_optional_text(payload.item_location, allow_empty=True) or ""
            )
        if payload.item_note is not None:
            item.item_note = (
                self._normalize_optional_text(payload.item_note, allow_empty=True) or ""
            )
        if payload.item_amount is not None:
            amount = payload.item_amount.quantize(Decimal("0.01"))
            if amount < Decimal("0.00"):
                raise ValueError("费用金额不能小于 0。")
            item.item_amount = amount
        if payload.invoice_id is not None:
            item.invoice_id = self._normalize_optional_text(payload.invoice_id, allow_empty=True)

        self._refresh_item_attachment_analysis(item)
        self._sync_claim_from_items(claim)
        self._refresh_claim_pre_review_flags(claim, is_application_claim=False)
        self.db.commit()
        self.db.refresh(claim)

        self.audit_service.log_action(
            actor=current_user.name or current_user.username,
            action="expense_claim.item_update",
            resource_type="expense_claim",
            resource_id=claim.id,
            before_json=before_json,
            after_json=self._serialize_claim(claim),
        )

        return claim

    def create_claim_item(
        self,
        *,
        claim_id: str,
        payload: ExpenseClaimItemCreate | None,
        current_user: CurrentUserContext,
        commit: bool = True,
        write_audit: bool = True,
        refresh_pre_review: bool = True,
    ) -> ExpenseClaim | None:
        claim = self.get_claim(claim_id, current_user)
        if claim is None:
            return None

        self._ensure_draft_claim(claim)
        before_json = self._serialize_claim(claim)
        payload = payload or ExpenseClaimItemCreate()

        occurred_at = claim.occurred_at if claim.occurred_at is not None else datetime.now(UTC)
        item_amount = Decimal("0.00")
        if payload.item_amount is not None:
            item_amount = payload.item_amount.quantize(Decimal("0.01"))
            if item_amount < Decimal("0.00"):
                raise ValueError("费用金额不能小于 0。")

        item = ExpenseClaimItem(
            claim_id=claim.id,
            item_date=payload.item_date or occurred_at.date(),
            item_type=self._normalize_optional_text(
                payload.item_type,
                fallback=str(claim.expense_type or "").strip() or "other",
            )
            or "other",
            item_reason=self._normalize_optional_text(payload.item_reason, fallback="") or "",
            item_location=self._normalize_optional_text(payload.item_location, fallback="") or "",
            item_note=self._normalize_optional_text(payload.item_note, allow_empty=True) or "",
            item_amount=item_amount,
            invoice_id=self._normalize_optional_text(payload.invoice_id, allow_empty=True),
        )
        claim.items.append(item)
        self.db.add(item)

        self._sync_claim_from_items(claim)
        if refresh_pre_review:
            self._refresh_claim_pre_review_flags(claim, is_application_claim=False)
        if commit:
            self.db.commit()
            self.db.refresh(claim)
        else:
            self.db.flush()

        if write_audit:
            self.audit_service.log_action(
                actor=current_user.name or current_user.username,
                action="expense_claim.item_create",
                resource_type="expense_claim",
                resource_id=claim.id,
                before_json=before_json,
                after_json=self._serialize_claim(claim),
            )

        return claim

    def delete_claim_item(
        self,
        *,
        claim_id: str,
        item_id: str,
        current_user: CurrentUserContext,
    ) -> dict[str, Any] | None:
        claim, item = self._get_claim_item_or_raise(
            claim_id=claim_id,
            item_id=item_id,
            current_user=current_user,
        )
        if claim is None:
            return None

        self._ensure_draft_claim(claim)
        before_json = self._serialize_claim(claim)
        item_label = str(item.item_reason or "").strip() or self._resolve_expense_type_label(
            item.item_type
        )

        stage_attachment_deletion(
            self.db,
            storage=self._attachment_storage,
            item=item,
        )
        claim.items = [entry for entry in claim.items if entry.id != item.id]
        self.db.delete(item)

        self._sync_claim_from_items(claim)
        self._refresh_claim_pre_review_flags(claim, is_application_claim=False)
        self.db.commit()
        self.db.refresh(claim)

        self.audit_service.log_action(
            actor=current_user.name or current_user.username,
            action="expense_claim.item_delete",
            resource_type="expense_claim",
            resource_id=claim.id,
            before_json=before_json,
            after_json=self._serialize_claim(claim),
        )

        return {
            "message": f"费用明细“{item_label}”已删除。",
            "claim_id": claim.id,
            "item_id": item.id,
        }

    def submit_claim(
        self,
        claim_id: str,
        current_user: CurrentUserContext,
        *,
        correlation_id: str | None = None,
        pre_review_id: str = "",
        pre_review_input_fingerprint: str = "",
        before_commit: Callable[[BusinessEvent], None] | None = None,
    ) -> ExpenseClaim | None:
        claim = self.get_claim(claim_id, current_user)
        if claim is None:
            return None

        self._ensure_draft_claim(claim)
        self._access_policy.backfill_claim_identity_from_current_user(claim, current_user)
        is_application_claim = self._is_expense_application_claim(claim)
        if not is_application_claim:
            self._sync_claim_from_items(claim)
        missing_fields = (
            self._validate_application_claim_for_submission(claim)
            if is_application_claim
            else self._validate_claim_for_submission(claim)
        )
        if missing_fields:
            raise ExpenseClaimSubmissionBlockedError(missing_fields)

        before_json = self._serialize_claim(claim)
        pre_review_flag = self.refresh_claim_pre_review_state(
            claim,
            is_application_claim=is_application_claim,
            tenant_id=current_user.tenant_id,
        )
        if pre_review_flag is None:
            raise RuntimeError("无法生成提交前预审结果。")

        client_review_provided = bool(
            str(pre_review_id or "").strip() or str(pre_review_input_fingerprint or "").strip()
        )
        client_review_matches = pre_review_identity_matches(
            pre_review_flag,
            review_id=pre_review_id,
            input_fingerprint=pre_review_input_fingerprint,
        )
        correlation_id = self._expense_cases.normalize_correlation_id(
            correlation_id or str(pre_review_flag.get("review_id") or "")
        )
        _, pre_review_event = self._record_pre_review_event(
            claim,
            pre_review_flag=pre_review_flag,
            current_user=current_user,
            is_application_claim=is_application_claim,
            correlation_id=correlation_id,
        )
        correlation_id = pre_review_event.correlation_id
        decision = str(pre_review_flag.get("decision") or "")
        review_error_code = (
            "PRE_REVIEW_NEEDS_FIX"
            if decision == "needs_fix"
            else "PRE_REVIEW_CHANGED"
            if client_review_provided and not client_review_matches
            else ""
        )
        if review_error_code:
            self.db.commit()
            self.db.refresh(claim)
            raise ExpenseClaimPreReviewBlockedError(
                pre_review_public_payload(pre_review_flag) or {},
                code=review_error_code,
            )

        try:
            budget_flags = self._reserve_budget_for_submission(
                claim,
                current_user,
                is_application_claim=is_application_claim,
            )
        except BudgetControlError as exc:
            if is_application_claim:
                raise
            budget_flags = list(exc.flags or [])
        if is_application_claim:
            submitted_at = datetime.now(UTC)
            preserved_flags = [
                flag
                for flag in list(claim.risk_flags_json or [])
                if not (
                    isinstance(flag, dict)
                    and str(flag.get("source") or "").strip()
                    in {"submission_review", "attachment_analysis"}
                )
            ]
            platform_review = self.evaluate_platform_risk_rules(
                claim,
                business_stage="expense_application",
                tenant_id=current_user.tenant_id,
            )
            platform_flags = list(platform_review.get("flags") or [])
            submit_flag = with_risk_business_stage(
                {
                    "source": "application_submission",
                    "event_type": "expense_application_submission",
                    "severity": "info",
                    "label": "申请提交",
                    "message": "费用申请已提交至直属领导审批，请等待审核结果。",
                    "previous_status": str(claim.status or "").strip(),
                    "previous_approval_stage": str(claim.approval_stage or "").strip(),
                    "next_status": "submitted",
                    "next_approval_stage": "直属领导审批",
                    "created_at": submitted_at.isoformat(),
                },
                "expense_application",
            )
            claim.status = "submitted"
            claim.approval_stage = "直属领导审批"
            claim.risk_flags_json = self._append_budget_flags(
                [*preserved_flags, submit_flag, *platform_flags],
                budget_flags,
                business_stage="expense_application",
            )
            claim.submitted_at = submitted_at
        else:
            claim.risk_flags_json = self._append_budget_flags(
                claim.risk_flags_json,
                budget_flags,
                business_stage="reimbursement",
            )

            claim.status = "submitted"
            claim.approval_stage = DIRECT_MANAGER_APPROVAL_STAGE
            claim.submitted_at = datetime.now(UTC)

        claim.risk_flags_json = dedupe_claim_risk_flags(claim.risk_flags_json)

        _, submission_event = self._expense_cases.record_claim_event(
            claim,
            event_type=("application_submitted" if is_application_claim else "claim_submitted"),
            actor_id=current_user.username,
            tenant_id=getattr(current_user, "tenant_id", None),
            correlation_id=correlation_id,
            causation_id=pre_review_event.id if pre_review_event is not None else None,
            idempotency_key=(
                f"submit:{claim.id}:{claim.submitted_at.isoformat()}"
                if claim.submitted_at is not None
                else f"submit:{claim.id}:{before_json.get('status') or 'draft'}"
            ),
            previous_status=str(before_json.get("status") or ""),
            previous_approval_stage=str(before_json.get("approval_stage") or ""),
        )
        from app.services.approval_task_lifecycle import ApprovalTaskLifecycleService

        ApprovalTaskLifecycleService(self.db).ensure_root_task(
            claim,
            tenant_id=current_user.tenant_id,
            entered_at=submission_event.occurred_at,
            entered_at_source="workflow_event",
            business_event=submission_event,
            actor_id="system",
            actor_name="审批任务引擎",
            request_id=f"node-enter:{submission_event.id}",
        )
        if before_commit is not None:
            before_commit(submission_event)

        self.db.commit()
        self.db.refresh(claim)

        self.audit_service.log_action(
            actor=current_user.name or current_user.username,
            action="expense_claim.submit",
            resource_type="expense_claim",
            resource_id=claim.id,
            before_json=before_json,
            after_json=self._serialize_claim(claim),
        )
        if str(claim.status or "").strip().lower() == "submitted":
            self._delete_claim_assistant_sessions(claim.id)

        return claim

    def delete_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None:
        claim = self.get_claim(claim_id, current_user)
        if claim is None and current_user.is_admin:
            candidate_stmt = (
                select(ExpenseClaim)
                .options(
                    selectinload(ExpenseClaim.items),
                    selectinload(ExpenseClaim.employee).selectinload(Employee.manager),
                    selectinload(ExpenseClaim.employee).selectinload(Employee.roles),
                )
                .where(ExpenseClaim.id == claim_id)
            )
            candidate_claim = self.db.scalar(
                self._access_policy.apply_tenant_scope(candidate_stmt, current_user)
            )
            if candidate_claim is not None:
                claim = candidate_claim
        if claim is None:
            return None

        if self._access_policy.is_archived_claim(claim) and not current_user.is_admin:
            raise ValueError("已归档单据不能删除，只有高级管理员可以执行删除。")

        if not self._access_policy.has_claim_delete_access(current_user):
            self._ensure_draft_claim(claim)
            if not self._access_policy.is_claim_owned_by_current_user(claim, current_user):
                raise ValueError(
                    "只有系统管理员或草稿、待补充、退回待提交阶段的申请人本人可以删除单据。"
                )

        before_json = self._serialize_claim(claim)
        resource_id = claim.id
        operator = self._access_policy.resolve_current_user_display_name(current_user)
        if not self._is_expense_application_claim(claim):
            self._sync_linked_applications_after_reimbursement_deleted(
                reimbursement_claim=claim,
                operator=operator,
                current_user=current_user,
            )

        self._release_budget_for_delete(claim, current_user)
        self._delete_claim_analysis_records(resource_id)
        stage_claim_attachment_deletion(
            self.db,
            storage=self._attachment_storage,
            claim_id=claim.id,
        )
        ReceiptFolderService().unlink_receipts_for_claim(resource_id)
        self.db.delete(claim)
        self.db.commit()

        self.audit_service.log_action(
            actor=current_user.name or current_user.username,
            action="expense_claim.delete",
            resource_type="expense_claim",
            resource_id=resource_id,
            before_json=before_json,
            after_json=None,
        )
        self._delete_claim_assistant_sessions(resource_id)

        return claim

    def _delete_claim_analysis_records(self, claim_id: str) -> None:
        observation_ids = select(RiskObservation.id).where(RiskObservation.claim_id == claim_id)
        self.db.execute(
            delete(RiskObservationFeedback).where(
                RiskObservationFeedback.observation_id.in_(observation_ids)
            )
        )
        self.db.execute(delete(RiskObservation).where(RiskObservation.claim_id == claim_id))
        self.db.execute(delete(HermesRiskReport).where(HermesRiskReport.claim_id == claim_id))


class ExpenseClaimService(
    ExpenseClaimActionProtocolMixin,
    ExpenseClaimReturnFlowMixin,
    ExpenseClaimStandardAdjustmentMixin,
    ExpenseClaimItemActionMixin,
    ExpenseClaimPaginationMixin,
    ExpenseClaimApprovalFlowMixin,
    ExpenseClaimApprovalRoutingMixin,
    ExpenseClaimApplicationHandoffMixin,
    ExpenseClaimPreReviewMixin,
    ExpenseClaimBudgetFlowMixin,
    ExpenseClaimAttachmentOperationsMixin,
    ExpenseClaimReviewPreviewMixin,
    ExpenseClaimDraftFlowMixin,
    ExpenseClaimDraftPersistenceMixin,
    ExpenseClaimDocumentItemBuilderMixin,
    ExpenseClaimDocumentParsingMixin,
    ExpenseClaimOntologyResolverMixin,
    ExpenseClaimAttachmentDocumentMixin,
    ExpenseClaimAttachmentAnalysisMixin,
    ExpenseClaimReadModelMixin,
    ExpenseClaimRiskReviewMixin,
    ExpenseClaimWorkflowRepairMixin,
):
    def __init__(self, db: Session) -> None:
        self.db = db
        self.audit_service = AuditLogService(db)
        self._expense_cases = ExpenseCaseService(db)
        self._access_policy = ExpenseClaimAccessPolicy(db)
        self._attachment_storage = ExpenseClaimAttachmentStorage()
        self._attachment_presentation = ExpenseClaimAttachmentPresentation(self._attachment_storage)

    @staticmethod
    def _is_expense_application_claim(claim: ExpenseClaim) -> bool:
        claim_no = str(getattr(claim, "claim_no", "") or "").strip().upper()
        expense_type = str(getattr(claim, "expense_type", "") or "").strip().lower()
        document_type = (
            str(
                getattr(claim, "document_type_code", "")
                or getattr(claim, "document_type", "")
                or ""
            )
            .strip()
            .lower()
        )
        return (
            is_application_claim_no(claim_no)
            or expense_type == "application"
            or expense_type.endswith("_application")
            or document_type in {"application", "expense_application"}
        )

    def _validate_application_claim_for_submission(self, claim: ExpenseClaim) -> list[str]:
        issues: list[str] = []
        if self._is_missing_value(claim.employee_name):
            issues.append("申请人未完善")
        if self._is_missing_value(claim.department_name):
            issues.append("所属部门未完善")
        if self._is_missing_value(claim.expense_type):
            issues.append("申请类型未完善")
        if self._is_missing_value(claim.reason):
            issues.append("申请事由未完善")
        if self._is_missing_value(claim.location):
            issues.append("业务地点未完善")
        if claim.amount is None or claim.amount <= Decimal("0.00"):
            issues.append("预计总费用未完善")
        if claim.occurred_at is None:
            issues.append("申请时间未完善")
        return issues

    def list_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]:
        stmt = (
            select(ExpenseClaim)
            .options(
                selectinload(ExpenseClaim.items),
                selectinload(ExpenseClaim.employee).selectinload(Employee.manager),
                selectinload(ExpenseClaim.employee).selectinload(Employee.organization_unit),
                selectinload(ExpenseClaim.employee).selectinload(Employee.roles),
            )
            .order_by(ExpenseClaim.created_at.desc(), ExpenseClaim.occurred_at.desc())
        )
        stmt = self._access_policy.apply_claim_scope(stmt, current_user)
        claims = list(self.db.scalars(stmt).all())
        return self._access_policy.attach_budget_approval_snapshots(claims)

    def list_approval_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]:
        stmt = (
            select(ExpenseClaim)
            .options(
                selectinload(ExpenseClaim.items),
                selectinload(ExpenseClaim.employee).selectinload(Employee.manager),
                selectinload(ExpenseClaim.employee).selectinload(Employee.organization_unit),
                selectinload(ExpenseClaim.employee).selectinload(Employee.roles),
            )
            .order_by(ExpenseClaim.submitted_at.desc(), ExpenseClaim.created_at.desc())
        )
        stmt = self._access_policy.apply_approval_claim_scope(stmt, current_user)
        claims = list(self.db.scalars(stmt).all())
        return self._access_policy.attach_budget_approval_snapshots(claims)

    def list_archived_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]:
        stmt = (
            select(ExpenseClaim)
            .options(
                selectinload(ExpenseClaim.items),
                selectinload(ExpenseClaim.employee).selectinload(Employee.manager),
                selectinload(ExpenseClaim.employee).selectinload(Employee.organization_unit),
                selectinload(ExpenseClaim.employee).selectinload(Employee.roles),
            )
            .order_by(
                ExpenseClaim.updated_at.desc(),
                ExpenseClaim.submitted_at.desc(),
                ExpenseClaim.created_at.desc(),
            )
        )
        stmt = self._access_policy.apply_archived_claim_scope(stmt, current_user)
        return list(self.db.scalars(stmt).all())

    def get_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None:
        stmt = (
            select(ExpenseClaim)
            .options(
                selectinload(ExpenseClaim.items),
                selectinload(ExpenseClaim.employee).selectinload(Employee.manager),
                selectinload(ExpenseClaim.employee).selectinload(Employee.organization_unit),
                selectinload(ExpenseClaim.employee).selectinload(Employee.roles),
            )
            .where(ExpenseClaim.id == claim_id)
        )
        stmt = self._access_policy.apply_claim_scope(
            stmt, current_user, include_approval_scope=True
        )
        claim = self.db.scalar(stmt)
        return self._access_policy.attach_approval_snapshot(claim)

    def can_view_budget_analysis(
        self, current_user: CurrentUserContext, claim: ExpenseClaim | None = None
    ) -> bool:
        if claim is None:
            return self._access_policy.is_budget_manager_user(current_user)
        if current_user.is_admin:
            return True
        role_codes = self._access_policy.normalize_role_codes(current_user)
        if "executive" in role_codes:
            return True
        if self._access_policy.has_privileged_claim_access(
            current_user
        ) and not self._access_policy.is_claim_owned_by_current_user(claim, current_user):
            return True
        if self._access_policy.can_approve_claim(current_user, claim):
            return True
        if self._access_policy.is_claim_owned_by_current_user(claim, current_user):
            return False
        return self._access_policy.is_department_p8_budget_monitor(current_user, claim)

    def update_claim(
        self,
        *,
        claim_id: str,
        payload: ExpenseClaimUpdate,
        current_user: CurrentUserContext,
    ) -> ExpenseClaim | None:
        claim = self.get_claim(claim_id, current_user)
        if claim is None:
            return None

        self._ensure_draft_pending_claim(claim)
        before_json = self._serialize_claim(claim)

        if payload.reason is not None:
            claim.reason = (
                self._normalize_optional_text(payload.reason, allow_empty=True) or "待补充"
            )

        if not self._is_expense_application_claim(claim):
            self._refresh_claim_pre_review_flags(claim, is_application_claim=False)

        self.db.commit()
        self.db.refresh(claim)

        self.audit_service.log_action(
            actor=current_user.name or current_user.username,
            action="expense_claim.update",
            resource_type="expense_claim",
            resource_id=claim.id,
            before_json=before_json,
            after_json=self._serialize_claim(claim),
        )

        return claim
