from datetime import date, datetime, timezone

from sqlalchemy import DateTime, Text
from sqlalchemy.orm import Mapped, mapped_column


def iso(value: datetime | date | None) -> str | None:
    """ISO-8601 for a payload, None for an unset column.

    Every model's to_dict serialises timestamps through this one helper so a
    row read before flush (created_at still None) and a nullable column both
    come out as null instead of raising on `.isoformat()`.
    """
    return value.isoformat() if value else None


class SoftDeleteMixin:
    """Recoverable-delete columns. NULL deleted_at = live row. deleted_batch_id
    groups rows soft-deleted in one operation so a cascade restores as a unit."""
    deleted_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True, default=None
    )
    deleted_batch_id: Mapped[str | None] = mapped_column(Text, nullable=True, default=None)


class TimestampMixin:
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        default=lambda: datetime.now(timezone.utc),
        onupdate=lambda: datetime.now(timezone.utc),
    )


class CreatedAtMixin:
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
    )
