from __future__ import annotations

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

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.ai_application_preview import AIApplicationPreviewDecision
from app.models.auth_session import AuthSession
from app.models.expense_case import BusinessEvent
from app.models.financial_record import ExpenseClaim
from app.services.expense_application_snapshot import (
    FINGERPRINT_KEY_VERSION,
    field_fingerprints,
    hmac_fingerprint,
    preview_response_fields,
    safe_fact_snapshot,
)
from app.services.expense_cases import ExpenseCaseService

PREVIEW_DECISION_TTL = timedelta(minutes=30)


class PreviewDecisionConflictError(ValueError):
    """预览决策已过期、已消费或与当前动作不一致。"""


@dataclass(frozen=True)
class IssuedPreviewDecision:
    decision: AIApplicationPreviewDecision
    fields: dict[str, str]


class ExpenseApplicationPreviewDecisionService:
    """签发、授权和消费费用申请预览决策，不保存明文建议值。"""

    def __init__(self, db: Session) -> None:
        self.db = db

    def issue(
        self,
        facts: dict[str, Any],
        current_user: CurrentUserContext,
        *,
        conversation_id: str,
        request_id: str,
        decision_source: str | None = None,
    ) -> IssuedPreviewDecision:
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        actor_id = self._actor_id(current_user)
        auth_session_id = self._auth_session_id(current_user)
        # 真实 Bearer 会话存在时锁住会话行，串行化同一登录会话内的并发签发，
        # 避免两个不同 request_id 同时越过 active snapshot 查询。
        self.db.scalar(
            select(AuthSession.id)
            .where(AuthSession.id == auth_session_id)
            .with_for_update()
        )
        normalized_request_id = self._required_text(request_id, "预览签发请求 ID", 120)
        values = safe_fact_snapshot(facts)
        if not values.get("time") and not values.get("location") and not values.get("reason"):
            raise ValueError("未识别到可签发的费用申请字段。")

        existing = self.db.scalar(
            select(AIApplicationPreviewDecision).where(
                AIApplicationPreviewDecision.tenant_id == tenant_id,
                AIApplicationPreviewDecision.actor_id == actor_id,
                AIApplicationPreviewDecision.auth_session_id == auth_session_id,
                AIApplicationPreviewDecision.issue_request_id == normalized_request_id,
            )
        )
        if existing is not None:
            snapshot_fingerprint = hmac_fingerprint(
                values,
                key_version=existing.fingerprint_key_version,
                allow_key_creation=False,
            )
            if existing.snapshot_fingerprint != snapshot_fingerprint:
                raise PreviewDecisionConflictError("同一预览签发请求不能对应不同内容。")
            return IssuedPreviewDecision(existing, preview_response_fields(facts))

        now = datetime.now(UTC)
        normalized_conversation_id = str(conversation_id or "").strip()[:120]
        snapshot_fingerprint = hmac_fingerprint(
            values,
            key_version=FINGERPRINT_KEY_VERSION,
        )
        active_same_snapshot = self.db.scalar(
            select(AIApplicationPreviewDecision)
            .where(
                AIApplicationPreviewDecision.tenant_id == tenant_id,
                AIApplicationPreviewDecision.actor_id == actor_id,
                AIApplicationPreviewDecision.auth_session_id == auth_session_id,
                AIApplicationPreviewDecision.conversation_id == normalized_conversation_id,
                AIApplicationPreviewDecision.status == "issued",
                AIApplicationPreviewDecision.expires_at > now,
                AIApplicationPreviewDecision.fingerprint_key_version
                == FINGERPRINT_KEY_VERSION,
                AIApplicationPreviewDecision.snapshot_fingerprint == snapshot_fingerprint,
            )
            .order_by(AIApplicationPreviewDecision.created_at.desc())
            .with_for_update()
        )
        if active_same_snapshot is not None:
            return IssuedPreviewDecision(
                active_same_snapshot,
                preview_response_fields(facts),
            )

        source = decision_source or self._decision_source(facts)
        decision = AIApplicationPreviewDecision(
            tenant_id=tenant_id,
            actor_id=actor_id,
            auth_session_id=auth_session_id,
            conversation_id=normalized_conversation_id,
            decision_type="expense_application_prefill",
            decision_source=source,
            field_keys_json=sorted(values),
            field_fingerprints_json=field_fingerprints(
                values,
                key_version=FINGERPRINT_KEY_VERSION,
            ),
            snapshot_fingerprint=snapshot_fingerprint,
            fingerprint_key_version=FINGERPRINT_KEY_VERSION,
            source_version_json={
                "schema_version": 1,
                "rule_name_fingerprint": hmac_fingerprint(
                    values.get("rule_name", ""),
                    key_version=FINGERPRINT_KEY_VERSION,
                ),
                "rule_version_fingerprint": hmac_fingerprint(
                    values.get("rule_version", ""),
                    key_version=FINGERPRINT_KEY_VERSION,
                ),
            },
            status="issued",
            issue_request_id=normalized_request_id,
            expires_at=now + PREVIEW_DECISION_TTL,
        )
        self.db.add(decision)
        self.db.flush()
        return IssuedPreviewDecision(decision, preview_response_fields(facts))

    def require_for_action(
        self,
        decision_id: str,
        current_user: CurrentUserContext,
        *,
        conversation_id: str,
        request_id: str,
        action_type: str,
        final_values: dict[str, Any],
    ) -> AIApplicationPreviewDecision:
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        actor_id = self._actor_id(current_user)
        auth_session_id = self._auth_session_id(current_user)
        normalized_id = self._required_text(decision_id, "预览决策 ID", 36)
        normalized_request_id = self._required_text(request_id, "动作请求 ID", 120)
        normalized_action = self._normalize_action(action_type)
        decision = self.db.scalar(
            select(AIApplicationPreviewDecision)
            .where(
                AIApplicationPreviewDecision.id == normalized_id,
                AIApplicationPreviewDecision.tenant_id == tenant_id,
                AIApplicationPreviewDecision.actor_id == actor_id,
                AIApplicationPreviewDecision.auth_session_id == auth_session_id,
            )
            .with_for_update()
        )
        if decision is None:
            raise ValueError("预览决策不存在或不属于当前登录会话。")
        if decision.conversation_id != str(conversation_id or "").strip()[:120]:
            raise ValueError("预览决策不属于当前会话。")

        final_fingerprint = self._final_fingerprint(decision, final_values)
        if decision.status == "consumed":
            if (
                decision.consumed_action == normalized_action
                and decision.consumed_request_id == normalized_request_id
                and decision.consumed_final_fingerprint == final_fingerprint
            ):
                return decision
            raise PreviewDecisionConflictError("该预览决策已被其他动作消费，请重新生成预览。")
        if decision.status != "issued":
            raise PreviewDecisionConflictError("该预览决策当前不可使用，请重新生成预览。")
        if self._as_utc(decision.expires_at) <= datetime.now(UTC):
            raise PreviewDecisionConflictError("该预览决策已过期，请重新生成预览。")
        return decision

    def reject_active_decision_bypass(
        self,
        current_user: CurrentUserContext,
        *,
        conversation_id: str,
    ) -> None:
        """同一登录会话已有有效签发时，不允许通过省略 decision_id 降级。"""
        tenant_id = ExpenseCaseService.normalize_tenant_id(current_user.tenant_id)
        actor_id = self._actor_id(current_user)
        auth_session_id = str(current_user.auth_session_id or "").strip()[:120]
        # 旧调用没有会话标识，也不具备签发 decision_id 的前提，继续走非可信兼容路径。
        if not auth_session_id:
            return
        normalized_conversation_id = str(conversation_id or "").strip()[:120]
        active_decision = self.db.scalar(
            select(AIApplicationPreviewDecision)
            .where(
                AIApplicationPreviewDecision.tenant_id == tenant_id,
                AIApplicationPreviewDecision.actor_id == actor_id,
                AIApplicationPreviewDecision.auth_session_id == auth_session_id,
                AIApplicationPreviewDecision.conversation_id == normalized_conversation_id,
                AIApplicationPreviewDecision.status == "issued",
                AIApplicationPreviewDecision.expires_at > datetime.now(UTC),
            )
            .order_by(AIApplicationPreviewDecision.created_at.desc())
            .with_for_update()
        )
        if active_decision is not None:
            raise PreviewDecisionConflictError(
                "当前会话已有服务端签发预览，动作必须携带 decision_id。"
            )

    def consume(
        self,
        decision: AIApplicationPreviewDecision,
        *,
        request_id: str,
        action_type: str,
        final_values: dict[str, Any],
        claim: ExpenseClaim,
        business_event: BusinessEvent,
    ) -> None:
        normalized_request_id = self._required_text(request_id, "动作请求 ID", 120)
        normalized_action = self._normalize_action(action_type)
        final_fingerprint = self._final_fingerprint(decision, final_values)
        if decision.status == "consumed":
            if (
                decision.consumed_action == normalized_action
                and decision.consumed_request_id == normalized_request_id
                and decision.consumed_claim_id == claim.id
                and decision.consumed_business_event_id == business_event.id
                and decision.consumed_final_fingerprint == final_fingerprint
            ):
                return
            raise PreviewDecisionConflictError("该预览决策已被消费。")
        if decision.status != "issued":
            raise PreviewDecisionConflictError("该预览决策当前不可消费。")
        decision.status = "consumed"
        decision.consumed_action = normalized_action
        decision.consumed_request_id = normalized_request_id
        decision.consumed_claim_id = claim.id
        decision.consumed_business_event_id = business_event.id
        decision.consumed_final_fingerprint = final_fingerprint
        decision.consumed_at = datetime.now(UTC)
        self.db.flush()

    @staticmethod
    def _final_fingerprint(
        decision: AIApplicationPreviewDecision,
        final_values: dict[str, Any],
    ) -> str:
        return hmac_fingerprint(
            safe_fact_snapshot(final_values),
            key_version=decision.fingerprint_key_version,
            allow_key_creation=False,
        )

    @staticmethod
    def _decision_source(facts: dict[str, Any]) -> str:
        has_rule = bool(str(facts.get("rule_version") or "").strip())
        has_transport_estimate = bool(str(facts.get("transport_estimate_source") or "").strip())
        if has_rule and has_transport_estimate:
            return "hybrid"
        if has_rule:
            return "rule"
        return "heuristic"

    @staticmethod
    def _actor_id(current_user: CurrentUserContext) -> str:
        return str(current_user.employee_id or current_user.username or "").strip()[:120]

    @staticmethod
    def _auth_session_id(current_user: CurrentUserContext) -> str:
        session_id = str(current_user.auth_session_id or "").strip()[:120]
        if not session_id:
            raise ValueError("当前登录会话缺少可验证的会话标识，请重新登录。")
        return session_id

    @staticmethod
    def _required_text(value: object, label: str, limit: int) -> str:
        normalized = str(value or "").strip()[:limit]
        if not normalized:
            raise ValueError(f"{label}不能为空。")
        return normalized

    @staticmethod
    def _normalize_action(action_type: str) -> str:
        normalized = str(action_type or "").strip().lower()
        if normalized not in {"save_draft", "submit"}:
            raise ValueError("不支持的预览决策动作。")
        return normalized

    @staticmethod
    def _as_utc(value: datetime) -> datetime:
        return value if value.tzinfo is not None else value.replace(tzinfo=UTC)
