"""Orchestration service for isolated ISTAT XML file generation."""

from __future__ import annotations

import logging
from datetime import date, timedelta

from bookings.models import Booking
from istat.xml_export.builders.c59_aggregation_builder import (
    build_c59_aggregation_payloads_for_structure,
)
from istat.xml_export.builders.payload_builder import build_guest_payload
from istat.xml_export.exceptions import XmlPayloadValidationError
from istat.xml_export.files.file_builder import build_xml_export_result
from istat.xml_export.files.naming import (
    build_c59_daily_xml_filename,
    build_c59_xml_filename,
    build_c59_zip_filename,
    build_guest_xml_filename,
)
from istat.xml_export.files.zip_builder import build_c59_zip_result
from istat.xml_export.models.export_result import IstatXmlExportResult
from istat.xml_export.models.zip_export_result import IstatZipExportResult
from istat.xml_export.serializers.c59_xml_serializer import build_c59_xml_document
from istat.xml_export.services.c59_aggregation_service import (
    build_daily_c59_rows,
    calculate_c59_available_beds,
    calculate_c59_available_rooms,
    calculate_c59_occupied_rooms,
    fetch_c59_bookings,
)
from istat.xml_export.xml.serializer import serialize_payloads
from services.guest_night_service import (
    generate_guest_nights,
    with_guest_night_prefetch,
)
from structures.models import Structure


logger = logging.getLogger(__name__)


def _validate_date(value: date | None, field_name: str) -> date:
    if value is None:
        raise XmlPayloadValidationError(f"{field_name} is required")
    if not isinstance(value, date):
        raise XmlPayloadValidationError(f"{field_name} must be a date instance")
    return value


def _validate_date_range(
    start_date: date | None,
    end_date: date | None,
) -> tuple[date, date]:
    valid_start = _validate_date(start_date, "start_date")
    valid_end = _validate_date(end_date, "end_date")
    if valid_end < valid_start:
        raise XmlPayloadValidationError(
            "end_date must be greater than or equal to start_date"
        )
    return valid_start, valid_end


def _get_structure_or_raise(structure_id: int) -> Structure:
    structure = Structure.objects.filter(id=structure_id).first()
    if structure is None:
        raise XmlPayloadValidationError("Structure not found")
    return structure


def _get_structure_istat_code_or_raise(structure: Structure) -> str:
    istat_code = str(structure.istat_code or "").strip()
    if not istat_code:
        raise XmlPayloadValidationError(
            f"Structure {structure.id} is missing ISTAT structure code"
        )
    return istat_code


def _fetch_overlapping_bookings(
    *,
    structure_id: int,
    start_date: date,
    end_date: date,
):
    """Return checked-in bookings that overlap the given date range.

    Only bookings with ``is_checked_in=True`` are included so that the XML
    export dataset exactly matches the ISTAT page dataset, which also filters
    to checked-in reservations only.
    """
    return with_guest_night_prefetch(
        Booking.objects.filter(
            structure_id=structure_id,
            is_checked_in=True,
            check_in_date__lte=end_date,
            check_out_date__gt=start_date,
        )
    ).order_by("check_in_date", "id")


def generate_guest_xml_export(
    *,
    structure_id: int,
    start_date: date,
    end_date: date,
) -> IstatXmlExportResult:
    """Generate a guest movement XML export for a structure and date range."""

    structure = _get_structure_or_raise(structure_id)
    valid_start, valid_end = _validate_date_range(start_date, end_date)
    period_end_exclusive = valid_end + timedelta(days=1)
    bookings = list(
        _fetch_overlapping_bookings(
            structure_id=structure_id,
            start_date=valid_start,
            end_date=valid_end,
        )
    )

    guest_nights = generate_guest_nights(
        bookings,
        period_start=valid_start,
        period_end_exclusive=period_end_exclusive,
    )
    arrival_guest_nights = [
        guest_night for guest_night in guest_nights if guest_night.is_arrival_night
    ]
    bookings_by_id = {booking.id: booking for booking in bookings}
    guests_by_id = {
        guest.id: guest
        for booking in bookings
        for guest in booking.guests.all()
        if guest.id is not None
    }
    missing_guest_night = next(
        (
            guest_night
            for guest_night in arrival_guest_nights
            if guest_night.guest_id is None
            or guest_night.guest_id not in guests_by_id
            or guest_night.booking_id not in bookings_by_id
        ),
        None,
    )
    if missing_guest_night is not None:
        raise XmlPayloadValidationError(
            "Guest export failed because booking "
            f"{missing_guest_night.booking_id} occupancy exceeds saved guest records"
        )

    payloads = [
        build_guest_payload(
            booking=bookings_by_id[guest_night.booking_id],
            guest=guests_by_id[guest_night.guest_id],
            guest_night=guest_night,
        )
        for guest_night in arrival_guest_nights
    ]
    xml_content = serialize_payloads(payloads)
    filename = build_guest_xml_filename(
        structure_id=structure_id,
        start_date=valid_start,
        end_date=valid_end,
    )
    result = build_xml_export_result(filename=filename, content=xml_content)

    logger.info(
        "Generated guest XML export",
        extra={
            "structure_id": structure_id,
            "structure_istat_code": (
                str(structure.istat_code or "").strip() or None
            ),
            "export_type": "guest_xml",
            "start_date": valid_start.isoformat(),
            "end_date": valid_end.isoformat(),
            "payload_count": len(payloads),
            "filename": filename,
            "xsd_validation_enabled": False,
            "xsd_path": None,
            "status": "success",
        },
    )
    return result


