"""Public (unauthenticated) endpoints for restaurant info and booking."""

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

import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import SQLModel, select

from app.db.session import get_session
from app.dependencies import get_restate_client, restate_proxy
from app.models.menu_item import MenuItem
from app.models.reservation import Reservation, ReservationRead
from app.models.restaurant import Restaurant
from app.models.table_combination import TableCombination
from app.services.availability import compute_available_slots
from app.services.service_blocks import generate_available_slots, resolve_blocks_in_memory
from app.utils.tz import local_day_window, naive_utc_to_local, resolve_tz

router = APIRouter(prefix="/public", tags=["public"])


class PublicRestaurantRead(SQLModel):
    """Whitelisted public fields only — no internal IDs or full settings JSONB."""

    name: str
    slug: str
    description: str | None = None
    phone: str | None = None
    address: dict[str, str | None] | None = None
    opening_hours: Any = None
    max_party_size: int = 20
    min_advance_hours: int = 1
    max_advance_days: int = 90
    default_language: str | None = None

    dish_chooser_enabled: bool = False
    dish_chooser_min_party_size: int = 8
    dish_chooser_max_dishes: int = 5
    dish_chooser_from_menu: bool = False


class PublicMenuItemRead(SQLModel):
    """Public menu item fields — no internal IDs exposed."""

    id: str
    name: str
    category: str
    price_cents: int
    description: str | None = None


class PublicReservationCreate(SQLModel):
    guest_name: str
    guest_email: str | None = None
    guest_phone: str | None = None
    party_size: int
    reserved_at: datetime
    notes: str | None = None
    dishes: list[dict] | None = None
    dishes_text: str | None = None


async def _get_restaurant_by_slug(slug: str, session: AsyncSession) -> Restaurant:
    """Load a restaurant by slug and set tenant context for downstream RLS queries."""
    result = await session.execute(select(Restaurant).where(Restaurant.slug == slug))
    restaurant = result.scalar_one_or_none()
    if restaurant is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Restaurant not found")
    return restaurant


@router.get("/restaurants/{slug}", response_model=PublicRestaurantRead)
async def get_public_restaurant(
    slug: str,
    session: AsyncSession = Depends(get_session),
) -> PublicRestaurantRead:
    """Return whitelisted public restaurant info — no auth required."""
    restaurant = await _get_restaurant_by_slug(slug, session)
    settings = restaurant.settings or {}
    address = {
        "street": settings.get("address_street"),
        "city": settings.get("address_city"),
        "postcode": settings.get("address_postcode"),
        "country": settings.get("address_country"),
    }
    return PublicRestaurantRead(
        name=restaurant.name,
        slug=restaurant.slug,
        description=settings.get("description"),
        phone=restaurant.phone,
        address=address,
        opening_hours=settings.get("opening_hours"),
        max_party_size=int(settings.get("max_party_size", 20)),
        min_advance_hours=int(settings.get("min_advance_hours", 1)),
        max_advance_days=int(settings.get("max_advance_days", 90)),
        default_language=settings.get("default_language"),
        dish_chooser_enabled=bool(settings.get("dish_chooser_enabled", False)),
        dish_chooser_min_party_size=int(settings.get("dish_chooser_min_party_size", 8)),
        dish_chooser_max_dishes=int(settings.get("dish_chooser_max_dishes", 5)),
        dish_chooser_from_menu=bool(settings.get("dish_chooser_from_menu", False)),
    )


@router.get("/restaurants/{slug}/menu-items", response_model=list[PublicMenuItemRead])
async def get_public_menu_items(
    slug: str,
    session: AsyncSession = Depends(get_session),
) -> list[PublicMenuItemRead]:
    """Return active menu items for a restaurant — no auth required."""
    restaurant = await _get_restaurant_by_slug(slug, session)
    result = await session.execute(
        select(MenuItem)
        .where(
            MenuItem.restaurant_id == restaurant.id,
            MenuItem.is_active == True,  # noqa: E712
        )
        .order_by(MenuItem.category, MenuItem.sort_order, MenuItem.name)  # type: ignore[arg-type]
    )
    items = result.scalars().all()
    return [
        PublicMenuItemRead(
            id=item.id,
            name=item.name,
            category=item.category,
            price_cents=item.price_cents,
            description=item.description,
        )
        for item in items
    ]


