"""RSVP write routes (tasks 2.7-2.11).

Cancellation is POST-only and requires an explicit confirmation flag in the body. A
WhatsApp or email link-preview crawler issues a GET with no body, so it cannot cancel an
RSVP by accident (spec cancellation FR-3.3) — `test_cancel_get_is_inert` holds that line.
"""

import logging
from urllib.parse import quote

from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.config import get_settings
from app.db import get_session
from app.models import Event, Guest, Invitation, Rsvp
from app.models.enums import InvitedVia
from app.schemas.rsvp import (
    OpenRsvpConfirmation,
    OpenRsvpRequest,
    RsvpAcceptRequest,
    RsvpCancelRequest,
    RsvpConfirmation,
    RsvpDeclineRequest,
)
from app.services import lifecycle, rate_limit, rsvp_service, turnstile
from app.services.tokens import mask_token

logger = logging.getLogger(__name__)
router = APIRouter(tags=["rsvp"])


def _client_ip(request: Request) -> str | None:
    # Caddy sits in front and sets X-Forwarded-For; fall back to the socket peer.
    forwarded = request.headers.get("x-forwarded-for")
    if forwarded:
        return forwarded.split(",")[0].strip()
    return request.client.host if request.client else None


def _not_found() -> HTTPException:
    return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invitation not found")


async def _load_invitation(session: AsyncSession, token: str) -> Invitation:
    invitation = await session.scalar(
        select(Invitation)
        .where(Invitation.token == token)
        .options(
            selectinload(Invitation.guest),
            selectinload(Invitation.rsvp),
            selectinload(Invitation.event).selectinload(Event.wedding),
        )
    )
    if invitation is None or not invitation.event.is_published:
        logger.info("rsvp write miss for token %s", mask_token(token))
        raise _not_found()
    return invitation


def _calendar_url(base_url: str, token: str) -> str:
    return f"{base_url.rstrip('/')}/api/ics/{quote(token)}"


def _confirmation(
    invitation: Invitation, rsvp: Rsvp | None, *, email_queued: bool
) -> RsvpConfirmation:
    """The Rsvp is passed in rather than read off the relationship: touching
    `invitation.rsvp` lazy-loads, which the async engine cannot do here."""
    settings = get_settings()
    _, cancel_url = rsvp_service.build_urls(settings.app_base_url, invitation.token)
    return RsvpConfirmation(
        invitation_id=invitation.id,
        status=invitation.status,
        response=rsvp.response if rsvp else None,
        party_size=rsvp.party_size if rsvp else None,
        event_title_en=invitation.event.title_en,
        event_title_bn=invitation.event.title_bn,
        starts_at=invitation.event.starts_at,
        venue_name=invitation.event.venue_name,
        cancel_url=cancel_url,
        calendar_url=_calendar_url(settings.app_base_url, invitation.token),
        confirmation_email_queued=email_queued,
    )


async def _queue_confirmation(invitation: Invitation, rsvp: Rsvp | None) -> bool:
    """Queue the acceptance confirmation email.

    Wired up in Phase 3 (task 4.7), which writes the message_job row and fires an immediate
    attempt via BackgroundTasks. Returns whether an email is expected, which the success
    screen uses to decide what to promise the guest.
    """
    guest_email = rsvp.email if rsvp is not None else None
    if not guest_email and invitation.guest is not None:
        guest_email = invitation.guest.email
    return bool(guest_email)


@router.post("/rsvp/{token}/accept", response_model=RsvpConfirmation)
async def accept_invitation(
    token: str,
    payload: RsvpAcceptRequest,
    request: Request,
    session: AsyncSession = Depends(get_session),
) -> RsvpConfirmation:
    invitation = await _load_invitation(session, token)

    if not lifecycle.is_rsvp_open(invitation.event.rsvp_deadline):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT, detail="RSVP has closed for this event"
        )

    if not await rate_limit.check_and_increment(session, _client_ip(request)):
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail="Too many submissions. Please try again shortly.",
        )

    try:
        rsvp = await rsvp_service.accept(
            session,
            invitation,
            name=payload.name,
            phone=payload.phone,
            email=payload.email,
            party_size=payload.party_size,
            adults=payload.adults,
            children=payload.children,
            message_to_couple=payload.message_to_couple,
            dietary_notes=payload.dietary_notes,
            ip=_client_ip(request),
            user_agent=request.headers.get("user-agent"),
        )
    except lifecycle.InvalidTransitionError as exc:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc

    return _confirmation(invitation, rsvp, email_queued=await _queue_confirmation(invitation, rsvp))


