from __future__ import annotations

import uuid
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from typing import Any

from sqlalchemy import select

from app.api.deps import CurrentUserContext
from app.models.expense_case import BusinessEvent, ExpenseCase
from app.models.savings import (
    SavingsEvent,
    SavingsEvidenceLink,
    SavingsOpportunity,
    SavingsRealization,
)
from app.schemas.savings import (
    SavingsEvidenceCreate,
    SavingsRealizationMutationRead,
)
from app.services.savings_protocol import (
    SavingsIdempotencyConflictError,
    savings_payload_fingerprint,
)


@dataclass(slots=True)
class SavingsRealizationMutation:
    response: SavingsRealizationMutationRead


class SavingsRealizationSupport:
    """实际结果服务的锁、证据、事件和响应构建辅助职责。"""

    @staticmethod
    def _new_actual_realization(
        opportunity: SavingsOpportunity,
        *,
        actor_id: str,
        actor_name: str,
        actual_gross: Decimal,
        incremental_cost: Decimal,
        currency: str,
        realized_at: datetime,
        attribution_method: str,
        attribution_ratio: Decimal,
        evidence_level: str,
        realization_key: str,
        now: datetime,
        business_event_id: str | None = None,
    ) -> SavingsRealization:
        gross = Decimal(actual_gross).quantize(Decimal("0.0001"))
        cost = Decimal(incremental_cost).quantize(Decimal("0.0001"))
        net = gross - cost
        return SavingsRealization(
            id=str(uuid.uuid4()),
            tenant_id=opportunity.tenant_id,
            realization_key=realization_key[:180],
            opportunity_id=opportunity.id,
            expense_case_id=opportunity.expense_case_id,
            claim_id=opportunity.claim_id,
            claim_item_id=opportunity.claim_item_id,
            business_event_id=business_event_id,
            realization_type="actual",
            realized_at=realized_at,
            recorded_by_id=actor_id,
            recorded_by_name=actor_name,
            actual_gross=gross,
            incremental_cost=cost,
            actual_net=net,
            original_currency=currency,
            reporting_amount=net,
            reporting_currency=opportunity.reporting_currency,
            fx_rate=Decimal("1.00000000"),
            fx_source="same_currency",
            fx_date=realized_at.date(),
            fx_version="identity-v1",
            attribution_method=attribution_method[:60],
            attribution_ratio=attribution_ratio,
            benefit_key=opportunity.benefit_key,
            dedupe_status="pending_review",
            status="pending_confirmation",
            baseline_snapshot_json=dict(opportunity.baseline_snapshot_json or {}),
            final_snapshot_json={
                "evidence_level": evidence_level,
                "estimated_net": str(opportunity.estimated_net),
            },
            evidence_json=[],
            version=1,
            created_at=now,
            updated_at=now,
        )

    def _lock_realization_graph(
        self,
        realization_id: str,
        *,
        tenant_id: str,
    ) -> tuple[SavingsOpportunity | None, SavingsRealization | None]:
        opportunity_id = self.db.scalar(
            select(SavingsRealization.opportunity_id).where(
                SavingsRealization.tenant_id == tenant_id,
                SavingsRealization.id == realization_id,
            )
        )
        if opportunity_id is None:
            return None, None
        opportunity = self._lock_opportunity(opportunity_id, tenant_id=tenant_id)
        statement = select(SavingsRealization).where(
            SavingsRealization.tenant_id == tenant_id,
            SavingsRealization.id == realization_id,
            SavingsRealization.opportunity_id == opportunity_id,
        )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            statement = statement.with_for_update()
        realization = self.db.scalar(statement.execution_options(populate_existing=True))
        return opportunity, realization

    def _lock_opportunity(
        self,
        opportunity_id: str,
        *,
        tenant_id: str,
    ) -> SavingsOpportunity | None:
        statement = select(SavingsOpportunity).where(
            SavingsOpportunity.tenant_id == tenant_id,
            SavingsOpportunity.id == opportunity_id,
        )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            statement = statement.with_for_update()
        return self.db.scalar(statement.execution_options(populate_existing=True))

    @staticmethod
    def _mark_opportunity_realized(
        opportunity: SavingsOpportunity,
        *,
        realized_at: datetime,
    ) -> None:
        opportunity.status = "realized"
        opportunity.realized_at = realized_at
        opportunity.version += 1
        opportunity.updated_at = realized_at

    def _new_realization_event(
        self,
        realization: SavingsRealization,
        *,
        action: str,
        request_id: str,
        expected_version: int,
        actor_id: str,
        actor_name: str,
        fingerprint: str,
        payload_json: dict[str, Any],
        before_json: dict[str, Any],
        now: datetime,
        causation_id: str | None = None,
    ) -> SavingsEvent:
        return SavingsEvent(
            id=str(uuid.uuid4()),
            tenant_id=realization.tenant_id,
            aggregate_type="realization",
            aggregate_id=realization.id,
            realization_id=realization.id,
            action=action,
            actor_id=actor_id,
            actor_name=actor_name,
            actor_type="user",
            request_id=request_id[:120],
            expected_version=expected_version,
            result_version=realization.version,
            payload_fingerprint=fingerprint,
            payload_json=payload_json,
            before_json=before_json,
            after_json=self._realization_state(realization),
            response_json={},
            correlation_id=str(uuid.uuid4()),
            causation_id=causation_id,
            occurred_at=now,
        )

    def _add_opportunity_projection_event(
        self,
        opportunity: SavingsOpportunity,
        *,
        action: str,
        request_id: str,
        actor_id: str,
        prior_version: int,
        causation_id: str,
        now: datetime,
    ) -> None:
        fingerprint = savings_payload_fingerprint(
            action=action,
            aggregate_type="opportunity",
            aggregate_id=opportunity.id,
            actor_id="savings-ledger",
            payload={"causation_id": causation_id, "request_id": request_id},
        )
        event = SavingsEvent(
            id=str(uuid.uuid4()),
            tenant_id=opportunity.tenant_id,
            aggregate_type="opportunity",
            aggregate_id=opportunity.id,
            opportunity_id=opportunity.id,
            action=action,
            actor_id="savings-ledger",
            actor_name="Savings Ledger",
            actor_type="service",
            request_id=f"projection:{opportunity.id}:{fingerprint[-64:]}",
            expected_version=prior_version,
            result_version=opportunity.version,
            payload_fingerprint=fingerprint,
            payload_json={"source_actor_id": actor_id},
            before_json={"version": prior_version},
            after_json={"status": opportunity.status, "version": opportunity.version},
            response_json={"status": opportunity.status, "version": opportunity.version},
            correlation_id=str(uuid.uuid4()),
            causation_id=causation_id,
            occurred_at=now,
        )
        self.db.add(event)

    def _add_realization_projection_event(
        self,
        realization: SavingsRealization,
        *,
        request_id: str,
        causation_id: str,
        now: datetime,
    ) -> None:
        fingerprint = savings_payload_fingerprint(
            action="reversal_created",
            aggregate_type="realization",
            aggregate_id=realization.id,
            actor_id="savings-ledger",
            payload={"causation_id": causation_id, "request_id": request_id},
        )
        event = SavingsEvent(
            id=str(uuid.uuid4()),
            tenant_id=realization.tenant_id,
            aggregate_type="realization",
            aggregate_id=realization.id,
            realization_id=realization.id,
            action="reversal_created",
            actor_id="savings-ledger",
            actor_name="Savings Ledger",
            actor_type="service",
            request_id=f"reversal:{realization.id}:{fingerprint[-64:]}",
            expected_version=0,
            result_version=1,
            payload_fingerprint=fingerprint,
            payload_json={"causation_id": causation_id},
            before_json={},
            after_json=self._realization_state(realization),
            response_json=self._realization_state(realization),
            correlation_id=str(uuid.uuid4()),
            causation_id=causation_id,
            occurred_at=now,
        )
        self.db.add(event)

    def _add_evidence(
        self,
        realization: SavingsRealization,
        evidence: list[SavingsEvidenceCreate],
        *,
        actor_id: str,
        verified: bool,
        now: datetime,
    ) -> None:
        public_evidence = list(realization.evidence_json or [])
        for item in evidence:
            verification_status = (
                "verified"
                if verified
                else ("unavailable" if item.verification_status == "unavailable" else "unverified")
            )
            self.db.add(
                SavingsEvidenceLink(
                    id=str(uuid.uuid4()),
                    tenant_id=realization.tenant_id,
                    evidence_key=item.evidence_key,
                    entity_type="realization",
                    entity_id=realization.id,
                    realization_id=realization.id,
                    evidence_role=item.evidence_role,
                    resource_type=item.resource_type,
                    resource_id=item.resource_id,
                    source_system=item.source_system,
                    external_event_id=item.external_event_id,
                    content_hash=item.content_hash,
                    occurred_at=item.occurred_at,
                    collected_at=now,
                    verification_status=verification_status,
                    verified_by=actor_id if verified else None,
                    verified_at=now if verified else None,
                    metadata_json=dict(item.metadata_json or {}),
                    created_at=now,
                )
            )
            public_evidence.append(
                {
                    "evidence_key": item.evidence_key,
                    "role": item.evidence_role,
                    "resource_type": item.resource_type,
                    "verification_status": verification_status,
                }
            )
        realization.evidence_json = public_evidence

    def _verify_realization_evidence(
        self,
        realization: SavingsRealization,
        *,
        actor_id: str,
        now: datetime,
    ) -> int:
        """财务确认时固化证据复核结果，避免“无证据可核验”进入 CFO 口径。"""
        links = list(
            self.db.scalars(
                select(SavingsEvidenceLink).where(
                    SavingsEvidenceLink.tenant_id == realization.tenant_id,
                    SavingsEvidenceLink.entity_type == "realization",
                    SavingsEvidenceLink.entity_id == realization.id,
                )
            ).all()
        )
        if not links:
            return 0
        verified_keys: set[str] = set()
        for link in links:
            link.verification_status = "verified"
            link.verified_by = actor_id
            link.verified_at = now
            verified_keys.add(link.evidence_key)
        realization.evidence_json = [
            {
                **item,
                "verification_status": "verified",
            }
            if item.get("evidence_key") in verified_keys
            else item
            for item in list(realization.evidence_json or [])
        ]
        return len(links)

    def _append_business_event(
        self,
        opportunity: SavingsOpportunity,
        realization: SavingsRealization,
        *,
        event_type: str,
        actor_id: str,
        idempotency_key: str,
        correlation_id: str | None,
        payload: dict[str, Any],
        causation_id: str | None = None,
    ) -> None:
        expense_case = self.db.get(ExpenseCase, opportunity.expense_case_id)
        if expense_case is None or expense_case.tenant_id != opportunity.tenant_id:
            raise RuntimeError("实际结果关联的费用事件不存在或租户不一致。")
        self.expense_cases.link_resource(
            expense_case,
            resource_type="savings_realization",
            resource_id=realization.id,
            relation_type="savings",
            tenant_id=opportunity.tenant_id,
        )
        self.expense_cases.record_resource_event(
            expense_case,
            aggregate_type="savings_realization",
            aggregate_id=realization.id,
            event_type=event_type,
            actor_id=actor_id,
            idempotency_key=idempotency_key[:120],
            tenant_id=opportunity.tenant_id,
            correlation_id=correlation_id,
            causation_id=causation_id,
            payload={
                **payload,
                "opportunity_id": opportunity.id,
                "realization_id": realization.id,
                "status": realization.status,
            },
        )

    def _mutation_response(
        self,
        realization: SavingsRealization,
        opportunity: SavingsOpportunity,
        event: SavingsEvent,
        current_user: CurrentUserContext,
    ) -> SavingsRealizationMutationRead:
        return SavingsRealizationMutationRead(
            realization=self.projection.realization_read(
                realization,
                opportunity,
                current_user,
            ),
            opportunity=self.projection.opportunity_snapshot(opportunity, current_user),
            event=self.projection.event_read(event),
            replayed=False,
        )

    def _find_replay(
        self,
        *,
        tenant_id: str,
        actor_id: str,
        request_id: str,
        fingerprint: str,
    ) -> SavingsRealizationMutation | None:
        event = self.db.scalar(
            select(SavingsEvent).where(
                SavingsEvent.tenant_id == tenant_id,
                SavingsEvent.actor_id == actor_id,
                SavingsEvent.request_id == request_id,
            )
        )
        if event is None:
            return None
        if event.payload_fingerprint != fingerprint:
            raise SavingsIdempotencyConflictError("request_id 已被不同的实际节省动作使用。")
        response = SavingsRealizationMutationRead.model_validate(event.response_json)
        return SavingsRealizationMutation(response=response.model_copy(update={"replayed": True}))

    @staticmethod
    def _realization_state(realization: SavingsRealization) -> dict[str, Any]:
        return {
            "id": realization.id,
            "status": realization.status,
            "dedupe_status": realization.dedupe_status,
            "actual_net": str(realization.actual_net),
            "version": realization.version,
            "confirmed_at": realization.confirmed_at.isoformat()
            if realization.confirmed_at
            else None,
            "rejected_at": realization.rejected_at.isoformat() if realization.rejected_at else None,
            "reversed_at": realization.reversed_at.isoformat() if realization.reversed_at else None,
        }

    @staticmethod
    def _event_content_hash(event: BusinessEvent) -> str:
        return savings_payload_fingerprint(
            action=event.event_type,
            aggregate_type=event.aggregate_type,
            aggregate_id=event.aggregate_id,
            actor_id=event.actor_id,
            payload=event.payload_json or {},
        )

    @staticmethod
    def _tenant(current_user: CurrentUserContext) -> str:
        return str(current_user.tenant_id or "default").strip() or "default"
