"""Compute available booking slots for a given date and party size.

Shared between the public availability endpoint and the agent's
``find_available_slots`` tool so both surfaces return identical results.
"""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from datetime import date as DateType
from typing import Any

from sqlalchemy.ext.asyncio import AsyncSession

from app.models.restaurant import Restaurant
from app.services.service_blocks import generate_available_slots, resolve_service_blocks
from app.services.table_allocation import (
    preload_slot_availability,
    slot_has_availability,
    table_free_in_window,
)
from app.utils.tz import local_day_window, resolve_tz


async def compute_available_slots(
    session: AsyncSession,
    restaurant: Restaurant,
    target_date: DateType,
    party_size: int,
) -> dict[str, Any]:
    """Return available reservation start times + combo options for *target_date*.

    Returns ``{"available_slots": list[str], "combo_options": list[dict],
    "reason": str | None}``. ``reason`` is populated when the slot list is empty
    so callers can distinguish between "open but full" and structural
    unavailability (``past``, ``too_far_in_advance``, ``closed``).
    """
    settings_data = restaurant.settings or {}
    min_advance_hours = int(settings_data.get("min_advance_hours", 1))
    max_advance_days = int(settings_data.get("max_advance_days", 90))
    tz = resolve_tz(restaurant.timezone)
    now_local = datetime.now(tz).replace(tzinfo=None)
    earliest_bookable = now_local + timedelta(hours=min_advance_hours)
    latest_bookable_date = now_local.date() + timedelta(days=max_advance_days)

    if target_date < now_local.date():
        return {"available_slots": [], "combo_options": [], "reason": "past"}
    if target_date > latest_bookable_date:
        return {"available_slots": [], "combo_options": [], "reason": "too_far_in_advance"}

    blocks = await resolve_service_blocks(session, restaurant.id, target_date)
    if not blocks:
        return {"available_slots": [], "combo_options": [], "reason": "closed"}

    open_blocks = [b for b in blocks if b.block.block_type == "open"]
    if not open_blocks:
        return {"available_slots": [], "combo_options": [], "reason": "closed"}

    day_start, day_end = local_day_window(target_date, tz)
    data = await preload_slot_availability(session, restaurant.id, party_size, day_start, day_end)

    def _to_naive_utc(local_dt: datetime) -> datetime:
        return local_dt.replace(tzinfo=tz).astimezone(UTC).replace(tzinfo=None)

    earliest_bookable_utc = _to_naive_utc(earliest_bookable)

    available: list[str] = []
    for block in open_blocks:
        slots = generate_available_slots(block)
        if not slots:
            continue

        block_start_dt = _to_naive_utc(datetime.combine(target_date, block.block.start_time))
        block_end_dt = _to_naive_utc(datetime.combine(target_date, block.block.end_time))
        booked_covers = sum(
            r.party_size
            for r in data.all_reservations
            if block_start_dt <= r.reserved_at < block_end_dt
        )

        max_cov = block.block.max_covers
        if max_cov is not None and booked_covers + party_size > max_cov:
            continue

        duration_minutes = block.block.default_duration_minutes or 90
        for slot in slots:
            slot_start_dt = _to_naive_utc(datetime.combine(target_date, slot))
            if slot_start_dt < earliest_bookable_utc:
                continue
            slot_end_dt = slot_start_dt + timedelta(minutes=duration_minutes)
            if slot_has_availability(party_size, slot_start_dt, slot_end_dt, data):
                available.append(slot.strftime("%H:%M"))

    combo_options: list[dict[str, Any]] = []
    if party_size > 0:
        for combo in data.all_combos:
            if combo.combined_capacity >= party_size:
                combo_options.append(
                    {
                        "id": combo.id,
                        "name": combo.name,
                        "table_ids": combo.table_ids,
                        "combined_capacity": combo.combined_capacity,
                    }
                )

    if combo_options and available:
        filtered_combos: list[dict[str, Any]] = []
        for combo_opt in combo_options:
            combo_tids: list[str] = combo_opt["table_ids"]
            for slot_str in available:
                slot_time = datetime.strptime(slot_str, "%H:%M").time()
                s_start = _to_naive_utc(datetime.combine(target_date, slot_time))
                duration_check = 90
                for b in open_blocks:
                    if b.block.start_time <= slot_time < b.block.end_time:
                        duration_check = b.block.default_duration_minutes or 90
                        break
                s_end = s_start + timedelta(minutes=duration_check)
                if all(
                    table_free_in_window(tid, s_start, s_end, data.occupied) for tid in combo_tids
                ):
                    filtered_combos.append(combo_opt)
                    break
        combo_options = filtered_combos

    reason = "fully_booked" if not available else None
    return {"available_slots": available, "combo_options": combo_options, "reason": reason}
