from __future__ import annotations

import uuid
from datetime import UTC, datetime

from app.api.deps import CurrentUserContext
from app.models.approval_action import ApprovalActionLedger
from app.models.financial_record import ExpenseClaim
from app.schemas.reimbursement import ExpenseClaimRead
from app.services.expense_claim_constants import RETURN_REASON_OPTIONS


class ExpenseClaimReturnFlowMixin:
    def return_claim(
        self,
        claim_id: str,
        current_user: CurrentUserContext,
        *,
        reason: str | None = None,
        reason_codes: list[str] | None = None,
        request_id: str | None = None,
        expected_status: str | None = None,
        expected_approval_stage: str | None = None,
        task_id: str | None = None,
        expected_task_version: int | None = None,
    ) -> ExpenseClaim | ExpenseClaimRead | None:
        normalized_reason = str(reason or "").strip()
        reason_code_payload = self._normalize_return_reason_code_payload(reason_codes)
        normalized_codes = [
            *reason_code_payload["reason_codes"],
            *reason_code_payload["unknown_reason_codes"],
        ]
        return self._execute_claim_action(
            action="return",
            claim_id=claim_id,
            current_user=current_user,
            request_id=request_id,
            expected_status=expected_status,
            expected_approval_stage=expected_approval_stage,
            payload={
                "reason": normalized_reason,
                "reason_codes": normalized_codes,
                "task_id": str(task_id or "").strip() or None,
                "expected_task_version": expected_task_version,
            },
            executor=lambda claim, ledger, normalized_request_id: self._return_claim_once(
                claim,
                current_user,
                reason=normalized_reason,
                reason_codes=normalized_codes,
                ledger=ledger,
                request_id=normalized_request_id,
            ),
            task_id=task_id,
            expected_task_version=expected_task_version,
        )

    def _return_claim_once(
        self,
        claim: ExpenseClaim,
        current_user: CurrentUserContext,
        *,
        reason: str,
        reason_codes: list[str],
        ledger: ApprovalActionLedger,
        request_id: str,
    ) -> ExpenseClaim:
        normalized_status = str(claim.status or "").strip().lower()
        if normalized_status == "draft":
            raise ValueError("草稿状态无需退回。")
        if normalized_status == "returned":
            raise ValueError("该单据已处于退回待提交状态，无需重复退回。")
        if normalized_status in {"approved", "completed", "paid"}:
            raise ValueError("已完成单据不允许退回。")

        if not getattr(claim, "_approval_task_authorized", False) and not (
            self._access_policy.can_return_claim(current_user, claim)
        ):
            raise ValueError("只有财务人员、高级财务人员或当前审批人可以退回报销单。")

        before_json = self._serialize_claim(claim)
        operator = self._access_policy.resolve_current_user_display_name(current_user)
        previous_status = str(claim.status or "").strip()
        previous_stage = str(claim.approval_stage or "").strip() or "未标记审批环节"
        previous_stage_key = self._normalize_return_stage_key(previous_stage)
        is_application_claim = self._is_expense_application_claim(claim)
        is_direct_manager_return = previous_stage_key == "direct_manager"
        is_budget_return = previous_stage_key == "budget"
        is_application_return = is_application_claim and (
            is_direct_manager_return or is_budget_return
        )
        return_event_type = (
            "expense_application_return" if is_application_return else "expense_claim_return"
        )
        return_label = (
            "领导退回"
            if is_application_claim and is_direct_manager_return
            else "预算退回"
            if is_application_claim and is_budget_return
            else "人工退回"
        )
        reason_code_payload = self._normalize_return_reason_code_payload(reason_codes)
        normalized_reason_codes = reason_code_payload["reason_codes"]
        unknown_reason_codes = reason_code_payload["unknown_reason_codes"]
        if is_application_return and not any(
            code.startswith("application_") for code in normalized_reason_codes
        ):
            raise ValueError("申请单退回必须选择至少一个退单类型。")
        risk_points = [RETURN_REASON_OPTIONS[code] for code in normalized_reason_codes]
        existing_return_flags = self._collect_return_flags(claim.risk_flags_json)
        return_count = len(existing_return_flags) + 1
        stage_return_count = (
            sum(
                1
                for flag in existing_return_flags
                if (
                    str(flag.get("return_stage_key") or "").strip()
                    or self._normalize_return_stage_key(str(flag.get("return_stage") or "").strip())
                )
                == previous_stage_key
            )
            + 1
        )
        message = reason or self._build_default_return_message(
            operator=operator,
            risk_points=risk_points,
        )
        return_flag = {
            "source": "manual_return",
            "event_type": return_event_type,
            "return_event_id": str(uuid.uuid4()),
            "severity": "medium",
            "label": return_label,
            "node_key": "returned",
            "node_label": "退回",
            "approval_node": "退回",
            "message": message,
            "reason": reason,
            "opinion": message,
            "leader_opinion": message if is_application_claim and is_direct_manager_return else "",
            "budget_opinion": message if is_application_claim and is_budget_return else "",
            "reason_codes": normalized_reason_codes,
            "risk_points": risk_points,
            "operator": operator,
            "operator_username": current_user.username,
            "operator_role_codes": [
                str(item).strip().lower() for item in current_user.role_codes if str(item).strip()
            ],
            "previous_status": previous_status,
            "previous_approval_stage": previous_stage,
            "return_stage": previous_stage,
            "return_stage_key": previous_stage_key,
            "next_status": "returned",
            "next_approval_stage": "待提交",
            "return_count": return_count,
            "stage_return_count": stage_return_count,
            "created_at": datetime.now(UTC).isoformat(),
        }
        if unknown_reason_codes:
            return_flag["unknown_reason_codes"] = unknown_reason_codes

        budget_flags = self._release_budget_for_return(
            claim,
            current_user,
            reason=message,
        )
        claim.status = "returned"
        claim.approval_stage = "待提交"
        claim.submitted_at = None
        claim.risk_flags_json = self._append_budget_flags(
            [*list(claim.risk_flags_json or []), return_flag],
            budget_flags,
            business_stage=("expense_application" if is_application_claim else "reimbursement"),
        )

        _, return_event = self._expense_cases.record_claim_event(
            claim,
            event_type=("application_returned" if is_application_claim else "claim_returned"),
            actor_id=current_user.username,
            tenant_id=getattr(current_user, "tenant_id", None),
            idempotency_key=str(return_flag.get("return_event_id") or ""),
            previous_status=previous_status,
            previous_approval_stage=previous_stage,
            extra_payload={
                "reason": message,
                "reason_codes": normalized_reason_codes,
            },
        )
        claim._approval_task_business_event = return_event
        self.audit_service.log_action(
            actor=operator,
            action="expense_claim.return",
            resource_type="expense_claim",
            resource_id=claim.id,
            before_json=before_json,
            after_json=self._serialize_claim(claim),
            request_id=request_id,
            commit=False,
        )
        return claim
