"""Guest, Invitation, Rsvp and RsvpHistory (PRD §4.2, §4.3).

The Invitation (guest x event) is the unit of tracking, not the Guest: a guest invited
to all three events holds three invitations and may accept two and decline one.
"""

import uuid
from datetime import datetime
from typing import TYPE_CHECKING

from sqlalchemy import (
    ARRAY,
    Boolean,
    CheckConstraint,
    ForeignKey,
    Index,
    Integer,
    String,
    Text,
    UniqueConstraint,
    func,
    text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.models.base import Base, TimestampTZ, created_at_col, pg_enum, updated_at_col, uuid_pk

if TYPE_CHECKING:
    from app.models.wedding import Event

from app.models.enums import (
    GuestSide,
    GuestSource,
    HistoryActor,
    InvitationStatus,
    InvitationType,
    InvitedVia,
    Locale,
    PreferredChannel,
    RsvpResponse,
)


class Guest(Base):
    """One person. Uniqueness on phone and lowercased email prevents duplicates from
    CSV re-import and QR self-registration (spec guest-management)."""

    __tablename__ = "guest"
    __table_args__ = (
        # Uniqueness is per EVENT, not per wedding (design D11). The same person attending
        # three ceremonies is three records, so their phone legitimately appears three times
        # — once under each event, never twice under one.
        Index(
            "uq_guest_event_phone",
            "event_id",
            "phone_e164",
            unique=True,
            postgresql_where=text("phone_e164 IS NOT NULL AND is_deleted = false"),
        ),
        Index(
            "uq_guest_event_email",
            "event_id",
            func.lower(text("email")),
            unique=True,
            postgresql_where=text("email IS NOT NULL AND is_deleted = false"),
        ),
        CheckConstraint(
            "phone_e164 IS NOT NULL OR email IS NOT NULL",
            name="ck_guest_has_contact",
        ),
    )

    id: Mapped[uuid.UUID] = uuid_pk()
    wedding_id: Mapped[uuid.UUID] = mapped_column(
        ForeignKey("wedding.id", ondelete="CASCADE"), nullable=False, index=True
    )
    #: The event this guest belongs to. A guest belongs to exactly one; attending several
    #: ceremonies means several records (design D11).
    event_id: Mapped[uuid.UUID] = mapped_column(
        ForeignKey("event.id", ondelete="CASCADE"), nullable=False, index=True
    )
    full_name: Mapped[str] = mapped_column(Text, nullable=False)
    email: Mapped[str | None] = mapped_column(String(320))
    phone_e164: Mapped[str | None] = mapped_column(String(20))
    whatsapp_phone_e164: Mapped[str | None] = mapped_column(String(20))

    side: Mapped[GuestSide] = mapped_column(
        pg_enum(GuestSide, "guest_side"), default=GuestSide.COMMON, nullable=False
    )
    group_tag: Mapped[list[str]] = mapped_column(
        ARRAY(Text), default=list, server_default="{}", nullable=False
    )
    preferred_locale: Mapped[Locale] = mapped_column(
        pg_enum(Locale, "locale"), default=Locale.EN, nullable=False
    )
    preferred_channel: Mapped[PreferredChannel] = mapped_column(
        pg_enum(PreferredChannel, "preferred_channel"),
        default=PreferredChannel.AUTO,
        nullable=False,
    )
    source: Mapped[GuestSource] = mapped_column(
        pg_enum(GuestSource, "guest_source"), default=GuestSource.MANUAL, nullable=False
    )
    #: Selects which greeting sentence the invitation opens with (design D6). The server
    #: default is what makes every guest created before this column existed valid without a
    #: backfill — and `single` is the safe reading of an unknown invitation.
    invitation_type: Mapped[InvitationType] = mapped_column(
        pg_enum(InvitationType, "invitation_type"),
        default=InvitationType.SINGLE,
        server_default=InvitationType.SINGLE.value,
        nullable=False,
    )

    # Set by unsubscribe (spec messaging). The invitation link keeps working.
    do_not_contact: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
    # Set by a hard bounce so future sends skip instead of wasting attempts.
    email_invalid: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)

    is_deleted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
    created_at: Mapped[datetime] = created_at_col()
    updated_at: Mapped[datetime] = updated_at_col()

    invitations: Mapped[list["Invitation"]] = relationship(
        back_populates="guest", passive_deletes=True
    )
    event: Mapped["Event"] = relationship(back_populates="guests")