@router.get("/restaurants/{slug}/calendar-status")
async def get_calendar_status(
    slug: str,
    start_date: str = Query(..., description="ISO start date YYYY-MM-DD"),
    end_date: str = Query(..., description="ISO end date YYYY-MM-DD"),
    party_size: int = Query(default=2, ge=1, le=20),
    session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
    """Return per-day status for a date range: closed, fully_booked, or available."""
    restaurant = await _get_restaurant_by_slug(slug, session)

    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)

    try:
        start = DateType.fromisoformat(start_date)
        end = DateType.fromisoformat(end_date)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid date format"
        ) from None

    # Cap range to 62 days to prevent abuse, and to max_advance_days
    if (end - start).days > 62:
        end = start + timedelta(days=62)
    if end > latest_bookable_date:
        end = latest_bookable_date

    # ── Pre-fetch all data in bulk ──────────────────────────────────────────
    from app.models.chair import Chair
    from app.models.service_block import ServiceBlock, ServiceBlockOverride
    from app.models.table import FloorTable

    # 1. All active service blocks grouped by day_of_week
    block_result = await session.execute(
        select(ServiceBlock).where(
            ServiceBlock.restaurant_id == restaurant.id,
            ServiceBlock.is_active == True,  # noqa: E712
        )
    )
    blocks_by_dow: dict[int, list[ServiceBlock]] = {}
    for b in block_result.scalars().all():
        blocks_by_dow.setdefault(b.day_of_week, []).append(b)

    # 2. Active overrides overlapping the date range
    override_result = await session.execute(
        select(ServiceBlockOverride).where(
            ServiceBlockOverride.restaurant_id == restaurant.id,
            ServiceBlockOverride.is_active == True,  # noqa: E712
            ServiceBlockOverride.start_date <= end,
            ServiceBlockOverride.end_date >= start,
        )
    )
    active_overrides = list(override_result.scalars().all())

    # 3. All non-cancelled reservations in the restaurant-local range.
    # The DB column is naive-UTC; convert the local day boundaries through
    # the restaurant timezone before querying. Then snapshot each row's
    # ``reserved_at`` / ``end_time`` in naive-local form so the rest of
    # the function (which thinks in restaurant-local time-of-day) can
    # compare cleanly without mixing tz domains.
    range_start_dt, _ = local_day_window(start, tz)
    _, range_end_dt = local_day_window(end, tz)
    res_result = await session.execute(
        select(Reservation).where(
            Reservation.restaurant_id == restaurant.id,
            Reservation.status != "cancelled",
            Reservation.reserved_at >= range_start_dt,
            Reservation.reserved_at < range_end_dt,
        )
    )
    all_reservations = list(res_result.scalars().all())
    local_reservations = [
        (
            r,
            naive_utc_to_local(r.reserved_at, tz),
            naive_utc_to_local(r.end_time, tz),
        )
        for r in all_reservations
    ]

    # 4. All tables with enabled-chair counts (capacity)
    from sqlalchemy import func

    table_result = await session.execute(
        select(FloorTable.id, func.count(Chair.id).label("capacity"))
        .outerjoin(
            Chair,
            (Chair.table_id == FloorTable.id) & (Chair.enabled == True),  # noqa: E712
        )
        .where(FloorTable.restaurant_id == restaurant.id)
        .group_by(FloorTable.id)
    )
    tables: list[tuple[str, int]] = [(r[0], r[1]) for r in table_result.all()]

    # 5. All table combinations
    combo_result = await session.execute(
        select(TableCombination).where(
            TableCombination.restaurant_id == restaurant.id,
        )
    )
    all_combos = list(combo_result.scalars().all())
    combo_map: dict[str, TableCombination] = {c.id: c for c in all_combos}

    # ── Build occupied-interval index per table (naive-local) ───────────────
    occupied: dict[str, list[tuple[datetime, datetime]]] = {}
    for r, r_start_local, r_end_local in local_reservations:
        if r.table_id:
            occupied.setdefault(r.table_id, []).append((r_start_local, r_end_local))
        if r.combination_id:
            combo = combo_map.get(r.combination_id)
            if combo:
                for tid in combo.table_ids:
                    occupied.setdefault(tid, []).append((r_start_local, r_end_local))

    def _table_free(table_id: str, slot_start: datetime, slot_end: datetime) -> bool:
        for r_start, r_end in occupied.get(table_id, []):
            if r_start < slot_end and r_end > slot_start:
                return False
        return True

    # ── Compute per-day status ───────────────────────────────────────────────
    days: dict[str, str] = {}
    current = start
    _DEFAULT_DURATION = 90

    while current <= end:
        if current < now_local.date() or current > latest_bookable_date:
            days[current.isoformat()] = "closed"
            current += timedelta(days=1)
            continue

        blocks = resolve_blocks_in_memory(
            current,
            blocks_by_dow,
            active_overrides,
            restaurant.id,
        )
        open_blocks = [b for b in blocks if b.block.block_type == "open"]

        if not open_blocks:
            days[current.isoformat()] = "closed"
            current += timedelta(days=1)
            continue

        has_availability = False
        for block in open_blocks:
            # Block-level capacity check
            if block.block.max_covers is not None:
                block_start_dt = datetime.combine(current, block.block.start_time)
                block_end_dt = datetime.combine(current, block.block.end_time)
                booked_covers = sum(
                    r.party_size
                    for r, r_start_local, _ in local_reservations
                    if r_start_local.date() == current
                    and block_start_dt <= r_start_local < block_end_dt
                )
                if booked_covers + party_size > block.block.max_covers:
                    continue  # this block is at cover capacity

            # Slot-level table check: find at least 1 free table at 1 slot
            duration = block.block.default_duration_minutes or _DEFAULT_DURATION
            for slot in generate_available_slots(block):
                if datetime.combine(current, slot) < earliest_bookable:
                    continue
                slot_start = datetime.combine(current, slot)
                slot_end = slot_start + timedelta(minutes=duration)
                for table_id, capacity in tables:
                    if capacity >= party_size and _table_free(table_id, slot_start, slot_end):
                        has_availability = True
                        break
                if not has_availability:
                    for combo in all_combos:
                        if combo.combined_capacity >= party_size:
                            all_free = all(
                                _table_free(tid, slot_start, slot_end) for tid in combo.table_ids
                            )
                            if all_free:
                                has_availability = True
                                break
                if has_availability:
                    break
            if has_availability:
                break

        days[current.isoformat()] = "available" if has_availability else "fully_booked"
        current += timedelta(days=1)

    return {"days": days}


