from __future__ import annotations

import hashlib
import json
import os
import uuid
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any

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

from app.api.deps import CurrentUserContext
from app.core.logging import get_logger
from app.models.financial_record import ExpenseClaim
from app.models.risk_disposition import RiskDisposition, RiskDispositionEvent
from app.models.risk_observation import RiskObservation, RiskObservationFeedback
from app.schemas.risk_disposition import (
    RiskDispositionActionCreate,
    RiskDispositionEventRead,
    RiskDispositionMutationRead,
    RiskDispositionRead,
)
from app.services.approval_task_projection_refresh import (
    ApprovalTaskProjectionRefreshService,
)
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin
from app.services.risk_disposition_learning import RiskDispositionLearningBridge
from app.services.risk_disposition_release_sync import sync_risk_disposition_release
from app.services.risk_observation_access_policy import RiskObservationAccessPolicy
from app.services.risk_waiver_decision_policy import (
    RISK_WAIVER_DECISION_ACTIONS,
    RiskWaiverDecisionPermissionError,
    RiskWaiverDecisionPolicy,
)

logger = get_logger("app.services.risk_dispositions")

class RiskDispositionConflictError(RuntimeError):
    pass


class RiskDispositionVersionConflictError(RiskDispositionConflictError):
    def __init__(self, current_version: int) -> None:
        self.current_version = current_version
        super().__init__(
            f"Risk disposition version conflict; current version is {current_version}."
        )


class RiskDispositionIdempotencyConflictError(RiskDispositionConflictError):
    pass


class RiskDispositionPermissionError(RuntimeError):
    pass


