"""ORM tables for the account domain (data/accounts.sqlite).

``User`` / ``AccessToken`` are fastapi-users' base tables (UUID primary keys);
the access-token table backs a database session strategy, so logins survive a
process restart and are individually revocable. ``Subscription`` and
``Notification`` are our own, keyed to a user and cascading on delete.
"""
import time
import uuid

from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID
from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyBaseAccessTokenTableUUID
from fastapi_users_db_sqlalchemy.generics import GUID
from sqlalchemy import (
    JSON,
    Boolean,
    CheckConstraint,
    Float,
    ForeignKey,
    Index,
    Integer,
    String,
    Text,
    UniqueConstraint,
    false,
)
from sqlalchemy.orm import Mapped, mapped_column

from accounts.db import Base


class User(SQLAlchemyBaseUserTableUUID, Base):
    # Inherits id (UUID), email (unique), hashed_password, is_active,
    # is_superuser, is_verified. Optional extras:
    display_name: Mapped[str | None] = mapped_column(String(120), nullable=True)
    # Linked Discord account id (OAuth2 identify) — the key DM alerts reach the user
    # by. NB: create_all only makes this on a fresh DB; an existing prod accounts.db
    # needs the manual migration in deploy/migrations/ (no Alembic in this project).
    discord_id: Mapped[str | None] = mapped_column(String(32), unique=True, nullable=True)
    # Whether to also deliver alerts as a Discord DM. Set True on linking (an active
    # opt-in); the user can mute it while staying linked. Same migration caveat.
    discord_dm: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False,
                                             server_default=false())


class AccessToken(SQLAlchemyBaseAccessTokenTableUUID, Base):
    # Inherits token (PK), user_id (FK -> user.id), created_at. Rows here ARE the
    # sessions: DatabaseStrategy looks a cookie's token up in this table.
    pass


class Subscription(Base):
    __tablename__ = "subscription"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    user_id: Mapped[uuid.UUID] = mapped_column(
        GUID, ForeignKey("user.id", ondelete="CASCADE"), nullable=False
    )
    # grid.snap(lat, lon)["id"] — the stable per-location key used across the app.
    cell_id: Mapped[str] = mapped_column(String(40), nullable=False)
    label: Mapped[str | None] = mapped_column(String(200), nullable=True)
    lat: Mapped[float] = mapped_column(Float, nullable=False)
    lon: Mapped[float] = mapped_column(Float, nullable=False)
    # Unusualness cutoff the user picked; the low tail mirrors it at 100-threshold.
    threshold: Mapped[int] = mapped_column(Integer, nullable=False)
    # Grading metric keys this subscription watches, e.g. ["tmax", "feels", "precip"].
    metrics: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
    # 'observed' (a recorded day crossed) or 'forecast' (an upcoming day is projected to).
    kind: Mapped[str] = mapped_column(String(16), nullable=False, default="observed")
    # Also alert the cold/low tail for temperature-like metrics (precip stays one-sided).
    two_sided: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
    active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
    # Epoch seconds of the last notification emitted — enforces the weekly cap.
    last_notified_at: Mapped[float | None] = mapped_column(Float, nullable=True)
    created_at: Mapped[float] = mapped_column(Float, nullable=False, default=time.time)

    __table_args__ = (
        CheckConstraint("threshold BETWEEN 95 AND 99", name="ck_sub_threshold"),
        CheckConstraint("kind IN ('observed','forecast')", name="ck_sub_kind"),
        # One observed + one forecast subscription per location per user.
        UniqueConstraint("user_id", "cell_id", "kind", name="uq_sub_user_cell_kind"),
        Index("idx_sub_user", "user_id"),
        Index("idx_sub_active", "active"),
    )