@router.get("/restaurants/{slug}/availability")
async def get_availability(
    slug: str,
    date: str = Query(..., description="ISO date YYYY-MM-DD"),
    party_size: int = Query(default=2, ge=1, le=20),
    session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
    """Return available slots and combo options for a given date — no auth required."""
    restaurant = await _get_restaurant_by_slug(slug, session)

    try:
        target_date = DateType.fromisoformat(date)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid date format"
        ) from None

    result = await compute_available_slots(session, restaurant, target_date, party_size)
    # Strip the internal ``reason`` field — public callers only consume the
    # list shape.  Agent callers use ``find_available_slots`` which keeps it.
    return {
        "available_slots": result["available_slots"],
        "combo_options": result["combo_options"],
    }


@router.post(
    "/restaurants/{slug}/reservations",
    response_model=ReservationRead,
    status_code=status.HTTP_201_CREATED,
)
async def create_public_reservation(
    slug: str,
    data: PublicReservationCreate,
    session: AsyncSession = Depends(get_session),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> ReservationRead:
    """Create a reservation for a public booking — no auth required."""
    restaurant = await _get_restaurant_by_slug(slug, session)

    reservation_id = str(uuid.uuid4())
    payload = {
        "guest_name": data.guest_name,
        "guest_email": data.guest_email,
        "guest_phone": data.guest_phone,
        "party_size": data.party_size,
        "reserved_at": data.reserved_at.isoformat(),
        "notes": data.notes,
        "dishes": data.dishes,
        "dishes_text": data.dishes_text,
        "status": "pending",
        "source": "widget",
        "restaurant_id": restaurant.id,
    }

    await restate_proxy(
        client,
        "ReservationWorkflow",
        reservation_id,
        "create_reservation",
        payload,
        mode="object",
    )

    # Fetch the committed row — the workflow computed end_time,
    # snapped reserved_at, and potentially assigned a table.
    result = await session.execute(select(Reservation).where(Reservation.id == reservation_id))
    reservation = result.scalar_one_or_none()
    if reservation is None:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Reservation was created but could not be retrieved",
        )
    return ReservationRead.model_validate(reservation)
