# FILE: src/apps/news/models/news_subscription.py
from __future__ import annotations

import datetime as dt
from typing import Optional
from uuid import UUID

from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column

from src.database.base import BaseModel


class NewsSubscription(BaseModel):
    """
    Tenant-scoped email subscription for cemetery news posts.

    One active subscription is enforced at the database level via a partial
    unique index on (tenant_id, email) WHERE unsubscribed_at IS NULL.  A
    subscriber who unsubscribes and later re-subscribes gets a new row (the
    old row retains its unsubscribed_at timestamp as an audit trail).

    tenant_id is declared explicitly here (rather than via TenantModel) so we
    can attach the ON DELETE CASCADE foreign key to accounts.id, which is
    required to automatically clean up subscriptions when a tenant account is
    deleted.
    """

    __tablename__ = "news_subscriptions"

    # Explicit FK to accounts — enforces referential integrity and CASCADE
    # deletion when the owning tenant account is removed.
    tenant_id: Mapped[UUID] = mapped_column(
        PG_UUID(as_uuid=True),
        ForeignKey("accounts.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
    )

    # RFC 5321 maximum address length is 320 characters.
    email: Mapped[str] = mapped_column(String(320), nullable=False)

    # Cryptographically random token embedded in unsubscribe links.
    # 64-character hex string (32 bytes from secrets.token_hex(32)).
    # UNIQUE constraint is defined at the DB level (see migration + below).
    unsubscribe_token: Mapped[str] = mapped_column(
        String(64), nullable=False, unique=True
    )

    subscribed_at: Mapped[dt.datetime] = mapped_column(
        DateTime(timezone=True), nullable=False
    )

    # Set when the subscriber clicks the unsubscribe link.  NULL means active.
    unsubscribed_at: Mapped[Optional[dt.datetime]] = mapped_column(
        DateTime(timezone=True), nullable=True, default=None
    )

    # ---------------------------------------------------------------------------
    # Table-level constraints
    # ---------------------------------------------------------------------------
    # The partial unique index on (tenant_id, email) WHERE unsubscribed_at IS NULL
    # cannot be expressed as a SQLAlchemy UniqueConstraint (those are always
    # full-table).  The index is created explicitly in the Alembic migration
    # (ix_news_subscriptions_tenant_email_active).  We document it here for
    # clarity but do not duplicate it via __table_args__ to avoid Alembic
    # autogenerate creating a second, full-table unique constraint.

    __table_args__ = (
        # Documented marker so developers know the partial index exists even
        # though it is not expressed as a UniqueConstraint here.
        # The real enforcement is: ix_news_subscriptions_tenant_email_active
        # defined in migration 0033.
        {"comment": "Partial unique index (tenant_id, email) WHERE unsubscribed_at IS NULL enforced via ix_news_subscriptions_tenant_email_active"},
    )

    # ---------------------------------------------------------------------------
    # Helpers
    # ---------------------------------------------------------------------------

    @property
    def is_active(self) -> bool:
        """True when the subscription has not been unsubscribed."""
        return self.unsubscribed_at is None

    def unsubscribe(self) -> None:
        """Soft-cancel the subscription by recording the timestamp."""
        self.unsubscribed_at = dt.datetime.now(dt.timezone.utc)
