"""Daily C59 aggregation service for Regione Liguria movement rows."""

from __future__ import annotations

from collections import defaultdict
from datetime import date
from typing import Iterable

from django.db.models import ExpressionWrapper, F, IntegerField, Q, Sum
from django.db.models.functions import Coalesce

from bookings.models import Booking
from guests.guest_defaults import normalize_guest_for_istat
from istat.xml_export.exceptions import XmlPayloadValidationError
from istat.xml_export.models.c59_payload import IstatC59RowPayload
from istat.xml_export.services.lookup_service import IstatLookupService
from properties.models import Property
from structures.models import Structure


def booking_overlaps_day(booking: Booking, target_date: date) -> bool:
    return booking.check_in_date <= target_date < booking.check_out_date


def c59_relevant_booking_filter(target_date: date) -> Q:
    return (
        Q(check_in_date__lte=target_date, check_out_date__gt=target_date)
        | Q(check_in_date=target_date)
        | Q(check_out_date=target_date)
    )


def fetch_c59_bookings(structure_id: int, target_date: date):
    return (
        Booking.objects.filter(
            c59_relevant_booking_filter(target_date),
            structure_id=structure_id,
            is_checked_in=True,
        )
        .select_related("property", "property_type", "structure")
        .prefetch_related("guests")
        .order_by("check_in_date", "id")
    )


def _normalize_code(value: str | None) -> str:
    return (value or "").strip().upper()


def _three_digit_code(value: str | None, *, context: str) -> str:
    code = str(value or "").strip()
    if code.isdigit():
        code = code.zfill(3)
    if not code.isdigit() or len(code) != 3:
        raise XmlPayloadValidationError(
            f"Invalid {context} code '{value}' in C59 aggregation (must be 3 digits)"
        )
    return code


def resolve_c59_residence_code(country: str | None, province: str | None) -> tuple[str, str]:
    country_code = _normalize_code(country)
    if not country_code:
        raise XmlPayloadValidationError("Country is required for C59 aggregation")

    if country_code == "IT":
        province_sigla = _normalize_code(province)
        if not province_sigla:
            raise XmlPayloadValidationError(
                "Province is required for Italian residents in C59 aggregation"
            )
        province_mapping = IstatLookupService.get_province(province_sigla)
        if province_mapping is None:
            raise XmlPayloadValidationError(
                f"Missing ISTAT province mapping for '{province_sigla}' in C59 aggregation"
            )
        return "i", _three_digit_code(
            province_mapping.get("province_code"),
            context=f"province '{province_sigla}'",
        )

    country_mapping = IstatLookupService.get_country(country_code)
    if country_mapping is None:
        raise XmlPayloadValidationError(
            f"Missing ISTAT country mapping for '{country_code}' in C59 aggregation"
        )
    return "e", _three_digit_code(
        country_mapping.get("istat_code"),
        context=f"country '{country_code}'",
    )


def _guest_residence_key(guest) -> tuple[str, str]:
    normalized_guest = normalize_guest_for_istat(guest)
    extra_data = getattr(normalized_guest, "extra_data", None)
    province = extra_data.get("province") if isinstance(extra_data, dict) else None
    return resolve_c59_residence_code(
        getattr(normalized_guest, "country", None),
        province,
    )


def build_daily_c59_rows(
    *,
    structure: Structure,
    target_date: date,
    bookings: Iterable[Booking],
) -> list[IstatC59RowPayload]:
    aggregation = defaultdict(lambda: {"arrivi": 0, "partenze": 0, "presenze": 0})

    for booking in bookings:
        is_arrival = booking.check_in_date == target_date
        is_departure = booking.check_out_date == target_date
        is_presence = booking_overlaps_day(booking, target_date)

        if not (is_arrival or is_departure or is_presence):
            continue

        for guest in booking.guests.all():
            try:
                key = _guest_residence_key(guest)
            except XmlPayloadValidationError as exc:
                raise XmlPayloadValidationError(
                    f"Guest {guest.id} (booking {booking.id}): {exc}"
                ) from exc

            if is_arrival:
                aggregation[key]["arrivi"] += 1
            if is_departure:
                aggregation[key]["partenze"] += 1
            if is_presence:
                aggregation[key]["presenze"] += 1

    rows = [
        IstatC59RowPayload(
            nazione=nazione,
            residenza=residenza,
            arrivi=counts["arrivi"],
            partenze=counts["partenze"],
            presenze=counts["presenze"],
            diurni=0,
        )
        for (nazione, residenza), counts in aggregation.items()
        if counts["arrivi"] or counts["partenze"] or counts["presenze"]
    ]
    rows.sort(key=lambda row: (row.nazione, row.residenza))
    return rows


def build_daily_c59_rows_for_structure(
    *,
    structure_id: int,
    target_date: date,
) -> list[IstatC59RowPayload]:
    structure = Structure.objects.get(id=structure_id)
    return build_daily_c59_rows(
        structure=structure,
        target_date=target_date,
        bookings=fetch_c59_bookings(structure_id, target_date),
    )


def calculate_c59_occupied_rooms(
    *,
    structure_id: int,
    target_date: date,
) -> int:
    return (
        Booking.objects.filter(
            structure_id=structure_id,
            is_checked_in=True,
            check_in_date__lte=target_date,
            check_out_date__gt=target_date,
            property_id__isnull=False,
        )
        .values("property_id")
        .distinct()
        .count()
    )


def _active_properties_for_structure(structure: Structure):
    return Property.objects.filter(
        structure=structure,
        availability=Property.Availability.AVAILABLE,
    ).select_related("property_type")


def calculate_c59_available_rooms(structure: Structure) -> int:
    total_rooms = getattr(structure, "total_rooms", None) or getattr(
        structure, "total_units", None
    )
    if total_rooms and total_rooms > 0:
        return int(total_rooms)
    return _active_properties_for_structure(structure).count()


def calculate_c59_available_beds(structure: Structure) -> int:
    """Calculate total available beds for a structure.

    Each PropertyType defines beds *per room/unit* (num_beds + num_sofa_beds).
    The total is the sum of (beds_per_room × number_of_active_rooms) across all
    property types that have at least one active room in this structure.

    Uses a single aggregated DB query to avoid N+1 issues:
      SUM(num_beds + num_sofa_beds)  grouped per active Property row.

    The ``structure.total_beds`` shortcut is intentionally NOT used here because
    that field is not present on the Structure model and the per-room calculation
    is the authoritative source of truth for C59 reporting.
    """
    result = (
        Property.objects.filter(
            structure=structure,
            availability=Property.Availability.AVAILABLE,
        )
        .annotate(
            beds_per_room=ExpressionWrapper(
                Coalesce(F("property_type__num_beds"), 0)
                + Coalesce(F("property_type__num_sofa_beds"), 0),
                output_field=IntegerField(),
            )
        )
        .aggregate(total=Coalesce(Sum("beds_per_room"), 0))
    )
    return int(result["total"])