@router.post("/rsvp/{token}/decline", response_model=RsvpConfirmation)
async def decline_invitation(
    token: str,
    payload: RsvpDeclineRequest,
    session: AsyncSession = Depends(get_session),
) -> RsvpConfirmation:
    invitation = await _load_invitation(session, token)
    try:
        rsvp = await rsvp_service.decline(session, invitation, reason=payload.reason)
    except lifecycle.InvalidTransitionError as exc:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
    return _confirmation(invitation, rsvp, email_queued=False)


@router.post("/rsvp/{token}/cancel", response_model=RsvpConfirmation)
async def cancel_invitation(
    token: str,
    payload: RsvpCancelRequest,
    session: AsyncSession = Depends(get_session),
) -> RsvpConfirmation:
    """POST-only, and the body must carry `confirm: true`.

    There is no GET counterpart by design: link scanners must not be able to cancel.
    """
    invitation = await _load_invitation(session, token)

    if not lifecycle.is_cancellable(invitation, invitation.event.starts_at):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="This RSVP can no longer be cancelled. Please contact the host.",
        )

    rsvp = await rsvp_service.cancel(session, invitation, reason=payload.reason)
    return _confirmation(invitation, rsvp, email_queued=False)


@router.post("/rsvp/{token}/reaccept", response_model=RsvpConfirmation)
async def reaccept_invitation(
    token: str,
    payload: RsvpAcceptRequest,
    request: Request,
    session: AsyncSession = Depends(get_session),
) -> RsvpConfirmation:
    """'Changed your mind?' after a cancellation or decline (FR-3.6)."""
    return await accept_invitation(token, payload, request, session)


@router.post("/events/{slug}/rsvp", response_model=OpenRsvpConfirmation)
async def open_link_rsvp(
    slug: str,
    payload: OpenRsvpRequest,
    request: Request,
    session: AsyncSession = Depends(get_session),
) -> OpenRsvpConfirmation:
    """Walk-in registration from a printed QR or shared link (FR-2.10, FR-2.11).

    Turnstile applies to this route only — the tokenized flow stays frictionless.
    """
    ip = _client_ip(request)
    if not await rate_limit.check_and_increment(session, ip):
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail="Too many submissions. Please try again shortly.",
        )

    # Verified before any lookup or write. A widget rendered in the browser proves nothing
    # until this server has exchanged the token with Cloudflare — skipping this call is a
    # bot defence that only looks like one.
    if not await turnstile.verify(payload.turnstile_token, remote_ip=ip):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="We could not verify that this came from a browser. Please try again.",
        )

    landing = await session.scalar(
        select(Event)
        .where(Event.slug == slug, Event.is_published.is_(True))
        .options(selectinload(Event.wedding))
    )
    if landing is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Event not found")

    all_events = list(
        await session.scalars(select(Event).where(Event.wedding_id == landing.wedding_id))
    )
    chosen = rsvp_service.resolve_events(all_events, payload.event_types)
    if not chosen:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="Select at least one event you are responding to",
        )

    # One registration per chosen event: a guest record belongs to exactly one event
    # (design D11), so ticking two ceremonies creates two records rather than one guest
    # holding two invitations. Each pass merges against that event's own list.
    merged = False
    registrations: list[tuple[Guest, Event, Invitation]] = []
    for event in chosen:
        guest, event_merged, invitation = await rsvp_service.register_open_guest(
            session,
            event,
            name=payload.name,
            phone=payload.phone,
            email=payload.email,
            invited_via=InvitedVia.PRINT_QR,
        )
        merged = merged or event_merged
        registrations.append((guest, event, invitation))

    confirmations: list[RsvpConfirmation] = []
    for guest, event, invitation in registrations:
        invitation.event = event
        invitation.guest = guest
        rsvp = await rsvp_service.accept(
            session,
            invitation,
            name=payload.name,
            phone=payload.phone,
            email=payload.email,
            party_size=payload.party_size,
            adults=payload.adults,
            children=payload.children,
            message_to_couple=payload.message_to_couple,
            dietary_notes=payload.dietary_notes,
            ip=ip,
            user_agent=request.headers.get("user-agent"),
            source="open_link",
        )
        confirmations.append(_confirmation(invitation, rsvp, email_queued=bool(payload.email)))

    # There is now one guest record per chosen event, so "the" guest id is ambiguous. Report
    # the record for the event whose link they actually scanned, falling back to the first
    # when they responded only for other ceremonies.
    primary = next((g for g, e, _ in registrations if e.id == landing.id), registrations[0][0])
    return OpenRsvpConfirmation(
        guest_id=primary.id,
        merged_into_existing_guest=merged,
        confirmations=confirmations,
    )