@dataclass(slots=True)
class RiskDispositionMutation:
    response: RiskDispositionMutationRead
    legacy_feedback: RiskObservationFeedback | None = None

    @property
    def disposition(self) -> RiskDispositionRead:
        return self.response.disposition

    @property
    def event(self) -> RiskDispositionEventRead:
        return self.response.event

    @property
    def replayed(self) -> bool:
        return self.response.replayed


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

    def get_disposition(
        self,
        observation_id: str,
        *,
        tenant_id: str,
    ) -> RiskDisposition | None:
        return self.db.scalar(
            select(RiskDisposition).where(
                RiskDisposition.tenant_id == _tenant(tenant_id),
                RiskDisposition.observation_id == observation_id,
            )
        )

    def get_current_version(self, observation_id: str, *, tenant_id: str) -> int:
        disposition = self.get_disposition(observation_id, tenant_id=tenant_id)
        return disposition.version if disposition is not None else 0

    def execute_action(
        self,
        observation_key_or_id: str,
        payload: RiskDispositionActionCreate,
        *,
        tenant_id: str,
        actor_id: str,
        actor_name: str,
        current_user: CurrentUserContext | None = None,
    ) -> RiskDispositionMutation:
        normalized_tenant = _tenant(tenant_id)
        normalized_actor_id = _text(actor_id) or "anonymous"
        observation_locator = self.db.execute(
            select(RiskObservation.id, RiskObservation.claim_id).where(
                RiskObservation.tenant_id == normalized_tenant,
                (
                    (RiskObservation.id == observation_key_or_id)
                    | (RiskObservation.observation_key == observation_key_or_id)
                ),
            )
        ).one_or_none()
        if observation_locator is None:
            raise LookupError("Risk observation not found.")
        observation_id, located_claim_id = observation_locator
        fingerprint = _payload_fingerprint(
            payload,
            observation_id=observation_id,
            actor_id=normalized_actor_id,
        )
        replay = self._find_replay(
            tenant_id=normalized_tenant,
            request_id=payload.request_id,
            fingerprint=fingerprint,
        )
        if replay is not None:
            sync_risk_disposition_release(
                self.db,
                tenant_id=normalized_tenant,
                disposition_event_id=replay.event.id,
            )
            return replay

        try:
            locked_claim = self._lock_claim(
                str(located_claim_id or "").strip(),
                tenant_id=normalized_tenant,
            )
            observation = self.db.scalar(
                select(RiskObservation)
                .where(
                    RiskObservation.tenant_id == normalized_tenant,
                    RiskObservation.id == observation_id,
                )
                .with_for_update()
            )
            if observation is None:
                raise LookupError("Risk observation not found.")
            if str(observation.claim_id or "").strip() != str(located_claim_id or "").strip():
                raise RiskDispositionConflictError(
                    "Risk observation claim changed concurrently; reload and retry."
                )
            is_waiver_decision = payload.action in RISK_WAIVER_DECISION_ACTIONS
            if (
                not is_waiver_decision
                and current_user is not None
                and not RiskObservationAccessPolicy(
                    self.db
                ).can_manage_locked_disposition(
                    observation,
                    current_user,
                    locked_claim=locked_claim,
                )
            ):
                raise RiskDispositionPermissionError("当前用户已不再是该单据的有效审批人。")

            disposition = self.db.scalar(
                select(RiskDisposition)
                .where(
                    RiskDisposition.tenant_id == normalized_tenant,
                    RiskDisposition.observation_id == observation.id,
                )
                .with_for_update()
            )
            current_version = disposition.version if disposition is not None else 0
            if current_version != payload.expected_version:
                raise RiskDispositionVersionConflictError(current_version)

            mutation_at = datetime.now(UTC)
            if disposition is None:
                disposition = RiskDisposition(
                    id=str(uuid.uuid4()),
                    tenant_id=normalized_tenant,
                    observation_id=observation.id,
                    adjudication=_initial_adjudication(observation),
                    lifecycle_status=_initial_lifecycle_status(observation),
                    version=0,
                    created_at=mutation_at,
                    updated_at=mutation_at,
                )
                self.db.add(disposition)

            waiver_policy = RiskWaiverDecisionPolicy(self.db)
            waiver_actor_id, waiver_actor_name = waiver_policy.actor_identity(
                current_user,
                fallback_id=normalized_actor_id,
                fallback_name=_text(actor_name) or normalized_actor_id,
            )
            _validate_transition(disposition, payload, now=mutation_at)
            if is_waiver_decision:
                waiver_actor_id, waiver_actor_name = waiver_policy.require_decider(
                    disposition,
                    current_user=current_user,
                    tenant_id=normalized_tenant,
                )

            before = _state(disposition)
            _apply_action(
                disposition,
                observation,
                payload,
                actor_id=waiver_actor_id,
                actor_name=waiver_actor_name,
                mutation_at=mutation_at,
            )
            disposition.version = current_version + 1
            disposition.updated_at = mutation_at
            event = RiskDispositionEvent(
                id=str(uuid.uuid4()),
                tenant_id=normalized_tenant,
                disposition_id=disposition.id,
                observation_id=observation.id,
                version=disposition.version,
                action=payload.action,
                actor_id=waiver_actor_id,
                actor_name=waiver_actor_name,
                request_id=payload.request_id,
                payload_fingerprint=fingerprint,
                comment=payload.comment or payload.waiver_reason,
                before_json=before,
                after_json=_state(disposition),
                created_at=mutation_at,
            )
            response = self._build_response_snapshot(
                disposition,
                event,
                prior_version=current_version,
            )
            # PostgreSQL 会拒绝 append-only 事件的后续 UPDATE，因此完整响应
            # 必须和事件在同一条 INSERT 中原子写入。
            event.response_json = response.model_dump(mode="json")
            self.db.add(event)
            legacy_feedback = self._append_safe_feedback(
                observation,
                event,
                payload,
                actor_name=actor_name,
            )
            # 风险事实与审批任务队列投影必须原子可见；autoflush 关闭时先显式
            # 落下 observation/disposition，再按最新风险状态重算开放任务。
            self.db.flush()
            RiskDispositionLearningBridge(self.db).record(
                claim=locked_claim,
                observation=observation,
                event=event,
                action=payload.action,
                actor_id=waiver_actor_id,
            )
            ApprovalTaskProjectionRefreshService(self.db).refresh_claim(
                tenant_id=normalized_tenant,
                claim_id=str(observation.claim_id or "").strip(),
                now=mutation_at,
            )
            self.db.commit()
            self.db.refresh(disposition)
            self.db.refresh(event)
            sync_risk_disposition_release(
                self.db,
                tenant_id=normalized_tenant,
                disposition_event_id=event.id,
            )
            if legacy_feedback is not None:
                self.db.refresh(legacy_feedback)
                self._ingest_feedback_sample(observation, legacy_feedback)
            return RiskDispositionMutation(
                response=response,
                legacy_feedback=legacy_feedback,
            )
        except (
            LookupError,
            RiskDispositionConflictError,
            RiskDispositionPermissionError,
            RiskWaiverDecisionPermissionError,
        ):
            self.db.rollback()
            raise
        except IntegrityError as error:
            self.db.rollback()
            replay = self._find_replay(
                tenant_id=normalized_tenant,
                request_id=payload.request_id,
                fingerprint=fingerprint,
            )
            if replay is not None:
                sync_risk_disposition_release(
                    self.db,
                    tenant_id=normalized_tenant,
                    disposition_event_id=replay.event.id,
                )
                return replay
            raise RiskDispositionConflictError(
                "Risk disposition was changed concurrently; reload and retry."
            ) from error

    def _lock_claim(
        self,
        claim_id: str,
        *,
        tenant_id: str,
    ) -> ExpenseClaim | None:
        if not claim_id:
            return None
        statement = select(ExpenseClaim).where(
            ExpenseClaim.id == claim_id,
            ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant_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 _build_response_snapshot(
        self,
        disposition: RiskDisposition,
        event: RiskDispositionEvent,
        *,
        prior_version: int,
    ) -> RiskDispositionMutationRead:
        prior_events = list(
            self.db.scalars(
                select(RiskDispositionEvent)
                .where(
                    RiskDispositionEvent.tenant_id == disposition.tenant_id,
                    RiskDispositionEvent.disposition_id == disposition.id,
                    RiskDispositionEvent.version <= prior_version,
                )
                .order_by(RiskDispositionEvent.version.asc())
            ).all()
        )
        event_read = RiskDispositionEventRead.model_validate(event)
        event_history = [
            *[RiskDispositionEventRead.model_validate(item) for item in prior_events],
            event_read,
        ]
        disposition_read = RiskDispositionRead(
            id=disposition.id,
            tenant_id=disposition.tenant_id,
            observation_id=disposition.observation_id,
            adjudication=disposition.adjudication,
            lifecycle_status=disposition.lifecycle_status,
            version=disposition.version,
            assignee=disposition.assignee,
            due_at=disposition.due_at,
            resolution=disposition.resolution,
            waiver_requester_id=disposition.waiver_requester_id,
            waiver_requester_name=disposition.waiver_requester_name,
            waiver_requested_at=disposition.waiver_requested_at,
            waiver_reason=disposition.waiver_reason,
            waiver_scope=disposition.waiver_scope,
            waiver_expires_at=disposition.waiver_expires_at,
            waiver_conditions_json=list(disposition.waiver_conditions_json or []),
            waiver_decision=disposition.waiver_decision,
            waiver_decider_id=disposition.waiver_decider_id,
            waiver_decider_name=disposition.waiver_decider_name,
            waiver_decided_at=disposition.waiver_decided_at,
            waiver_decision_reason=disposition.waiver_decision_reason,
            created_at=disposition.created_at,
            updated_at=disposition.updated_at,
            events=event_history,
        )
        return RiskDispositionMutationRead(
            disposition=disposition_read,
            event=event_read,
            replayed=False,
        )

    def _find_replay(
        self,
        *,
        tenant_id: str,
        request_id: str,
        fingerprint: str,
    ) -> RiskDispositionMutation | None:
        event = self.db.scalar(
            select(RiskDispositionEvent).where(
                RiskDispositionEvent.tenant_id == tenant_id,
                RiskDispositionEvent.request_id == request_id,
            )
        )
        if event is None:
            return None
        if event.payload_fingerprint != fingerprint:
            raise RiskDispositionIdempotencyConflictError("request_id 已被不同的风险处置内容使用。")
        response = self._event_response(event)
        feedback = self.db.scalar(
            select(RiskObservationFeedback).where(
                RiskObservationFeedback.observation_id == event.observation_id,
                RiskObservationFeedback.action == f"disposition:{event.id}",
            )
        )
        return RiskDispositionMutation(
            response=response.model_copy(update={"replayed": True}),
            legacy_feedback=feedback,
        )

    def _event_response(
        self,
        event: RiskDispositionEvent,
    ) -> RiskDispositionMutationRead:
        if isinstance(event.response_json, dict) and event.response_json:
            try:
                response = RiskDispositionMutationRead.model_validate(event.response_json)
            except ValidationError as error:
                raise RiskDispositionConflictError(
                    "Stored risk disposition response snapshot is invalid."
                ) from error
            self._validate_snapshot_identity(event, response)
            return response
        return self._rebuild_legacy_event_response(event)

    def _rebuild_legacy_event_response(
        self,
        event: RiskDispositionEvent,
    ) -> RiskDispositionMutationRead:
        # 0012 之前的事件没有响应快照。只读取目标版本及以前的 append-only
        # 事件，并使用目标事件 after_json 重建；禁止读取当前 disposition。
        history = list(
            self.db.scalars(
                select(RiskDispositionEvent)
                .where(
                    RiskDispositionEvent.tenant_id == event.tenant_id,
                    RiskDispositionEvent.disposition_id == event.disposition_id,
                    RiskDispositionEvent.version <= event.version,
                )
                .order_by(RiskDispositionEvent.version.asc())
            ).all()
        )
        target = next((item for item in history if item.id == event.id), None)
        if target is None:
            raise RiskDispositionConflictError(
                "Legacy risk disposition replay event is missing from its audit chain."
            )
        state: dict[str, Any] = dict(event.after_json or {})
        event_reads = [RiskDispositionEventRead.model_validate(item) for item in history]
        target_read = RiskDispositionEventRead.model_validate(target)
        try:
            disposition = RiskDispositionRead(
                id=event.disposition_id,
                tenant_id=event.tenant_id,
                observation_id=event.observation_id,
                adjudication=state.get("adjudication"),
                lifecycle_status=state.get("lifecycle_status"),
                version=event.version,
                assignee=state.get("assignee"),
                due_at=state.get("due_at"),
                resolution=state.get("resolution"),
                waiver_requester_id=state.get("waiver_requester_id"),
                waiver_requester_name=state.get("waiver_requester_name"),
                waiver_requested_at=state.get("waiver_requested_at"),
                waiver_reason=state.get("waiver_reason"),
                waiver_scope=state.get("waiver_scope"),
                waiver_expires_at=state.get("waiver_expires_at"),
                waiver_conditions_json=state.get("waiver_conditions_json") or [],
                waiver_decision=state.get("waiver_decision"),
                waiver_decider_id=state.get("waiver_decider_id"),
                waiver_decider_name=state.get("waiver_decider_name"),
                waiver_decided_at=state.get("waiver_decided_at"),
                waiver_decision_reason=state.get("waiver_decision_reason"),
                created_at=history[0].created_at,
                updated_at=event.created_at,
                events=event_reads,
            )
            response = RiskDispositionMutationRead(
                disposition=disposition,
                event=target_read,
                replayed=False,
            )
        except ValidationError as error:
            raise RiskDispositionConflictError(
                "Legacy risk disposition response cannot be reconstructed safely."
            ) from error
        self._validate_snapshot_identity(event, response)
        return response

    @staticmethod
    def _validate_snapshot_identity(
        event: RiskDispositionEvent,
        response: RiskDispositionMutationRead,
    ) -> None:
        if (
            response.event.id != event.id
            or response.event.request_id != event.request_id
            or response.event.version != event.version
            or response.disposition.id != event.disposition_id
            or response.disposition.observation_id != event.observation_id
            or response.disposition.tenant_id != event.tenant_id
            or response.disposition.version != event.version
            or not any(item.id == event.id for item in response.disposition.events)
            or any(item.version > event.version for item in response.disposition.events)
        ):
            raise RiskDispositionConflictError(
                "Risk disposition response snapshot does not match its immutable event."
            )

    def _append_safe_feedback(
        self,
        observation: RiskObservation,
        event: RiskDispositionEvent,
        payload: RiskDispositionActionCreate,
        *,
        actor_name: str,
    ) -> RiskObservationFeedback | None:
        if payload.action not in {"confirm", "false_positive"}:
            return None
        feedback = RiskObservationFeedback(
            observation_id=observation.id,
            feedback_type=payload.action,
            action=f"disposition:{event.id}",
            actor=_text(actor_name) or "anonymous",
            comment=payload.comment,
            payload_json={
                "decision": payload.action,
                "source": "typed_risk_disposition",
            },
        )
        self.db.add(feedback)
        return feedback

    def _ingest_feedback_sample(
        self,
        observation: RiskObservation,
        feedback: RiskObservationFeedback,
    ) -> None:
        enabled = os.environ.get("FEW_SHOT_INJECTION_ENABLED", "true")
        if enabled.strip().lower() in {"0", "false", "no"}:
            return
        try:
            from app.services.few_shot_ingestion import FewShotIngestionService

            FewShotIngestionService(self.db).ingest_observation_feedback(
                observation,
                feedback,
            )
        except Exception:
            logger.exception(
                "few-shot ingestion failed for disposition event %s",
                feedback.action,
            )


def _apply_action(
    disposition: RiskDisposition,
    observation: RiskObservation,
    payload: RiskDispositionActionCreate,
    *,
    actor_id: str,
    actor_name: str,
    mutation_at: datetime,
) -> None:
    if payload.action == "confirm":
        disposition.adjudication = "confirmed"
        observation.status = "confirmed"
        observation.feedback_status = "confirmed"
        return
    if payload.action == "false_positive":
        disposition.adjudication = "false_positive"
        observation.status = "false_positive"
        observation.feedback_status = "false_positive"
        return
    if payload.action == "request_waiver":
        disposition.lifecycle_status = "waiver_requested"
        disposition.waiver_requester_id = actor_id
        disposition.waiver_requester_name = actor_name
        disposition.waiver_requested_at = mutation_at
        disposition.waiver_reason = payload.waiver_reason
        disposition.waiver_scope = payload.waiver_scope
        disposition.waiver_expires_at = payload.waiver_expires_at
        disposition.waiver_conditions_json = list(payload.waiver_conditions)
        disposition.waiver_decision = None
        disposition.waiver_decider_id = None
        disposition.waiver_decider_name = None
        disposition.waiver_decided_at = None
        disposition.waiver_decision_reason = None
        disposition.resolution = None
        if payload.assignee is not None:
            disposition.assignee = payload.assignee
        if payload.due_at is not None:
            disposition.due_at = payload.due_at
        return
    if payload.action == "approve_waiver":
        disposition.lifecycle_status = "waived"
        disposition.waiver_decision = "approved"
        disposition.waiver_decider_id = actor_id
        disposition.waiver_decider_name = actor_name
        disposition.waiver_decided_at = mutation_at
        disposition.waiver_decision_reason = payload.comment
        observation.status = "waived"
        return
    if payload.action == "reject_waiver":
        disposition.lifecycle_status = "waiver_rejected"
        disposition.waiver_decision = "rejected"
        disposition.waiver_decider_id = actor_id
        disposition.waiver_decider_name = actor_name
        disposition.waiver_decided_at = mutation_at
        disposition.waiver_decision_reason = payload.comment
        observation.status = "confirmed"
        return

    lifecycle_by_action = {
        "request_supplement": "supplement_requested",
        "start_remediation": "remediation_in_progress",
        "resolve": "resolved",
    }
    disposition.lifecycle_status = lifecycle_by_action[payload.action]
    if payload.assignee is not None:
        disposition.assignee = payload.assignee
    if payload.due_at is not None:
        disposition.due_at = payload.due_at
    if payload.action != "resolve":
        disposition.resolution = None
    else:
        disposition.resolution = payload.resolution
        observation.status = "resolved"


def _validate_transition(
    disposition: RiskDisposition,
    payload: RiskDispositionActionCreate,
    *,
    now: datetime,
) -> None:
    if payload.action in RISK_WAIVER_DECISION_ACTIONS:
        if disposition.lifecycle_status != "waiver_requested":
            raise RiskDispositionConflictError("只有待决定的豁免申请才能批准或拒绝。")
        if not _text(disposition.waiver_requester_id):
            raise RiskDispositionConflictError(
                "历史豁免申请缺少职责分离所需的申请人身份，请重新提交申请。"
            )
        if payload.action == "approve_waiver":
            expires_at = _as_utc(disposition.waiver_expires_at)
            if expires_at is None or expires_at <= _as_utc(now):
                raise RiskDispositionConflictError("风险豁免申请已过期，不能批准。")
        return
    if disposition.lifecycle_status == "resolved":
        raise RiskDispositionConflictError("已解决的风险不能再次变更裁决或处置生命周期。")
    if payload.action == "confirm":
        if disposition.adjudication == "confirmed":
            raise RiskDispositionConflictError("该风险已经确认成立，请勿重复提交。")
        return
    if payload.action == "false_positive":
        if disposition.adjudication == "false_positive":
            raise RiskDispositionConflictError("该风险已经标记为误报，请勿重复提交。")
        return
    target_lifecycle = {
        "request_supplement": "supplement_requested",
        "start_remediation": "remediation_in_progress",
        "request_waiver": "waiver_requested",
        "resolve": "resolved",
    }[payload.action]
    if disposition.lifecycle_status == target_lifecycle:
        if payload.action == "request_waiver" and not _text(
            disposition.waiver_requester_id
        ):
            # 0014 前的旧申请没有申请人和有效期，允许以完整协议重新提交。
            pass
        else:
            raise RiskDispositionConflictError("该风险已处于目标处置状态，请勿重复提交。")
    if payload.action == "request_waiver":
        expires_at = _as_utc(payload.waiver_expires_at)
        if expires_at is None or expires_at <= _as_utc(now):
            raise RiskDispositionConflictError("风险豁免有效期必须晚于申请时间。")
    if payload.action == "request_supplement":
        return
    if disposition.adjudication != "confirmed":
        raise RiskDispositionConflictError("风险必须先确认成立，才能启动整改、申请豁免或标记解决。")


def _state(disposition: RiskDisposition) -> dict[str, object]:
    return {
        "adjudication": disposition.adjudication,
        "lifecycle_status": disposition.lifecycle_status,
        "assignee": disposition.assignee,
        "due_at": disposition.due_at.isoformat() if disposition.due_at else None,
        "resolution": disposition.resolution,
        "waiver_requester_id": disposition.waiver_requester_id,
        "waiver_requester_name": disposition.waiver_requester_name,
        "waiver_requested_at": (
            disposition.waiver_requested_at.isoformat()
            if disposition.waiver_requested_at
            else None
        ),
        "waiver_reason": disposition.waiver_reason,
        "waiver_scope": disposition.waiver_scope,
        "waiver_expires_at": (
            disposition.waiver_expires_at.isoformat()
            if disposition.waiver_expires_at
            else None
        ),
        "waiver_conditions_json": list(disposition.waiver_conditions_json or []),
        "waiver_decision": disposition.waiver_decision,
        "waiver_decider_id": disposition.waiver_decider_id,
        "waiver_decider_name": disposition.waiver_decider_name,
        "waiver_decided_at": (
            disposition.waiver_decided_at.isoformat()
            if disposition.waiver_decided_at
            else None
        ),
        "waiver_decision_reason": disposition.waiver_decision_reason,
        "version": disposition.version,
    }


def _initial_adjudication(observation: RiskObservation) -> str:
    values = {
        _text(observation.feedback_status).lower(),
        _text(observation.status).lower(),
    }
    if "false_positive" in values:
        return "false_positive"
    if "confirmed" in values:
        return "confirmed"
    return "unreviewed"


def _initial_lifecycle_status(observation: RiskObservation) -> str:
    return "resolved" if _text(observation.status).lower() == "resolved" else "open"


def _payload_fingerprint(
    payload: RiskDispositionActionCreate,
    *,
    observation_id: str,
    actor_id: str,
) -> str:
    canonical_payload = payload.model_dump(
        mode="json",
        exclude={"request_id"},
        exclude_none=False,
    )
    canonical_payload["observation_id"] = observation_id
    canonical_payload["actor_id"] = actor_id
    canonical = json.dumps(
        canonical_payload,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def _tenant(value: str) -> str:
    return _text(value) or "default"


def _text(value: object) -> str:
    return str(value or "").strip()


def _as_utc(value: datetime | None) -> datetime | None:
    if value is None:
        return None
    if value.tzinfo is None:
        return value.replace(tzinfo=UTC)
    return value.astimezone(UTC)
