from __future__ import annotations

from dataclasses import dataclass
from datetime import date, timedelta
from typing import Iterable

from bookings.models import Booking
from django.db.models import Prefetch


@dataclass(frozen=True)
class GuestNightSubject:
    key: str
    booking_id: int
    guest_id: int | None
    full_name: str
    email: str
    phone: str
    date_of_birth: date | None
    nationality: str | None
    exemption_reason: str | None
    is_synthetic_minor: bool = False
    is_main_guest: bool = False


@dataclass(frozen=True)
class GuestNight:
    guest_id: int | None
    guest_key: str
    booking_id: int
    date: date
    month: int
    structure_id: int | None
    property_id: int | None
    platform: str
    age_on_night: int | None
    is_minor: bool
    exemption_reason: str | None
    nationality: str | None
    full_name: str
    email: str
    phone: str
    is_main_guest: bool
    night_index: int
    is_arrival_night: bool
    is_departure_night: bool


def with_guest_night_prefetch(queryset):
    return queryset.select_related("property", "property_type").prefetch_related(
        Prefetch("guests")
    )


def get_prefetched_guests(booking: Booking):
    if (
        hasattr(booking, "_prefetched_objects_cache")
        and "guests" in booking._prefetched_objects_cache
    ):
        return booking._prefetched_objects_cache["guests"]
    return list(booking.guests.all())


def calculate_age_on(date_of_birth, on_date: date) -> int | None:
    if not date_of_birth:
        return None

    years = on_date.year - date_of_birth.year
    if (on_date.month, on_date.day) < (date_of_birth.month, date_of_birth.day):
        years -= 1
    return years


def get_booking_guest_count(booking: Booking) -> int:
    booking_count = max(
        int(booking.adults_count or 0) + int(booking.children_count or 0),
        0,
    )
    actual_guest_count = len(get_prefetched_guests(booking))
    return max(booking_count, actual_guest_count)


def build_guest_night_subjects(booking: Booking) -> list[GuestNightSubject]:
    total_guests_count = get_booking_guest_count(booking)
    if total_guests_count <= 0:
        return []

    guests = sorted(
        list(get_prefetched_guests(booking)),
        key=lambda guest: (not guest.is_main_guest, guest.id or 0),
    )
    considered_guests = guests[:total_guests_count]

    booking_children_count = max(int(booking.children_count or 0), 0)
    known_minors = sum(
        1
        for guest in considered_guests
        if (
            calculate_age_on(guest.date_of_birth, booking.check_in_date) is not None
            and calculate_age_on(guest.date_of_birth, booking.check_in_date) < 18
        )
    )

    remaining_slots = total_guests_count - len(considered_guests)
    synthetic_minor_count = max(booking_children_count - known_minors, 0)
    synthetic_minor_count = min(synthetic_minor_count, remaining_slots)
    synthetic_adult_count = max(remaining_slots - synthetic_minor_count, 0)

    subjects = [
        GuestNightSubject(
            key=f"booking-{booking.id}-guest-{guest.id}",
            booking_id=booking.id,
            guest_id=guest.id,
            full_name=guest.full_name or "",
            email=guest.email or "",
            phone=guest.phone or "",
            date_of_birth=guest.date_of_birth,
            nationality=guest.nationality or None,
            exemption_reason=(
                guest.city_tax_exemption_reason
                if getattr(guest, "is_city_tax_exempt", False)
                else None
            ),
            is_main_guest=guest.is_main_guest,
        )
        for guest in considered_guests
    ]

    for index in range(synthetic_minor_count):
        subjects.append(
            GuestNightSubject(
                key=f"booking-{booking.id}-synthetic-child-{index + 1}",
                booking_id=booking.id,
                guest_id=None,
                full_name="",
                email="",
                phone="",
                date_of_birth=None,
                nationality=None,
                exemption_reason=None,
                is_synthetic_minor=True,
            )
        )

    for index in range(synthetic_adult_count):
        subjects.append(
            GuestNightSubject(
                key=f"booking-{booking.id}-synthetic-adult-{index + 1}",
                booking_id=booking.id,
                guest_id=None,
                full_name="",
                email="",
                phone="",
                date_of_birth=None,
                nationality=None,
                exemption_reason=None,
            )
        )

    return subjects[:total_guests_count]


def generate_guest_nights(
    bookings: Iterable[Booking],
    *,
    period_start: date,
    period_end_exclusive: date,
    minor_age_limit: int = 18,
) -> list[GuestNight]:
    if hasattr(bookings, "select_related"):
        bookings = with_guest_night_prefetch(bookings)

    guest_nights: list[GuestNight] = []

    for booking in bookings:
        if not booking.check_in_date or not booking.check_out_date:
            continue
        if booking.check_out_date <= booking.check_in_date:
            continue

        subjects = build_guest_night_subjects(booking)
        if not subjects:
            continue

        total_nights = (booking.check_out_date - booking.check_in_date).days
        departure_night = booking.check_out_date - timedelta(days=1)

        for subject in subjects:
            for night_offset in range(total_nights):
                night_date = booking.check_in_date + timedelta(days=night_offset)
                if not (period_start <= night_date < period_end_exclusive):
                    continue

                age_on_night = calculate_age_on(subject.date_of_birth, night_date)
                is_minor = subject.is_synthetic_minor or (
                    age_on_night is not None and age_on_night < minor_age_limit
                )

                guest_nights.append(
                    GuestNight(
                        guest_id=subject.guest_id,
                        guest_key=subject.key,
                        booking_id=booking.id,
                        date=night_date,
                        month=night_date.month,
                        structure_id=booking.structure_id,
                        property_id=booking.property_id,
                        platform=str(booking.platform or ""),
                        age_on_night=age_on_night,
                        is_minor=is_minor,
                        exemption_reason=subject.exemption_reason,
                        nationality=subject.nationality,
                        full_name=subject.full_name,
                        email=subject.email,
                        phone=subject.phone,
                        is_main_guest=subject.is_main_guest,
                        night_index=night_offset + 1,
                        is_arrival_night=night_date == booking.check_in_date,
                        is_departure_night=night_date == departure_night,
                    )
                )

    return guest_nights