class Notification(Base):
    __tablename__ = "notification"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    user_id: Mapped[uuid.UUID] = mapped_column(
        GUID, ForeignKey("user.id", ondelete="CASCADE"), nullable=False
    )
    subscription_id: Mapped[int] = mapped_column(
        Integer, ForeignKey("subscription.id", ondelete="CASCADE"), nullable=False
    )
    event_date: Mapped[str] = mapped_column(String(10), nullable=False)  # YYYY-MM-DD
    metric: Mapped[str] = mapped_column(String(16), nullable=False)
    direction: Mapped[str] = mapped_column(String(4), nullable=False)  # 'high' | 'low'
    kind: Mapped[str] = mapped_column(String(16), nullable=False, default="observed")
    percentile: Mapped[float] = mapped_column(Float, nullable=False)
    value: Mapped[float | None] = mapped_column(Float, nullable=True)
    grade: Mapped[str | None] = mapped_column(String(40), nullable=True)
    title: Mapped[str] = mapped_column(String(200), nullable=False)
    body: Mapped[str | None] = mapped_column(Text, nullable=True)
    # 'inapp' today; the seam for future 'email' / 'push' delivery.
    channel: Mapped[str] = mapped_column(String(16), nullable=False, default="inapp")
    created_at: Mapped[float] = mapped_column(Float, nullable=False, default=time.time)
    read_at: Mapped[float | None] = mapped_column(Float, nullable=True)  # NULL == unread

    __table_args__ = (
        # The dedup key: a given event (day+metric+direction+kind) notifies a
        # subscription at most once, so re-running the evaluator never repeats it.
        UniqueConstraint(
            "subscription_id", "event_date", "metric", "direction", "kind",
            name="uq_notif_event",
        ),
        Index("idx_notif_user_created", "user_id", "created_at"),
        Index("idx_notif_user_read", "user_id", "read_at"),
    )


class PushSubscription(Base):
    """A single browser/device Web Push registration, owned by a user.

    One row per device (a user with a phone + a laptop has two). The `endpoint`
    is the push service URL the browser handed us; it's the natural identity, so
    re-subscribing from the same device updates the keys in place rather than
    duplicating. Rows are pruned when the push service reports the endpoint gone
    (404/410) — see notify.py / api_accounts.py.
    """
    __tablename__ = "push_subscription"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    user_id: Mapped[uuid.UUID] = mapped_column(
        GUID, ForeignKey("user.id", ondelete="CASCADE"), nullable=False
    )
    # The push service URL (per-device). Unique — it identifies the device.
    endpoint: Mapped[str] = mapped_column(Text, nullable=False)
    # The two client keys from PushSubscription.toJSON().keys, used to encrypt the
    # payload so only this device can read it.
    p256dh: Mapped[str] = mapped_column(String(200), nullable=False)
    auth: Mapped[str] = mapped_column(String(100), nullable=False)
    # Best-effort label for a future "manage devices" view.
    user_agent: Mapped[str | None] = mapped_column(String(300), nullable=True)
    created_at: Mapped[float] = mapped_column(Float, nullable=False, default=time.time)
    last_used_at: Mapped[float | None] = mapped_column(Float, nullable=True)

    __table_args__ = (
        UniqueConstraint("endpoint", name="uq_push_endpoint"),
        Index("idx_push_user", "user_id"),
    )


class PendingDigest(Base):
    """A monthly-digest signup, collected before email delivery is wired up.

    The digest form ships ahead of SMTP on purpose: building the list is the
    slow part, and making people wait for the mailer would throw away every
    signup in the meantime. Rows land here unconfirmed; once delivery is live, a
    confirmation pass mails each address and stamps ``confirmed_at``.

    Deliberately NOT tied to ``user`` — signing up for the digest must not
    require an account, and most subscribers won't have one.
    """
    __tablename__ = "pending_digest"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    # 320 = the practical maximum length of an email address (64 local + @ + 255 domain).
    email: Mapped[str] = mapped_column(String(320), nullable=False)
    # The place the digest should cover. Optional: an address with no place is
    # still a real signup, and the place can be asked for at confirmation time.
    place_label: Mapped[str | None] = mapped_column(String(200), nullable=True)
    lat: Mapped[float | None] = mapped_column(Float, nullable=True)
    lon: Mapped[float | None] = mapped_column(Float, nullable=True)
    cell_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
    # Where the signup came from (bare referrer domain), for attribution only.
    source: Mapped[str | None] = mapped_column(String(64), nullable=True)
    created_at: Mapped[float] = mapped_column(Float, nullable=False, default=time.time)
    # Set when the address is verified. The double opt-in token is stored as a
    # sha256 hash, never in the clear, so a leaked database can't confirm addresses.
    token_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
    confirmed_at: Mapped[float | None] = mapped_column(Float, nullable=True)
    last_sent_at: Mapped[float | None] = mapped_column(Float, nullable=True)
    unsubscribed_at: Mapped[float | None] = mapped_column(Float, nullable=True)

    __table_args__ = (
        # One row per address: a re-submit updates in place rather than
        # duplicating, which is what makes the form idempotent.
        UniqueConstraint("email", name="uq_pending_digest_email"),
    )
