"""Response models for the guest-facing invitation routes.

Two payload shapes exist on purpose:

* `TokenInvitationRead` includes the guest's own contact details, because holding the
  token is what authorizes seeing them — that is how the form pre-fills.
* `OpenEventRead` carries no personal data at all. It backs `/e/{slug}`, which anyone who
  scans a printed QR can reach, and task 8.2 asserts no phone or email ever appears there.

Both carry `title_bn` and `title_en` rather than a pre-resolved string, because the guest
can toggle language on the page (design D13).
"""

import uuid
from datetime import datetime

from pydantic import BaseModel, ConfigDict

from app.models.enums import (
    CardRenderer,
    EventType,
    InvitationStatus,
    InvitationType,
    Locale,
    RsvpResponse,
)


class EventRead(BaseModel):
    """Public event detail. Everything here is printed on a card anyway."""

    model_config = ConfigDict(from_attributes=True)

    id: uuid.UUID
    type: EventType
    slug: str
    title_bn: str
    title_en: str
    starts_at: datetime
    ends_at: datetime | None
    venue_name: str
    venue_address: str
    map_url: str | None
    dress_code: str | None
    notes: str | None
    #: The host block at the foot of the invitation (design D8). Public on purpose: this is
    #: the name and number a guest is meant to read and call, so it is printed on the card
    #: as well. Not guest data — `/e/{slug}` may carry it.
    host_name_1: str
    host_name_2: str | None
    host_phone: str
    theme_key: str
    cover_image_url: str | None
    music_url: str | None
    rsvp_deadline: datetime | None


class CoupleRead(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    bride_name: str
    groom_name: str
    default_locale: Locale
    host_contact_phone: str | None


class GreetingRead(BaseModel):
    """The header sentence, resolved server-side (design D9, task 4.3).

    Both locales are returned rather than one, because the guest can toggle language on the
    page without a round trip (design D13). Neither is ever empty: resolution falls through
    to a built-in default, so the frontend has no empty-greeting branch to get wrong.
    """

    invitation_type: InvitationType
    message_en: str
    message_bn: str


class CardRead(BaseModel):
    """The published card design, resolved for rendering (design D1, D3).

    Carries no guest data of any kind. The card is fixed for the event — the guest's name
    lives in the header above it and nowhere else (spec invitation-presentation), which is
    also what lets the same document be served to every guest.
    """

    renderer: CardRenderer
    #: `html` renderer: the sanitised, reference-rewritten document, injected into a shadow
    #: root so its styles and the page's cannot reach each other.
    document: str | None = None
    #: `image` renderer.
    image_url: str | None = None
    alt_en: str = ""
    alt_bn: str = ""
    width: int | None = None
    height: int | None = None


class PreviewImageRead(BaseModel):
    """Absolute URL and the dimensions the meta tags declare."""

    url: str
    width: int
    height: int


class LinkPreviewRead(BaseModel):
    """What a link to this invitation says when it is shown somewhere else (design D5).

    Guest-free on both routes, deliberately. These fields end up in meta tags that a chat
    application fetches and caches, so they must be identical for every guest invited to the
    event and derivable from public event information alone (design D3). The guest's name
    reaches their email and the admin panel by a different path — never this one.
    """

    title: str
    description: str
    #: Always the event's public page, so a platform caches and attributes the preview
    #: against that rather than against a tokenized URL.
    canonical_url: str
    site_name: str
    #: Absent when the event has no published card, or has one published without a picture.
    #: The page then declares no image rather than pointing at one that does not exist.
    image: PreviewImageRead | None


class GuestPrefill(BaseModel):
    """Only what the RSVP form needs to pre-fill (FR-2.5). Never sent on the open route."""

    full_name: str
    email: str | None
    phone_e164: str | None
    preferred_locale: Locale


class CurrentRsvpRead(BaseModel):
    """The guest's existing answer, so a returning visitor sees it instead of a blank
    form (FR-1.12)."""

    model_config = ConfigDict(from_attributes=True)

    response: RsvpResponse
    party_size: int
    message_to_couple: str | None
    dietary_notes: str | None
    responded_at: datetime


class TokenInvitationRead(BaseModel):
    """Payload for `/i/{token}`."""

    invitation_id: uuid.UUID
    status: InvitationStatus
    max_guests: int
    short_code: str
    event: EventRead
    couple: CoupleRead
    guest: GuestPrefill | None
    greeting: GreetingRead
    #: None when the event has no published design; the page then renders its plain
    #: composition rather than an empty frame.
    card: CardRead | None
    #: Guest-free on purpose — see `LinkPreviewRead`. Present even when the event has no
    #: card, because a link with no picture still previews with words.
    preview: LinkPreviewRead
    current_rsvp: CurrentRsvpRead | None

    # There is deliberately no list of the guest's other invitations. A guest record belongs
    # to one event (design D11), so "the same guest at another event" is a different record
    # that this one cannot be linked to without guessing at an identity match (task 3.7).

    # Page state, resolved server-side so the frontend never re-derives these rules.
    rsvp_open: bool
    can_cancel: bool


class OpenEventRead(BaseModel):
    """Payload for `/e/{slug}` — no personal data, cacheable, identical for everyone."""

    event: EventRead
    couple: CoupleRead
    greeting: GreetingRead
    card: CardRead | None
    preview: LinkPreviewRead
    rsvp_open: bool
    # Which events a walk-in may register for, since the open form asks (FR-2.10).
    selectable_events: list[EventRead]