class Invitation(Base):
    """Carries the bearer token. guest_id is nullable for open-link visits before
    registration (PRD §4.2)."""

    __tablename__ = "invitation"
    __table_args__ = (
        UniqueConstraint("event_id", "guest_id", name="uq_invitation_event_guest"),
        Index("ix_invitation_event_status", "event_id", "status"),
    )

    id: Mapped[uuid.UUID] = uuid_pk()
    event_id: Mapped[uuid.UUID] = mapped_column(
        ForeignKey("event.id", ondelete="CASCADE"), nullable=False, index=True
    )
    guest_id: Mapped[uuid.UUID | None] = mapped_column(
        ForeignKey("guest.id", ondelete="CASCADE"), index=True
    )

    # 22-char URL-safe, 128-bit (design D11). Never logged in full.
    token: Mapped[str] = mapped_column(String(32), unique=True, nullable=False)
    # 6-char human-typeable fallback printed under the QR.
    short_code: Mapped[str] = mapped_column(String(6), unique=True, nullable=False)

    status: Mapped[InvitationStatus] = mapped_column(
        pg_enum(InvitationStatus, "invitation_status"),
        default=InvitationStatus.PENDING,
        nullable=False,
    )
    # The RSVP form offers 1-4, so the ceiling defaults to 4: anything lower and a guest's
    # pick is silently clamped down (see tests/test_party_size.py). Hosts can still lower
    # or raise it per invitation.
    max_guests: Mapped[int] = mapped_column(Integer, default=4, nullable=False)

    opened_at: Mapped[datetime | None] = mapped_column(TimestampTZ)
    responded_at: Mapped[datetime | None] = mapped_column(TimestampTZ)
    cancelled_at: Mapped[datetime | None] = mapped_column(TimestampTZ)
    open_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)

    invited_via: Mapped[InvitedVia] = mapped_column(
        pg_enum(InvitedVia, "invited_via"), default=InvitedVia.LINK, nullable=False
    )
    invited_at: Mapped[datetime | None] = mapped_column(TimestampTZ)

    created_at: Mapped[datetime] = created_at_col()
    updated_at: Mapped[datetime] = updated_at_col()

    guest: Mapped[Guest | None] = relationship(back_populates="invitations")
    rsvp: Mapped["Rsvp | None"] = relationship(
        back_populates="invitation", uselist=False, passive_deletes=True
    )
    # Every read path eager-loads this; without it `selectinload(Invitation.event)` raises
    # AttributeError at request time rather than at import, so it is easy to miss.
    event: Mapped["Event"] = relationship(back_populates="invitations")


class Rsvp(Base):
    """One current answer per invitation — the unique FK is what makes double submission
    idempotent (spec rsvp-flow FR-2.9)."""

    __tablename__ = "rsvp"

    id: Mapped[uuid.UUID] = uuid_pk()
    invitation_id: Mapped[uuid.UUID] = mapped_column(
        ForeignKey("invitation.id", ondelete="CASCADE"), unique=True, nullable=False
    )

    response: Mapped[RsvpResponse] = mapped_column(
        pg_enum(RsvpResponse, "rsvp_response"), nullable=False
    )
    name: Mapped[str] = mapped_column(Text, nullable=False)
    email: Mapped[str | None] = mapped_column(String(320))
    phone_e164: Mapped[str | None] = mapped_column(String(20))

    party_size: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
    adults: Mapped[int | None] = mapped_column(Integer)
    children: Mapped[int | None] = mapped_column(Integer)

    message_to_couple: Mapped[str | None] = mapped_column(String(500))
    dietary_notes: Mapped[str | None] = mapped_column(Text)
    cancel_reason: Mapped[str | None] = mapped_column(Text)

    responded_at: Mapped[datetime] = mapped_column(TimestampTZ, nullable=False)

    # Abuse detection only — never displayed (PRD §4.2).
    ip_hash: Mapped[str | None] = mapped_column(String(64))
    user_agent: Mapped[str | None] = mapped_column(Text)

    created_at: Mapped[datetime] = created_at_col()
    updated_at: Mapped[datetime] = updated_at_col()

    invitation: Mapped[Invitation] = relationship(back_populates="rsvp")


class RsvpHistory(Base):
    """Append-only. A cancellation must not erase the fact that they once accepted."""

    __tablename__ = "rsvp_history"

    id: Mapped[uuid.UUID] = uuid_pk()
    invitation_id: Mapped[uuid.UUID] = mapped_column(
        ForeignKey("invitation.id", ondelete="CASCADE"), nullable=False, index=True
    )
    from_status: Mapped[InvitationStatus | None] = mapped_column(
        pg_enum(InvitationStatus, "invitation_status")
    )
    to_status: Mapped[InvitationStatus] = mapped_column(
        pg_enum(InvitationStatus, "invitation_status"), nullable=False
    )
    party_size: Mapped[int | None] = mapped_column(Integer)
    actor: Mapped[HistoryActor] = mapped_column(
        pg_enum(HistoryActor, "history_actor"), nullable=False
    )
    actor_admin_id: Mapped[uuid.UUID | None] = mapped_column(
        ForeignKey("admin_user.id", ondelete="SET NULL")
    )
    source: Mapped[str | None] = mapped_column(String(40))
    created_at: Mapped[datetime] = created_at_col()
