"""GuestStay synchronization service.

This service ensures that GuestStay rows exist for every (booking, guest) pair,
with proper idswh generation and lifecycle management.

Architecture:
- GuestStay is the canonical regulatory identity root
- idswh is generated once at creation and never recomputed
- Exports (Ross1000, C59, ISTAT) only READ stored identities
- This service is the ONLY place where GuestStay rows are created/updated/deleted

Usage:
    from guests.services.guest_stay_service import sync_guest_stays_for_booking

    # After creating/updating a booking with guests:
    sync_guest_stays_for_booking(booking)
"""

from __future__ import annotations

import logging
from datetime import date
from typing import TYPE_CHECKING

from django.db import transaction

from guests.models import GuestStay

if TYPE_CHECKING:
    from bookings.models import Booking


logger = logging.getLogger(__name__)


def sync_guest_stays_for_booking(booking: "Booking") -> None:
    """Synchronize GuestStay rows for a booking and its guests.

    This function ensures that:
    1. A GuestStay exists for every guest currently linked to the booking
    2. Existing GuestStay rows are preserved (idswh never changes)
    3. Check-in/check-out dates are synced from the booking
    4. Orphan GuestStay rows (for removed guests) are deleted
    5. The operation is idempotent (safe to run multiple times)

    Business rules:
    - idswh is generated automatically by GuestStay.save() on first creation
    - Once generated, idswh is NEVER modified or regenerated
    - GuestStay lifecycle is tied to booking guest membership, not check-in status
    - Identity exists for the entire stay lifecycle (from booking creation onward)

    Args:
        booking: The Booking instance to sync GuestStay rows for.

    Performance:
    - Single query to fetch existing GuestStay rows
    - Single query to fetch current guests
    - Bulk delete for orphan rows
    - No N+1 queries

    Example:
        >>> booking = Booking.objects.create(...)
        >>> sync_booking_guests(booking=booking, guests_data=[...])
        >>> sync_guest_stays_for_booking(booking)  # Creates GuestStay rows
    """
    booking_id = booking.id

    # Fetch current guests for this booking
    current_guest_ids = set(
        booking.guests.values_list("id", flat=True)
    )

    if not current_guest_ids:
        # No guests → remove all GuestStay rows for this booking
        deleted_count, _ = GuestStay.objects.filter(booking=booking).delete()
        if deleted_count:
            logger.info(
                "GuestStay: removed %d orphan stays for booking %d (no guests)",
                deleted_count,
                booking_id,
            )
        return

    # Fetch existing GuestStay rows for this booking
    existing_stays = list(
        GuestStay.objects.filter(booking=booking).select_related("guest")
    )
    existing_by_guest_id = {stay.guest_id: stay for stay in existing_stays}
    existing_guest_ids = set(existing_by_guest_id.keys())

    # Determine which GuestStay rows to create, update, or delete
    guest_ids_to_create = current_guest_ids - existing_guest_ids
    guest_ids_to_update = current_guest_ids & existing_guest_ids
    guest_ids_to_delete = existing_guest_ids - current_guest_ids

    # ── 1. Create missing GuestStay rows ──────────────────────────────────
    if guest_ids_to_create:
        _create_guest_stays(booking, guest_ids_to_create)

    # ── 2. Update dates on existing GuestStay rows ────────────────────────
    if guest_ids_to_update:
        _update_guest_stay_dates(
            booking,
            guest_ids_to_update,
            existing_by_guest_id,
        )

    # ── 3. Remove orphan GuestStay rows ───────────────────────────────────
    if guest_ids_to_delete:
        _delete_orphan_guest_stays(booking, guest_ids_to_delete)

    logger.info(
        "GuestStay: synced booking %d — created=%d, updated=%d, deleted=%d",
        booking_id,
        len(guest_ids_to_create),
        len(guest_ids_to_update),
        len(guest_ids_to_delete),
    )


def _create_guest_stays(
    booking: "Booking",
    guest_ids: set[int],
) -> None:
    """Create GuestStay rows for new guests.

    Each GuestStay is created individually to trigger the save() method,
    which auto-generates the idswh token.

    Args:
        booking: The parent booking.
        guest_ids: Set of guest IDs that need GuestStay rows created.
    """
    booking_id = booking.id
    check_in = booking.check_in_date
    check_out = booking.check_out_date

    created_count = 0
    for guest_id in guest_ids:
        try:
            GuestStay.objects.create(
                booking=booking,
                guest_id=guest_id,
                check_in_date=check_in,
                check_out_date=check_out,
            )
            created_count += 1
        except Exception as exc:
            logger.error(
                "GuestStay: failed to create stay for guest %d / booking %d: %s",
                guest_id,
                booking_id,
                exc,
            )
            raise

    if created_count:
        logger.info(
            "GuestStay: created %d new stays for booking %d",
            created_count,
            booking_id,
        )


def _update_guest_stay_dates(
    booking: "Booking",
    guest_ids: set[int],
    existing_by_guest_id: dict[int, GuestStay],
) -> None:
    """Update check-in/check-out dates on existing GuestStay rows.

    This preserves the idswh token while keeping dates in sync with the booking.

    Args:
        booking: The parent booking (source of truth for dates).
        guest_ids: Set of guest IDs that need date updates.
        existing_by_guest_id: Dict mapping guest_id → GuestStay instance.
    """
    booking_id = booking.id
    check_in = booking.check_in_date
    check_out = booking.check_out_date

    stays_to_update = []
    for guest_id in guest_ids:
        stay = existing_by_guest_id.get(guest_id)
        if stay is None:
            continue

        # Only update if dates actually changed
        if stay.check_in_date != check_in or stay.check_out_date != check_out:
            stay.check_in_date = check_in
            stay.check_out_date = check_out
            stays_to_update.append(stay)

    if stays_to_update:
        # Bulk update for performance (does NOT trigger save(), so idswh is safe)
        GuestStay.objects.bulk_update(
            stays_to_update,
            fields=["check_in_date", "check_out_date", "updated_at"],
        )
        logger.info(
            "GuestStay: updated dates for %d stays on booking %d",
            len(stays_to_update),
            booking_id,
        )


def _delete_orphan_guest_stays(
    booking: "Booking",
    guest_ids: set[int],
) -> None:
    """Delete GuestStay rows for guests that were removed from the booking.

    Args:
        booking: The parent booking.
        guest_ids: Set of guest IDs whose GuestStay rows should be deleted.
    """
    booking_id = booking.id

    # Delete in a single query
    deleted_count, _ = GuestStay.objects.filter(
        booking=booking,
        guest_id__in=guest_ids,
    ).delete()

    if deleted_count:
        logger.info(
            "GuestStay: deleted %d orphan stays for booking %d (guests: %s)",
            deleted_count,
            booking_id,
            sorted(guest_ids),
        )
