from __future__ import annotations

import uuid
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any

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 ExpenseCase
from app.models.savings import SavingsEvent, SavingsOpportunity
from app.schemas.savings import (
    SavingsOpportunityActionCreate,
    SavingsOpportunityMutationRead,
)
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


class SavingsTransitionError(ValueError):
    pass


@dataclass(slots=True)
class SavingsOpportunityMutation:
    response: SavingsOpportunityMutationRead


class SavingsActionService:
    """机会状态动作；一次调用只更新一个聚合并追加一个不可变事件。"""

    _TRANSITIONS = {
        ("identified", "accept"): "accepted",
        ("accepted", "start"): "in_progress",
        ("identified", "reject"): "rejected",
        ("accepted", "reject"): "rejected",
        ("in_progress", "reject"): "rejected",
        ("identified", "expire"): "expired",
        ("accepted", "expire"): "expired",
        ("in_progress", "expire"): "expired",
    }

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

    def execute(
        self,
        opportunity_id: str,
        payload: SavingsOpportunityActionCreate,
        current_user: CurrentUserContext,
    ) -> SavingsOpportunityMutation:
        tenant_id = self._tenant(current_user)
        actor_id, actor_name = savings_actor(current_user)
        fingerprint = savings_payload_fingerprint(
            action=payload.action,
            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,
                    payload.action,
                )
                if opportunity.version != payload.expected_version:
                    raise SavingsVersionConflictError(opportunity.version)

                target = self._TRANSITIONS.get((opportunity.status, payload.action))
                if target is None:
                    raise SavingsTransitionError(
                        f"机会状态 {opportunity.status} 不允许执行 {payload.action}。"
                    )
                now = datetime.now(UTC)
                before = self._state(opportunity)
                self._apply_transition(opportunity, target=target, now=now)
                opportunity.version += 1
                opportunity.updated_at = now
                event = self._new_event(
                    opportunity=opportunity,
                    payload=payload,
                    actor_id=actor_id,
                    actor_name=actor_name,
                    fingerprint=fingerprint,
                    before=before,
                    now=now,
                )
                response = SavingsOpportunityMutationRead(
                    opportunity=self.projection.opportunity_snapshot(
                        opportunity,
                        current_user,
                    ),
                    event=self.projection.event_read(event),
                    replayed=False,
                )
                event.response_json = response.model_dump(mode="json")
                self.db.add(event)
                self._append_business_event(
                    opportunity,
                    event,
                    current_user=current_user,
                    comment=payload.comment,
                )
                self.db.commit()
                return SavingsOpportunityMutation(response=response)
        except (
            LookupError,
            SavingsPermissionError,
            SavingsTransitionError,
            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 SavingsTransitionError("节省机会已被并发更新，请刷新后重试。") from error

    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))

    def _find_replay(
        self,
        *,
        tenant_id: str,
        actor_id: str,
        request_id: str,
        fingerprint: str,
    ) -> SavingsOpportunityMutation | 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 = SavingsOpportunityMutationRead.model_validate(event.response_json)
        return SavingsOpportunityMutation(response=response.model_copy(update={"replayed": True}))

    @classmethod
    def _apply_transition(
        cls,
        opportunity: SavingsOpportunity,
        *,
        target: str,
        now: datetime,
    ) -> None:
        opportunity.status = target
        if target == "accepted":
            opportunity.accepted_at = now
        elif target == "in_progress":
            opportunity.started_at = now
        elif target in {"rejected", "expired"}:
            opportunity.closed_at = now

    def _new_event(
        self,
        *,
        opportunity: SavingsOpportunity,
        payload: SavingsOpportunityActionCreate,
        actor_id: str,
        actor_name: str,
        fingerprint: str,
        before: dict[str, Any],
        now: datetime,
    ) -> SavingsEvent:
        return SavingsEvent(
            id=str(uuid.uuid4()),
            tenant_id=opportunity.tenant_id,
            aggregate_type="opportunity",
            aggregate_id=opportunity.id,
            opportunity_id=opportunity.id,
            action=payload.action,
            actor_id=actor_id,
            actor_name=actor_name,
            actor_type="user",
            request_id=payload.request_id,
            expected_version=payload.expected_version,
            result_version=opportunity.version,
            payload_fingerprint=fingerprint,
            payload_json=payload.model_dump(mode="json"),
            before_json=before,
            after_json=self._state(opportunity),
            response_json={},
            correlation_id=str(uuid.uuid4()),
            occurred_at=now,
        )

    def _append_business_event(
        self,
        opportunity: SavingsOpportunity,
        event: SavingsEvent,
        *,
        current_user: CurrentUserContext,
        comment: str,
    ) -> 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_opportunity",
            resource_id=opportunity.id,
            relation_type="savings",
            tenant_id=opportunity.tenant_id,
        )
        self.expense_cases.record_resource_event(
            expense_case,
            aggregate_type="savings_opportunity",
            aggregate_id=opportunity.id,
            event_type=f"saving_opportunity_{event.action}",
            actor_id=current_user.username,
            idempotency_key=event.request_id,
            tenant_id=opportunity.tenant_id,
            correlation_id=event.correlation_id,
            payload={
                "status": opportunity.status,
                "version": opportunity.version,
                "comment": comment,
            },
        )

    @staticmethod
    def _state(opportunity: SavingsOpportunity) -> dict[str, Any]:
        return {
            "id": opportunity.id,
            "status": opportunity.status,
            "version": opportunity.version,
            "accepted_at": opportunity.accepted_at.isoformat() if opportunity.accepted_at else None,
            "started_at": opportunity.started_at.isoformat() if opportunity.started_at else None,
            "realized_at": opportunity.realized_at.isoformat() if opportunity.realized_at else None,
            "verified_at": opportunity.verified_at.isoformat() if opportunity.verified_at else None,
            "closed_at": opportunity.closed_at.isoformat() if opportunity.closed_at else None,
        }

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