from __future__ import annotations

import uuid
from datetime import UTC, datetime
from decimal import Decimal

from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.expense_case import BusinessEvent
from app.models.financial_record import ExpenseClaim
from app.models.savings import (
    SavingsOpportunity,
    SavingsRealization,
)
from app.schemas.savings import (
    SavingsEvidenceCreate,
    SavingsRealizationActionCreate,
    SavingsRealizationCreate,
)
from app.services.expense_cases import ExpenseCaseService
from app.services.savings_access_policy import SavingsAccessPolicy, SavingsPermissionError
from app.services.savings_protocol import (
    SavingsIdempotencyConflictError,
    SavingsRequestProtocol,
    SavingsVersionConflictError,
    savings_actor,
    savings_payload_fingerprint,
)
from app.services.savings_read_projection import SavingsReadProjection
from app.services.savings_realization_support import (
    SavingsRealizationMutation,
    SavingsRealizationSupport,
)


class SavingsRealizationError(ValueError):
    pass


class SavingsRealizationService(SavingsRealizationSupport):
    """记录实际结果，并执行独立财务确认、拒绝和追加冲回。"""

    def __init__(self, db: Session) -> None:
        self.db = db
        self.protocol = SavingsRequestProtocol(db)
        self.projection = SavingsReadProjection()
        self.expense_cases = ExpenseCaseService(db)

    def record(
        self,
        opportunity_id: str,
        payload: SavingsRealizationCreate,
        current_user: CurrentUserContext,
    ) -> SavingsRealizationMutation:
        tenant_id = self._tenant(current_user)
        actor_id, actor_name = savings_actor(current_user)
        fingerprint = savings_payload_fingerprint(
            action="record_realization",
            aggregate_type="opportunity",
            aggregate_id=opportunity_id,
            actor_id=actor_id,
            payload=payload.model_dump(mode="json"),
        )
        try:
            with self.protocol.serialize(
                tenant_id=tenant_id,
                actor_id=actor_id,
                request_id=payload.request_id,
            ):
                replay = self._find_replay(
                    tenant_id=tenant_id,
                    actor_id=actor_id,
                    request_id=payload.request_id,
                    fingerprint=fingerprint,
                )
                if replay is not None:
                    return replay
                opportunity = self._lock_opportunity(opportunity_id, tenant_id=tenant_id)
                if opportunity is None:
                    raise LookupError("节省机会不存在。")
                SavingsAccessPolicy.require_opportunity_action(
                    opportunity,
                    current_user,
                    "record_realization",
                )
                if opportunity.version != payload.expected_version:
                    raise SavingsVersionConflictError(opportunity.version)
                if opportunity.status != "in_progress":
                    raise SavingsRealizationError("只有执行中的机会可以记录实际结果。")
                if payload.currency != opportunity.reporting_currency:
                    raise SavingsRealizationError(
                        "当前未提供可锁定汇率，实际结果只能使用机会报告币种。"
                    )

                now = datetime.now(UTC)
                realization = self._new_actual_realization(
                    opportunity,
                    actor_id=actor_id,
                    actor_name=actor_name,
                    actual_gross=payload.actual_gross,
                    incremental_cost=payload.incremental_cost,
                    currency=payload.currency,
                    realized_at=payload.realized_at,
                    attribution_method=payload.attribution_method,
                    attribution_ratio=payload.attribution_ratio,
                    evidence_level=payload.evidence_level,
                    realization_key=f"manual:{opportunity.id}:{payload.request_id}",
                    now=now,
                )
                self.db.add(realization)
                self.db.flush()
                self._add_evidence(
                    realization,
                    payload.evidence,
                    actor_id=actor_id,
                    verified=False,
                    now=now,
                )
                prior_opportunity_version = opportunity.version
                self._mark_opportunity_realized(opportunity, realized_at=payload.realized_at)
                main_event = self._new_realization_event(
                    realization,
                    action="record_realization",
                    request_id=payload.request_id,
                    expected_version=0,
                    actor_id=actor_id,
                    actor_name=actor_name,
                    fingerprint=fingerprint,
                    payload_json=payload.model_dump(mode="json"),
                    before_json={},
                    now=now,
                )
                response = self._mutation_response(
                    realization,
                    opportunity,
                    main_event,
                    current_user,
                )
                main_event.response_json = response.model_dump(mode="json")
                self.db.add(main_event)
                self._add_opportunity_projection_event(
                    opportunity,
                    action="realization_recorded",
                    request_id=payload.request_id,
                    actor_id=actor_id,
                    prior_version=prior_opportunity_version,
                    causation_id=main_event.id,
                    now=now,
                )
                self._append_business_event(
                    opportunity,
                    realization,
                    event_type="saving_action_completed",
                    actor_id=current_user.username,
                    idempotency_key=payload.request_id,
                    correlation_id=main_event.correlation_id,
                    payload={"evidence_level": payload.evidence_level},
                )
                self.db.commit()
                return SavingsRealizationMutation(response=response)
        except (
            LookupError,
            SavingsPermissionError,
            SavingsRealizationError,
            SavingsVersionConflictError,
            SavingsIdempotencyConflictError,
        ):
            self.db.rollback()
            raise
        except IntegrityError as error:
            self.db.rollback()
            replay = self._find_replay(
                tenant_id=tenant_id,
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                return replay
            raise SavingsRealizationError("实际结果已被并发记录，请刷新后重试。") from error

    def execute_action(
        self,
        realization_id: str,
        payload: SavingsRealizationActionCreate,
        current_user: CurrentUserContext,
    ) -> SavingsRealizationMutation:
        tenant_id = self._tenant(current_user)
        actor_id, actor_name = savings_actor(current_user)
        fingerprint = savings_payload_fingerprint(
            action=payload.action,
            aggregate_type="realization",
            aggregate_id=realization_id,
            actor_id=actor_id,
            payload=payload.model_dump(mode="json"),
        )
        try:
            with self.protocol.serialize(
                tenant_id=tenant_id,
                actor_id=actor_id,
                request_id=payload.request_id,
            ):
                replay = self._find_replay(
                    tenant_id=tenant_id,
                    actor_id=actor_id,
                    request_id=payload.request_id,
                    fingerprint=fingerprint,
                )
                if replay is not None:
                    return replay
                opportunity, realization = self._lock_realization_graph(
                    realization_id,
                    tenant_id=tenant_id,
                )
                if opportunity is None or realization is None:
                    raise LookupError("实际节省结果不存在。")
                SavingsAccessPolicy.require_realization_action(
                    realization,
                    opportunity,
                    current_user,
                    payload.action,
                )
                if realization.version != payload.expected_version:
                    raise SavingsVersionConflictError(realization.version)

                now = datetime.now(UTC)
                before = self._realization_state(realization)
                prior_opportunity_version = opportunity.version
                if payload.action == "confirm":
                    self._add_evidence(
                        realization,
                        payload.evidence,
                        actor_id=actor_id,
                        verified=True,
                        now=now,
                    )
                    self._confirm(
                        opportunity,
                        realization,
                        actor_id=actor_id,
                        actor_name=actor_name,
                        comment=payload.comment,
                        now=now,
                    )
                    result = realization
                    business_event_type = "saving_confirmed"
                elif payload.action == "reject":
                    self._reject(
                        opportunity,
                        realization,
                        actor_id=actor_id,
                        actor_name=actor_name,
                        comment=payload.comment,
                        now=now,
                    )
                    result = realization
                    business_event_type = "saving_result_rejected"
                elif payload.action == "reverse":
                    result = self._reverse(
                        opportunity,
                        realization,
                        actor_id=actor_id,
                        actor_name=actor_name,
                        amount=payload.reversal_amount,
                        comment=payload.comment,
                        now=now,
                    )
                    business_event_type = "saving_reversed"
                else:  # pragma: no cover - Pydantic 已限制枚举
                    raise SavingsRealizationError("未知的实际结果动作。")

                if payload.action != "confirm":
                    self._add_evidence(
                        result,
                        payload.evidence,
                        actor_id=actor_id,
                        verified=False,
                        now=now,
                    )
                realization.version += 1
                realization.updated_at = now
                fingerprint_payload = payload.model_dump(mode="json")
                main_event = self._new_realization_event(
                    realization,
                    action=payload.action,
                    request_id=payload.request_id,
                    expected_version=payload.expected_version,
                    actor_id=actor_id,
                    actor_name=actor_name,
                    fingerprint=fingerprint,
                    payload_json=fingerprint_payload,
                    before_json=before,
                    now=now,
                )
                response = self._mutation_response(
                    result,
                    opportunity,
                    main_event,
                    current_user,
                )
                main_event.response_json = response.model_dump(mode="json")
                self.db.add(main_event)
                self._add_opportunity_projection_event(
                    opportunity,
                    action=f"realization_{payload.action}",
                    request_id=payload.request_id,
                    actor_id=actor_id,
                    prior_version=prior_opportunity_version,
                    causation_id=main_event.id,
                    now=now,
                )
                if result.id != realization.id:
                    self._add_realization_projection_event(
                        result,
                        request_id=payload.request_id,
                        causation_id=main_event.id,
                        now=now,
                    )
                self._append_business_event(
                    opportunity,
                    result,
                    event_type=business_event_type,
                    actor_id=current_user.username,
                    idempotency_key=payload.request_id,
                    correlation_id=main_event.correlation_id,
                    payload={"comment": payload.comment, "action": payload.action},
                )
                self.db.commit()
                return SavingsRealizationMutation(response=response)
        except (
            LookupError,
            SavingsPermissionError,
            SavingsRealizationError,
            SavingsVersionConflictError,
            SavingsIdempotencyConflictError,
        ):
            self.db.rollback()
            raise
        except IntegrityError as error:
            self.db.rollback()
            replay = self._find_replay(
                tenant_id=tenant_id,
                actor_id=actor_id,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                return replay
            raise SavingsRealizationError("实际结果已被并发处理，请刷新后重试。") from error

    def realize_paid_claim(
        self,
        claim: ExpenseClaim,
        payment_event: BusinessEvent,
        current_user: CurrentUserContext,
    ) -> list[SavingsRealization]:
        """付款事务内生成待确认 actual；调用方负责最终 commit。"""
        tenant_id = self._tenant(current_user)
        if payment_event.tenant_id != tenant_id:
            raise PermissionError("付款事件与当前租户不一致。")
        opportunity_statement = select(SavingsOpportunity).where(
            SavingsOpportunity.tenant_id == tenant_id,
            SavingsOpportunity.claim_id == claim.id,
            SavingsOpportunity.status == "in_progress",
        )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            opportunity_statement = opportunity_statement.with_for_update()
        opportunities = list(self.db.scalars(opportunity_statement).all())
        created: list[SavingsRealization] = []
        actor_id, actor_name = savings_actor(current_user)
        for opportunity in opportunities:
            key = f"payment:{payment_event.id}:{opportunity.id}"
            existing = self.db.scalar(
                select(SavingsRealization).where(
                    SavingsRealization.tenant_id == tenant_id,
                    SavingsRealization.realization_key == key,
                )
            )
            if existing is not None:
                continue
            if opportunity.currency != opportunity.reporting_currency:
                continue
            realized_at = payment_event.occurred_at or datetime.now(UTC)
            external_evidence = dict(
                (payment_event.payload_json or {}).get("external_payment_evidence") or {}
            )
            evidence_classification = str(
                external_evidence.get("evidence_classification") or "business_state"
            ).strip()
            realization = self._new_actual_realization(
                opportunity,
                actor_id=actor_id,
                actor_name=actor_name,
                actual_gross=opportunity.estimated_gross,
                incremental_cost=opportunity.estimated_cost,
                currency=opportunity.currency,
                realized_at=realized_at,
                attribution_method=opportunity.attribution_method,
                attribution_ratio=Decimal("1.000000"),
                evidence_level=evidence_classification,
                realization_key=key,
                now=realized_at,
                business_event_id=payment_event.id,
            )
            self.db.add(realization)
            self.db.flush()
            connector_event_id = str(
                external_evidence.get("connector_event_id") or ""
            ).strip()
            if connector_event_id:
                evidence = SavingsEvidenceCreate(
                    evidence_key=f"connector:{connector_event_id}:{realization.id}",
                    evidence_role="external_payment_settlement",
                    resource_type="financial_connector_event",
                    resource_id=connector_event_id,
                    source_system=str(external_evidence.get("provider") or "financial-connector"),
                    external_event_id=str(external_evidence.get("external_event_id") or "") or None,
                    content_hash=str(external_evidence.get("content_hash") or ""),
                    occurred_at=realized_at,
                    verification_status=(
                        "unavailable"
                        if evidence_classification == "simulated_connector"
                        else "unverified"
                    ),
                    metadata_json={
                        "evidence_level": evidence_classification,
                        "verification_level": external_evidence.get("verification_level"),
                        "external_reference_tail": external_evidence.get(
                            "external_reference_tail"
                        ),
                    },
                )
            else:
                evidence = SavingsEvidenceCreate(
                    evidence_key=f"payment:{payment_event.id}:{realization.id}",
                    evidence_role="payment_business_state",
                    resource_type="business_event",
                    resource_id=payment_event.id,
                    source_system="x-financial",
                    external_event_id=payment_event.id,
                    content_hash=self._event_content_hash(payment_event),
                    occurred_at=realized_at,
                    verification_status="unverified",
                    metadata_json={"evidence_level": "business_state"},
                )
            self._add_evidence(
                realization,
                [evidence],
                actor_id=actor_id,
                verified=False,
                now=realized_at,
            )
            prior_opportunity_version = opportunity.version
            self._mark_opportunity_realized(opportunity, realized_at=realized_at)
            request_id = key[:120]
            fingerprint = savings_payload_fingerprint(
                action="record_realization",
                aggregate_type="realization",
                aggregate_id=realization.id,
                actor_id=actor_id,
                payload={"payment_event_id": payment_event.id},
            )
            event = self._new_realization_event(
                realization,
                action="record_realization",
                request_id=request_id,
                expected_version=0,
                actor_id=actor_id,
                actor_name=actor_name,
                fingerprint=fingerprint,
                payload_json={"payment_event_id": payment_event.id},
                before_json={},
                now=realized_at,
                causation_id=payment_event.id,
            )
            response = self._mutation_response(
                realization,
                opportunity,
                event,
                current_user,
            )
            event.response_json = response.model_dump(mode="json")
            self.db.add(event)
            self._add_opportunity_projection_event(
                opportunity,
                action="payment_realized",
                request_id=request_id,
                actor_id=actor_id,
                prior_version=prior_opportunity_version,
                causation_id=event.id,
                now=realized_at,
            )
            self._append_business_event(
                opportunity,
                realization,
                event_type="saving_action_completed",
                actor_id=current_user.username,
                idempotency_key=request_id,
                correlation_id=event.correlation_id,
                causation_id=payment_event.id,
                payload={"evidence_level": "business_state"},
            )
            created.append(realization)
        self.db.flush()
        return created

    def _confirm(
        self,
        opportunity: SavingsOpportunity,
        realization: SavingsRealization,
        *,
        actor_id: str,
        actor_name: str,
        comment: str,
        now: datetime,
    ) -> None:
        if self._verify_realization_evidence(
            realization,
            actor_id=actor_id,
            now=now,
        ) == 0:
            raise SavingsRealizationError("实际节省结果缺少可复核证据，不能进入财务确认口径。")
        duplicate = self.db.scalar(
            select(SavingsRealization.id).where(
                SavingsRealization.tenant_id == realization.tenant_id,
                SavingsRealization.benefit_key == realization.benefit_key,
                SavingsRealization.realization_type == "actual",
                SavingsRealization.dedupe_status == "canonical",
                SavingsRealization.id != realization.id,
            )
        )
        if duplicate is not None:
            raise SavingsRealizationError("同一经济收益已经存在 canonical 实际结果。")
        realization.status = "finance_confirmed"
        realization.dedupe_status = "canonical"
        realization.finance_confirmer_id = actor_id
        realization.finance_confirmer_name = actor_name
        realization.confirmed_at = now
        realization.confirmation_note = comment
        opportunity.status = "verified"
        opportunity.verified_at = now
        opportunity.closed_at = now
        opportunity.version += 1
        opportunity.updated_at = now

    @staticmethod
    def _reject(
        opportunity: SavingsOpportunity,
        realization: SavingsRealization,
        *,
        actor_id: str,
        actor_name: str,
        comment: str,
        now: datetime,
    ) -> None:
        realization.status = "rejected"
        realization.dedupe_status = "excluded"
        realization.rejected_by_id = actor_id
        realization.rejected_by_name = actor_name
        realization.rejected_at = now
        realization.rejection_reason = comment
        opportunity.status = "rejected"
        opportunity.closed_at = now
        opportunity.version += 1
        opportunity.updated_at = now

    def _reverse(
        self,
        opportunity: SavingsOpportunity,
        realization: SavingsRealization,
        *,
        actor_id: str,
        actor_name: str,
        amount: Decimal | None,
        comment: str,
        now: datetime,
    ) -> SavingsRealization:
        normalized = Decimal(amount or Decimal("0.00")).quantize(Decimal("0.0001"))
        if normalized != Decimal(realization.actual_net).quantize(Decimal("0.0001")):
            raise SavingsRealizationError("首期仅支持对已确认净收益执行全额冲回。")
        existing_reversal = self.db.scalar(
            select(SavingsRealization.id).where(
                SavingsRealization.tenant_id == realization.tenant_id,
                SavingsRealization.reversal_of_realization_id == realization.id,
            )
        )
        if existing_reversal is not None:
            raise SavingsRealizationError("该已确认结果已经完成冲回，不能重复冲回。")
        reversal = SavingsRealization(
            id=str(uuid.uuid4()),
            tenant_id=realization.tenant_id,
            realization_key=f"reversal:{realization.id}:{uuid.uuid4().hex[:12]}",
            opportunity_id=opportunity.id,
            expense_case_id=opportunity.expense_case_id,
            claim_id=opportunity.claim_id,
            claim_item_id=opportunity.claim_item_id,
            realization_type="reversal",
            reversal_of_realization_id=realization.id,
            realized_at=now,
            recorded_by_id=actor_id,
            recorded_by_name=actor_name,
            actual_gross=-normalized,
            incremental_cost=Decimal("0.0000"),
            actual_net=-normalized,
            original_currency=realization.original_currency,
            reporting_amount=-Decimal(realization.reporting_amount),
            reporting_currency=realization.reporting_currency,
            fx_rate=realization.fx_rate,
            fx_source=realization.fx_source,
            fx_date=realization.fx_date,
            fx_version=realization.fx_version,
            attribution_method="full_reversal",
            attribution_ratio=realization.attribution_ratio,
            benefit_key=realization.benefit_key,
            dedupe_status="canonical",
            status="finance_confirmed",
            finance_confirmer_id=actor_id,
            finance_confirmer_name=actor_name,
            confirmed_at=now,
            confirmation_note=comment,
            baseline_snapshot_json=dict(realization.baseline_snapshot_json or {}),
            final_snapshot_json={"reversal_reason": comment, "reversal_of": realization.id},
            evidence_json=[],
            version=1,
            created_at=now,
            updated_at=now,
        )
        self.db.add(reversal)
        self.db.flush()
        # 原正向确认事实保持 finance_confirmed；冲回由独立负向事实表达。
        realization.reversed_by_id = actor_id
        realization.reversed_by_name = actor_name
        realization.reversed_at = now
        realization.reversal_reason = comment
        opportunity.status = "reversed"
        opportunity.closed_at = now
        opportunity.version += 1
        opportunity.updated_at = now
        return reversal