def generate_c59_xml_export(
    *,
    structure_id: int,
    report_date: date,
) -> IstatXmlExportResult:
    """Generate a C59 XML export for a single report date."""

    structure = _get_structure_or_raise(structure_id)
    structure_istat_code = _get_structure_istat_code_or_raise(structure)
    valid_report_date = _validate_date(report_date, "report_date")
    rows = build_c59_aggregation_payloads_for_structure(
        structure_id=structure_id,
        start_date=valid_report_date,
        end_date=valid_report_date,
    )
    xml_content = build_c59_xml_document(
        structure_code=structure_istat_code,
        report_date=valid_report_date,
        rows=rows,
        available_rooms=calculate_c59_available_rooms(structure),
        available_beds=calculate_c59_available_beds(structure),
        occupied_rooms=calculate_c59_occupied_rooms(
            structure_id=structure_id,
            target_date=valid_report_date,
        ),
    )
    filename = build_c59_xml_filename(
        structure_id=structure_id,
        export_date=valid_report_date,
    )
    result = build_xml_export_result(filename=filename, content=xml_content)

    logger.info(
        "Generated C59 XML export",
        extra={
            "structure_id": structure_id,
            "structure_istat_code": structure_istat_code,
            "export_type": "c59_xml",
            "report_date": valid_report_date.isoformat(),
            "row_count": len(rows),
            "filename": filename,
            "xsd_validation_enabled": False,
            "xsd_path": None,
            "status": "success",
        },
    )
    return result


def generate_c59_zip_export(
    *,
    structure_id: int,
    start_date: date,
    end_date: date,
) -> IstatZipExportResult:
    """Generate a ZIP archive containing one C59 XML file per calendar day.

    The Liguria / Ross1000 specification requires one XML file per day.
    Days with no guest activity still produce a valid (empty) XML document.

    Performance notes:
    - Structure metadata (istat_code, room/bed counts) is fetched once.
    - All bookings that overlap the full date range are fetched in a single
      query and held in memory; per-day aggregation is done in Python to
      avoid N+1 database round-trips.
    - Occupied-room counts still require one lightweight DB query per day
      (distinct property_id count) — this is unavoidable without a more
      complex pre-aggregation step, but each query is O(1) in rows returned.

    Args:
        structure_id: ID of the Structure to report on.
        start_date:   First day of the reporting period (inclusive).
        end_date:     Last day of the reporting period (inclusive).

    Returns:
        An :class:`IstatZipExportResult` with the raw ZIP bytes ready for
        an HTTP download response.

    Raises:
        XmlPayloadValidationError: On invalid inputs or missing structure data.
    """
    valid_start, valid_end = _validate_date_range(start_date, end_date)

    # ── Fetch structure metadata once ────────────────────────────────────────
    structure = _get_structure_or_raise(structure_id)
    structure_istat_code = _get_structure_istat_code_or_raise(structure)

    # Capacity metrics are static for the structure — compute once.
    available_rooms = calculate_c59_available_rooms(structure)
    available_beds = calculate_c59_available_beds(structure)

    # ── Pre-fetch all overlapping bookings for the full range ────────────────
    # A booking overlaps the range if check_in < end+1 AND check_out > start.
    # We also include bookings whose check_out == a day in the range so that
    # departure rows are generated correctly.
    all_bookings = list(
        Booking.objects.filter(
            structure_id=structure_id,
            is_checked_in=True,
            check_in_date__lte=valid_end,
            check_out_date__gte=valid_start,
        )
        .select_related("property", "property_type", "structure")
        .prefetch_related("guests")
        .order_by("check_in_date", "id")
    )

    # ── Iterate day-by-day and build one XML per day ─────────────────────────
    daily_files: list[tuple[str, str]] = []
    current_day = valid_start

    while current_day <= valid_end:
        # Filter the pre-fetched bookings to those relevant for this day.
        # Relevant = arrival day OR departure day OR overnight presence.
        day_bookings = [
            b for b in all_bookings
            if b.check_in_date <= current_day and b.check_out_date >= current_day
        ]

        rows = build_daily_c59_rows(
            structure=structure,
            target_date=current_day,
            bookings=day_bookings,
        )

        occupied_rooms = calculate_c59_occupied_rooms(
            structure_id=structure_id,
            target_date=current_day,
        )

        xml_content = build_c59_xml_document(
            structure_code=structure_istat_code,
            report_date=current_day,
            rows=rows,
            available_rooms=available_rooms,
            available_beds=available_beds,
            occupied_rooms=occupied_rooms,
        )

        daily_filename = build_c59_daily_xml_filename(report_date=current_day)
        daily_files.append((daily_filename, xml_content))

        current_day += timedelta(days=1)

    # ── Pack into an in-memory ZIP ────────────────────────────────────────────
    zip_filename = build_c59_zip_filename(start_date=valid_start, end_date=valid_end)
    result = build_c59_zip_result(
        zip_filename=zip_filename,
        daily_files=daily_files,
    )

    logger.info(
        "Generated C59 ZIP export",
        extra={
            "structure_id": structure_id,
            "structure_istat_code": structure_istat_code,
            "export_type": "c59_zip",
            "start_date": valid_start.isoformat(),
            "end_date": valid_end.isoformat(),
            "file_count": result.file_count,
            "zip_filename": zip_filename,
            "zip_byte_size": result.byte_size,
            "status": "success",
        },
    )
    return result
