from __future__ import annotations

from datetime import UTC, datetime
from decimal import Decimal
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.db.schema_ownership import create_legacy_schema
from app.models.budget import BudgetAllocation, BudgetReservation
from app.models.financial_record import ExpenseClaim
from app.schemas.budget import (
    BudgetAllocationCreate,
    BudgetAllocationRead,
    BudgetCheckRead,
    BudgetCheckRequest,
    BudgetOperationRead,
    BudgetOperationRequest,
    BudgetSummaryRead,
    BudgetTransactionRead,
)
from app.services.budget_expense_control import BudgetExpenseControlModel
from app.services.budget_pagination import BudgetPaginationMixin
from app.services.budget_support import BudgetSupportMixin
from app.services.budget_types import BudgetControlError


class BudgetService(BudgetPaginationMixin, BudgetSupportMixin):
    def __init__(self, db: Session) -> None:
        self.db = db

    def ensure_budget_ready(self) -> None:
        # 复用当前 Session 连接，避免在业务事务中通过 Engine 隐式提交已 flush 的单据。
        create_legacy_schema(self.db.connection())
        exists = self.db.scalar(select(BudgetAllocation.id).limit(1))
        if exists:
            return
        self._seed_default_allocations()

    def list_allocations(
        self,
        *,
        fiscal_year: int | None = None,
        period_key: str | None = None,
        department_id: str | None = None,
        department_name: str | None = None,
        cost_center: str | None = None,
    ) -> list[BudgetAllocationRead]:
        self.ensure_budget_ready()
        stmt = self.build_allocation_stmt(
            fiscal_year=fiscal_year,
            period_key=period_key,
            department_id=department_id,
            department_name=department_name,
            cost_center=cost_center,
        )
        return [self.serialize_allocation(row) for row in self.db.scalars(stmt).all()]

    def get_summary(
        self,
        *,
        fiscal_year: int | None = None,
        period_key: str | None = None,
        department_id: str | None = None,
        department_name: str | None = None,
        cost_center: str | None = None,
    ) -> BudgetSummaryRead:
        allocations = self.list_allocations(
            fiscal_year=fiscal_year,
            period_key=period_key,
            department_id=department_id,
            department_name=department_name,
            cost_center=cost_center,
        )
        total_amount = sum((item.balance.total_amount for item in allocations), Decimal("0.00"))
        reserved_amount = sum((item.balance.reserved_amount for item in allocations), Decimal("0.00"))
        consumed_amount = sum((item.balance.consumed_amount for item in allocations), Decimal("0.00"))
        available_amount = sum((item.balance.available_amount for item in allocations), Decimal("0.00"))
        warning_count = sum(
            1
            for item in allocations
            if item.balance.usage_rate >= item.warning_threshold
            and item.balance.available_amount >= Decimal("0.00")
        )
        over_budget_count = sum(
            1 for item in allocations if item.balance.available_amount < Decimal("0.00")
        )
        warnings = self.build_summary_warnings(allocations)
        return BudgetSummaryRead(
            fiscal_year=fiscal_year,
            period_key=period_key,
            total_amount=total_amount,
            reserved_amount=reserved_amount,
            consumed_amount=consumed_amount,
            available_amount=available_amount,
            warning_count=warning_count,
            over_budget_count=over_budget_count,
            allocations=allocations,
            trend=self.build_summary_trend(
                fiscal_year=fiscal_year,
                department_id=department_id,
                department_name=department_name,
                cost_center=cost_center,
            ),
            warnings=warnings,
        )

    def analyze_claim_budget(self, claim: ExpenseClaim) -> dict[str, Any]:
        return BudgetExpenseControlModel().assess(self.build_claim_budget_context(claim), claim)

    def create_or_update_allocation(
        self,
        payload: BudgetAllocationCreate,
        *,
        operator: str,
    ) -> BudgetAllocationRead:
        self.ensure_budget_ready()
        subject_code = self._normalize_subject_code(payload.subject_code)
        if not self._is_supported_budget_subject(subject_code):
            raise ValueError("demo 阶段预算中心只维护差旅、通信、招待费、办公用品四类预算。")
        period_key = self._normalize_period_key(payload.fiscal_year, payload.period_key)
        existing = self._find_exact_allocation(
            fiscal_year=payload.fiscal_year,
            period_key=period_key,
            department_id=payload.department_id,
            department_name=payload.department_name,
            cost_center=payload.cost_center,
            project_code=payload.project_code,
            subject_code=subject_code,
        )
        if existing is None:
            allocation = BudgetAllocation(
                budget_no=self._make_no("BUD"),
                fiscal_year=payload.fiscal_year,
                period_type=self._normalize_period_type(payload.period_type),
                period_key=period_key,
                department_id=self._blank_to_none(payload.department_id),
                department_name=payload.department_name.strip(),
                cost_center=self._blank_to_none(payload.cost_center),
                project_code=self._blank_to_none(payload.project_code),
                subject_code=subject_code,
                subject_name=payload.subject_name.strip(),
                original_amount=self._money(payload.original_amount),
                adjusted_amount=Decimal("0.00"),
                status="active",
                warning_threshold=self._percent(payload.warning_threshold),
                control_action=self._normalize_control_action(payload.control_action),
                description=self._blank_to_none(payload.description),
                created_by=operator,
                updated_by=operator,
            )
            self.db.add(allocation)
            self.db.flush()
            self._record_transaction(
                allocation=allocation,
                transaction_type="init",
                amount=allocation.original_amount,
                before_available=Decimal("0.00"),
                after_available=self.get_balance(allocation).available_amount,
                source_type="budget_allocation",
                source_id=allocation.id,
                source_no=allocation.budget_no,
                operator=operator,
                reason="初始化预算额度",
            )
            self.db.flush()
            return self.serialize_allocation(allocation)

        before_balance = self.get_balance(existing)
        original_before = self._money(existing.original_amount)
        existing.period_type = self._normalize_period_type(payload.period_type)
        existing.department_name = payload.department_name.strip()
        existing.cost_center = self._blank_to_none(payload.cost_center)
        existing.project_code = self._blank_to_none(payload.project_code)
        existing.subject_name = payload.subject_name.strip()
        existing.original_amount = self._money(payload.original_amount)
        existing.warning_threshold = self._percent(payload.warning_threshold)
        existing.control_action = self._normalize_control_action(payload.control_action)
        existing.description = self._blank_to_none(payload.description)
        existing.updated_by = operator
        self.db.flush()
        amount_delta = self._money(existing.original_amount) - original_before
        if amount_delta:
            self._record_transaction(
                allocation=existing,
                transaction_type="adjust",
                amount=amount_delta,
                before_available=before_balance.available_amount,
                after_available=self.get_balance(existing).available_amount,
                source_type="budget_allocation",
                source_id=existing.id,
                source_no=existing.budget_no,
                operator=operator,
                reason="调整预算额度",
            )
            self.db.flush()
        return self.serialize_allocation(existing)

    def check(self, payload: BudgetCheckRequest) -> BudgetCheckRead:
        self.ensure_budget_ready()
        subject_code = self._normalize_subject_code(payload.subject_code)
        if not self._is_supported_budget_subject(subject_code):
            flag = self._build_budget_flag(
                event_type="budget_control_skipped",
                severity="low",
                label="预算暂不管控",
                message="demo 阶段该费用类型暂不纳入预算计算。",
                amount=self._money(payload.amount),
                extra={"subject_code": subject_code},
            )
            return BudgetCheckRead(passed=True, flags=[flag])
        allocation = self._find_allocation_for_dimension(
            fiscal_year=payload.fiscal_year,
            period_key=payload.period_key,
            department_id=payload.department_id,
            department_name=payload.department_name,
            cost_center=payload.cost_center,
            project_code=payload.project_code,
            subject_code=subject_code,
        )
        amount = self._money(payload.amount)
        if allocation is None:
            flag = self._build_budget_flag(
                event_type="budget_missing",
                severity="high",
                label="预算归属缺失",
                message="未找到匹配的预算额度，当前单据不能进入费用控制闭环。",
                amount=amount,
            )
            return BudgetCheckRead(passed=False, blocking_reasons=[flag["message"]], flags=[flag])

        review = self._review_allocation_amount(allocation, amount)
        return BudgetCheckRead(
            passed=not review["blocking_reasons"],
            blocking_reasons=review["blocking_reasons"],
            flags=review["flags"],
            allocation=self.serialize_allocation(allocation),
        )

    def review_claim_budget(self, claim: ExpenseClaim) -> dict[str, list[Any]]:
        self.ensure_budget_ready()
        if not self._claim_uses_budget_control(claim):
            return {"flags": [], "blocking_reasons": []}
        allocation = self._find_allocation_for_claim(claim)
        amount = self._money(claim.amount or Decimal("0.00"))
        if allocation is None:
            if self._budget_table_empty():
                allocation = self._create_fallback_allocation_for_claim(claim)
            else:
                flag = self._build_budget_flag(
                    event_type="budget_missing",
                    severity="high",
                    label="预算归属缺失",
                    message=f"单据 {claim.claim_no} 未找到预算池额度，请先在预算中心建立预算。",
                    amount=amount,
                )
                return {"flags": [flag], "blocking_reasons": [flag["message"]]}
        review = self._review_allocation_amount(allocation, amount)
        return {"flags": review["flags"], "blocking_reasons": review["blocking_reasons"]}

    def reserve_for_claim(
        self,
        claim: ExpenseClaim,
        *,
        source_type: str,
        operator: str,
    ) -> list[dict[str, Any]]:
        self.ensure_budget_ready()
        if not self._claim_uses_budget_control(claim):
            return []
        existing = self._find_active_reservation(source_type=source_type, source_id=claim.id)
        if existing is not None:
            return [
                self._build_operation_flag(
                    existing.allocation,
                    event_type="budget_reserve_reused",
                    label="预算预占已存在",
                    message=f"单据 {claim.claim_no} 已存在有效预算预占，本次提交复用原预占。",
                    amount=existing.amount,
                    reservation_id=existing.id,
                )
            ]

        allocation = self._find_allocation_for_claim(claim)
        if allocation is None and self._budget_table_empty():
            allocation = self._create_fallback_allocation_for_claim(claim)
        if allocation is None:
            raise BudgetControlError([f"单据 {claim.claim_no} 未找到预算池额度，请先建立预算。"])

        amount = self._money(claim.amount or Decimal("0.00"))
        review = self._review_allocation_amount(allocation, amount)
        if review["blocking_reasons"]:
            raise BudgetControlError(review["blocking_reasons"], flags=review["flags"])

        before_balance = self.get_balance(allocation)
        reservation = BudgetReservation(
            reservation_no=self._make_no("BRS"),
            allocation_id=allocation.id,
            source_type=source_type,
            source_id=claim.id,
            source_no=claim.claim_no,
            source_status="active",
            amount=amount,
            context_json=self._claim_context(claim),
        )
        self.db.add(reservation)
        self.db.flush()
        after_balance = self.get_balance(allocation)
        transaction = self._record_transaction(
            allocation=allocation,
            reservation=reservation,
            transaction_type="reserve",
            amount=amount,
            before_available=before_balance.available_amount,
            after_available=after_balance.available_amount,
            source_type=source_type,
            source_id=claim.id,
            source_no=claim.claim_no,
            operator=operator,
            reason="单据提交预算预占",
        )
        self.db.flush()
        flag = self._build_operation_flag(
            allocation,
            event_type="budget_reserved",
            label="预算已预占",
            message=f"已为单据 {claim.claim_no} 预占预算 {amount} 元。",
            amount=amount,
            reservation_id=reservation.id,
            transaction_id=transaction.id,
        )
        return [*review["flags"], flag]

    def release_for_claim(
        self,
        claim: ExpenseClaim,
        *,
        source_type: str,
        operator: str,
        reason: str,
    ) -> list[dict[str, Any]]:
        self.ensure_budget_ready()
        reservations = self._find_active_reservations(source_type=source_type, source_id=claim.id)
        flags: list[dict[str, Any]] = []
        for reservation in reservations:
            allocation = reservation.allocation
            before_balance = self.get_balance(allocation)
            amount = self._money(reservation.amount)
            reservation.source_status = "released"
            reservation.released_amount = amount
            reservation.released_at = datetime.now(UTC)
            self.db.flush()
            after_balance = self.get_balance(allocation)
            transaction = self._record_transaction(
                allocation=allocation,
                reservation=reservation,
                transaction_type="release",
                amount=amount,
                before_available=before_balance.available_amount,
                after_available=after_balance.available_amount,
                source_type=source_type,
                source_id=claim.id,
                source_no=claim.claim_no,
                operator=operator,
                reason=reason,
            )
            flags.append(
                self._build_operation_flag(
                    allocation,
                    event_type="budget_released",
                    label="预算预占已释放",
                    message=f"单据 {claim.claim_no} 已释放预算预占 {amount} 元。",
                    amount=amount,
                    reservation_id=reservation.id,
                    transaction_id=transaction.id,
                )
            )
        self.db.flush()
        return flags

    def consume_for_claim(
        self,
        claim: ExpenseClaim,
        *,
        operator: str,
        reason: str,
    ) -> dict[str, Any] | None:
        self.ensure_budget_ready()
        if not self._claim_uses_budget_control(claim):
            return None
        reservations = self._find_active_reservations(source_type="claim", source_id=claim.id)
        amount = self._money(claim.amount or Decimal("0.00"))
        if reservations:
            reservation = reservations[0]
            allocation = reservation.allocation
            before_balance = self.get_balance(allocation)
            reserved_amount = self._money(reservation.amount)
            consume_amount = min(amount, reserved_amount)
            release_amount = max(reserved_amount - consume_amount, Decimal("0.00"))
            reservation.source_status = "consumed"
            reservation.consumed_amount = consume_amount
            reservation.released_amount = release_amount
            reservation.consumed_at = datetime.now(UTC)
            self.db.flush()
            after_balance = self.get_balance(allocation)
            transaction = self._record_transaction(
                allocation=allocation,
                reservation=reservation,
                transaction_type="consume",
                amount=consume_amount,
                before_available=before_balance.available_amount,
                after_available=after_balance.available_amount,
                source_type="claim",
                source_id=claim.id,
                source_no=claim.claim_no,
                operator=operator,
                reason=reason,
            )
            self.db.flush()
            return self._build_operation_flag(
                allocation,
                event_type="budget_consumed",
                label="预算已核销",
                message=f"单据 {claim.claim_no} 已核销预算 {consume_amount} 元。",
                amount=consume_amount,
                reservation_id=reservation.id,
                transaction_id=transaction.id,
            )

        allocation = self._find_allocation_for_claim(claim)
        if allocation is None:
            return self._build_budget_flag(
                event_type="budget_consume_skipped",
                severity="low",
                label="预算未核销",
                message=f"单据 {claim.claim_no} 未找到预算池额度，按存量单据兼容放行。",
                amount=amount,
            )
        review = self._review_allocation_amount(allocation, amount)
        if review["blocking_reasons"]:
            raise BudgetControlError(review["blocking_reasons"], flags=review["flags"])
        before_balance = self.get_balance(allocation)
        transaction = self._record_transaction(
            allocation=allocation,
            transaction_type="consume",
            amount=amount,
            before_available=before_balance.available_amount,
            after_available=before_balance.available_amount - amount,
            source_type="claim",
            source_id=claim.id,
            source_no=claim.claim_no,
            operator=operator,
            reason=reason,
        )
        self.db.flush()
        return self._build_operation_flag(
            allocation,
            event_type="budget_consumed",
            label="预算已核销",
            message=f"单据 {claim.claim_no} 已核销预算 {amount} 元。",
            amount=amount,
            transaction_id=transaction.id,
        )

    def transfer_application_reservation(
        self,
        *,
        application_claim: ExpenseClaim,
        draft_claim: ExpenseClaim,
        operator: str,
    ) -> dict[str, Any] | None:
        self.ensure_budget_ready()
        reservation = self._find_active_reservation(
            source_type="application",
            source_id=application_claim.id,
        )
        if reservation is None:
            return None
        allocation = reservation.allocation
        before_balance = self.get_balance(allocation)
        reservation.source_type = "claim"
        reservation.source_id = draft_claim.id
        reservation.source_no = draft_claim.claim_no
        context = dict(reservation.context_json or {})
        context["application_claim_id"] = application_claim.id
        context["application_claim_no"] = application_claim.claim_no
        context["draft_claim_id"] = draft_claim.id
        context["draft_claim_no"] = draft_claim.claim_no
        reservation.context_json = context
        self.db.flush()
        transaction = self._record_transaction(
            allocation=allocation,
            reservation=reservation,
            transaction_type="transfer",
            amount=Decimal("0.00"),
            before_available=before_balance.available_amount,
            after_available=self.get_balance(allocation).available_amount,
            source_type="claim",
            source_id=draft_claim.id,
            source_no=draft_claim.claim_no,
            operator=operator,
            reason="申请审批通过后预算预占转入报销草稿",
        )
        self.db.flush()
        return self._build_operation_flag(
            allocation,
            event_type="budget_reservation_transferred",
            label="预算预占已转入报销",
            message=f"申请 {application_claim.claim_no} 的预算预占已转入报销草稿 {draft_claim.claim_no}。",
            amount=reservation.amount,
            reservation_id=reservation.id,
            transaction_id=transaction.id,
        )

    def execute_operation(
        self,
        payload: BudgetOperationRequest,
        *,
        transaction_type: str,
        operator: str,
    ) -> BudgetOperationRead:
        self.ensure_budget_ready()
        normalized_type = str(transaction_type or "").strip().lower()
        subject_code = self._normalize_subject_code(payload.subject_code)
        if not self._is_supported_budget_subject(subject_code):
            raise BudgetControlError(["demo 阶段该费用类型暂不纳入预算计算。"])
        allocation = self._find_allocation_for_dimension(
            fiscal_year=payload.fiscal_year,
            period_key=payload.period_key,
            department_id=payload.department_id,
            department_name=payload.department_name,
            cost_center=payload.cost_center,
            project_code=payload.project_code,
            subject_code=subject_code,
        )
        if allocation is None:
            raise BudgetControlError(["未找到匹配的预算额度。"])
        amount = self._money(payload.amount)

        if normalized_type == "reserve":
            return self._execute_manual_reserve_operation(payload, allocation, amount, operator)
        if normalized_type == "release":
            return self._execute_manual_release_operation(payload, operator)
        if normalized_type == "consume":
            return self._execute_manual_consume_operation(payload, allocation, amount, operator)

        before_balance = self.get_balance(allocation)
        transaction = self._record_transaction(
            allocation=allocation,
            transaction_type=normalized_type,
            amount=amount,
            before_available=before_balance.available_amount,
            after_available=before_balance.available_amount - amount,
            source_type=payload.source_type,
            source_id=payload.source_id,
            source_no=payload.source_no,
            operator=operator,
            reason=payload.reason,
        )
        self.db.flush()
        return BudgetOperationRead(
            ok=True,
            message="预算操作已记录。",
            allocation=self.serialize_allocation(allocation),
            transaction=BudgetTransactionRead.model_validate(transaction),
        )

    def _execute_manual_reserve_operation(
        self,
        payload: BudgetOperationRequest,
        allocation: BudgetAllocation,
        amount: Decimal,
        operator: str,
    ) -> BudgetOperationRead:
        existing = self._find_active_reservation(
            source_type=payload.source_type,
            source_id=payload.source_id,
        )
        if existing is not None:
            flag = self._build_operation_flag(
                existing.allocation,
                event_type="budget_reserve_reused",
                label="预算预占已存在",
                message=f"来源 {payload.source_no} 已存在有效预算预占。",
                amount=existing.amount,
                reservation_id=existing.id,
            )
            return BudgetOperationRead(
                ok=True,
                message="预算预占已存在。",
                reservation_id=existing.id,
                allocation=self.serialize_allocation(existing.allocation),
                flags=[flag],
            )

        review = self._review_allocation_amount(allocation, amount)
        if review["blocking_reasons"]:
            raise BudgetControlError(review["blocking_reasons"], flags=review["flags"])

        before_balance = self.get_balance(allocation)
        reservation = BudgetReservation(
            reservation_no=self._make_no("BRS"),
            allocation_id=allocation.id,
            source_type=payload.source_type,
            source_id=payload.source_id,
            source_no=payload.source_no,
            source_status="active",
            amount=amount,
            context_json={"manual_operation": True},
        )
        self.db.add(reservation)
        self.db.flush()
        after_balance = self.get_balance(allocation)
        transaction = self._record_transaction(
            allocation=allocation,
            reservation=reservation,
            transaction_type="reserve",
            amount=amount,
            before_available=before_balance.available_amount,
            after_available=after_balance.available_amount,
            source_type=payload.source_type,
            source_id=payload.source_id,
            source_no=payload.source_no,
            operator=operator,
            reason=payload.reason,
        )
        self.db.flush()
        flag = self._build_operation_flag(
            allocation,
            event_type="budget_reserved",
            label="预算已预占",
            message=f"来源 {payload.source_no} 已预占预算 {amount} 元。",
            amount=amount,
            reservation_id=reservation.id,
            transaction_id=transaction.id,
        )
        return BudgetOperationRead(
            ok=True,
            message="预算预占已记录。",
            reservation_id=reservation.id,
            allocation=self.serialize_allocation(allocation),
            transaction=BudgetTransactionRead.model_validate(transaction),
            flags=[*review["flags"], flag],
        )

    def _execute_manual_release_operation(
        self,
        payload: BudgetOperationRequest,
        operator: str,
    ) -> BudgetOperationRead:
        reservation = self._find_active_reservation(
            source_type=payload.source_type,
            source_id=payload.source_id,
        )
        if reservation is None:
            raise BudgetControlError(["未找到可释放的预算预占。"])

        allocation = reservation.allocation
        before_balance = self.get_balance(allocation)
        amount = self._money(reservation.amount)
        reservation.source_status = "released"
        reservation.released_amount = amount
        reservation.released_at = datetime.now(UTC)
        self.db.flush()
        after_balance = self.get_balance(allocation)
        transaction = self._record_transaction(
            allocation=allocation,
            reservation=reservation,
            transaction_type="release",
            amount=amount,
            before_available=before_balance.available_amount,
            after_available=after_balance.available_amount,
            source_type=payload.source_type,
            source_id=payload.source_id,
            source_no=payload.source_no,
            operator=operator,
            reason=payload.reason,
        )
        self.db.flush()
        flag = self._build_operation_flag(
            allocation,
            event_type="budget_released",
            label="预算预占已释放",
            message=f"来源 {payload.source_no} 已释放预算预占 {amount} 元。",
            amount=amount,
            reservation_id=reservation.id,
            transaction_id=transaction.id,
        )
        return BudgetOperationRead(
            ok=True,
            message="预算释放已记录。",
            reservation_id=reservation.id,
            allocation=self.serialize_allocation(allocation),
            transaction=BudgetTransactionRead.model_validate(transaction),
            flags=[flag],
        )

    def _execute_manual_consume_operation(
        self,
        payload: BudgetOperationRequest,
        allocation: BudgetAllocation,
        amount: Decimal,
        operator: str,
    ) -> BudgetOperationRead:
        reservation = self._find_active_reservation(
            source_type=payload.source_type,
            source_id=payload.source_id,
        )
        if reservation is None:
            return self._record_direct_operation(payload, allocation, "consume", amount, operator)

        before_balance = self.get_balance(reservation.allocation)
        reserved_amount = self._money(reservation.amount)
        consume_amount = min(amount, reserved_amount)
        reservation.source_status = "consumed"
        reservation.consumed_amount = consume_amount
        reservation.released_amount = max(reserved_amount - consume_amount, Decimal("0.00"))
        reservation.consumed_at = datetime.now(UTC)
        self.db.flush()
        after_balance = self.get_balance(reservation.allocation)
        transaction = self._record_transaction(
            allocation=reservation.allocation,
            reservation=reservation,
            transaction_type="consume",
            amount=consume_amount,
            before_available=before_balance.available_amount,
            after_available=after_balance.available_amount,
            source_type=payload.source_type,
            source_id=payload.source_id,
            source_no=payload.source_no,
            operator=operator,
            reason=payload.reason,
        )
        self.db.flush()
        flag = self._build_operation_flag(
            reservation.allocation,
            event_type="budget_consumed",
            label="预算已核销",
            message=f"来源 {payload.source_no} 已核销预算 {consume_amount} 元。",
            amount=consume_amount,
            reservation_id=reservation.id,
            transaction_id=transaction.id,
        )
        return BudgetOperationRead(
            ok=True,
            message="预算核销已记录。",
            reservation_id=reservation.id,
            allocation=self.serialize_allocation(reservation.allocation),
            transaction=BudgetTransactionRead.model_validate(transaction),
            flags=[flag],
        )

    def _record_direct_operation(
        self,
        payload: BudgetOperationRequest,
        allocation: BudgetAllocation,
        transaction_type: str,
        amount: Decimal,
        operator: str,
    ) -> BudgetOperationRead:
        before_balance = self.get_balance(allocation)
        transaction = self._record_transaction(
            allocation=allocation,
            transaction_type=transaction_type,
            amount=amount,
            before_available=before_balance.available_amount,
            after_available=before_balance.available_amount - amount,
            source_type=payload.source_type,
            source_id=payload.source_id,
            source_no=payload.source_no,
            operator=operator,
            reason=payload.reason,
        )
        self.db.flush()
        return BudgetOperationRead(
            ok=True,
            message="预算操作已记录。",
            allocation=self.serialize_allocation(allocation),
            transaction=BudgetTransactionRead.model_validate(transaction),
        )
