"""Reservations router.

GETs read directly from Neon via SQLModel.
POST/PUT/DELETE proxy to Restate ReservationObject for exactly-once write semantics.
"""

import uuid
from datetime import date as date_type
from typing import Any

import httpx
from fastapi import APIRouter, Depends, HTTPException, status
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select

from app.auth.better_auth import CurrentUser, get_current_user
from app.auth.tenant import get_current_restaurant, get_tenant_session
from app.cache import invalidate_restaurant_stats_cache
from app.dependencies import get_restate_client, restate_proxy
from app.models.reservation import (
    MoveReservationRequest,
    Reservation,
    ReservationActionResponse,
    ReservationApprovalRequest,
    ReservationCreate,
    ReservationPatch,
    ReservationRead,
)
from app.models.restaurant import Restaurant
from app.models.table import FloorTable
from app.schemas.dish_chooser import DishSubmissionRequest
from app.utils.tz import local_day_window, resolve_tz

router = APIRouter(prefix="/reservations", tags=["reservations"])
limiter = Limiter(key_func=get_remote_address)


async def _validate_table_id(
    session: AsyncSession, table_id: str | None, restaurant_id: str
) -> None:
    """Raise 404 if table_id is set but doesn't belong to the restaurant."""
    if table_id is None:
        return
    result = await session.execute(
        select(FloorTable.id).where(
            FloorTable.id == table_id, FloorTable.restaurant_id == restaurant_id
        )
    )
    if result.scalar_one_or_none() is None:
        raise HTTPException(status_code=404, detail="Table not found")


@router.get("/", response_model=list[ReservationRead])
async def list_reservations(
    date: str | None = None,
    date_from: str | None = None,
    date_to: str | None = None,
    status: str | None = None,
    customer_id: str | None = None,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> list[Any]:
    query = (
        select(Reservation)
        .where(Reservation.restaurant_id == restaurant.id)
        .order_by(Reservation.reserved_at.desc())  # type: ignore[attr-defined]
    )

    if status is not None:
        query = query.where(Reservation.status == status)

    if customer_id is not None:
        query = query.where(Reservation.customer_id == customer_id)

    if date_from is not None and date_to is not None:
        try:
            from_date = date_type.fromisoformat(date_from)
            to_date = date_type.fromisoformat(date_to)
        except ValueError as exc:
            raise HTTPException(
                status_code=422, detail="Invalid date format. Use YYYY-MM-DD"
            ) from exc

        # Naive-UTC day window so the filter lines up with the storage
        # convention. Operators think in restaurant-local dates — without
        # the timezone conversion, reservations near midnight land on the
        # wrong day for any restaurant not in UTC.
        tz = resolve_tz(restaurant.timezone)
        date_from_start, _ = local_day_window(from_date, tz)
        _, date_to_end = local_day_window(to_date, tz)
        query = query.where(
            Reservation.reserved_at >= date_from_start,
            Reservation.reserved_at < date_to_end,
        )
    elif date is not None:
        try:
            filter_date = date_type.fromisoformat(date)
        except ValueError as exc:
            raise HTTPException(
                status_code=422, detail="Invalid date format. Use YYYY-MM-DD"
            ) from exc

        tz = resolve_tz(restaurant.timezone)
        day_start, day_end = local_day_window(filter_date, tz)
        query = query.where(
            Reservation.reserved_at >= day_start,
            Reservation.reserved_at < day_end,
        )
    elif customer_id is not None:
        pass
    else:
        query = query.limit(100)

    result = await session.execute(query)
    return list(result.scalars().all())


@router.post("/", response_model=ReservationRead, status_code=status.HTTP_202_ACCEPTED)
async def create_reservation(
    data: ReservationCreate,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    await _validate_table_id(session, data.table_id, restaurant.id)
    reservation_id = str(uuid.uuid4())
    payload = {**data.model_dump(mode="json"), "restaurant_id": restaurant.id}
    # Customer-facing bookings go through the full saga (availability check, email, payment).
    # Manual/admin creates bypass the saga and go directly to ReservationObject.
    if data.source in ("widget", "agent"):
        await restate_proxy(
            client,
            "ReservationWorkflow",
            reservation_id,
            "create_reservation",
            payload,
            mode="object",
        )
    else:
        await restate_proxy(
            client,
            "ReservationObject",
            reservation_id,
            "create",
            payload,
            mode="object",
        )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationRead(
        id=reservation_id,
        restaurant_id=restaurant.id,
        **data.model_dump(),
    )


@router.put(
    "/{reservation_id}", response_model=ReservationRead, status_code=status.HTTP_202_ACCEPTED
)
async def update_reservation(
    reservation_id: str,
    data: ReservationCreate,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    payload = {**data.model_dump(mode="json"), "restaurant_id": restaurant.id}
    await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "update",
        payload,
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationRead(id=reservation_id, restaurant_id=restaurant.id, **data.model_dump())


@router.delete("/{reservation_id}", status_code=status.HTTP_202_ACCEPTED)
async def cancel_reservation(
    reservation_id: str,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> dict[str, str]:
    await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "cancel",
        {"restaurant_id": restaurant.id},
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return {"status": "cancellation accepted"}


@router.get("/{reservation_id}", response_model=ReservationRead)
async def get_reservation(
    reservation_id: str,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
    _: CurrentUser = Depends(get_current_user),
) -> Any:
    result = await session.execute(
        select(Reservation).where(
            Reservation.id == reservation_id,
            Reservation.restaurant_id == restaurant.id,
        )
    )
    reservation = result.scalar_one_or_none()
    if reservation is None:
        from fastapi import HTTPException

        raise HTTPException(status_code=404, detail="Reservation not found")
    return reservation


@router.patch(
    "/{reservation_id}",
    response_model=ReservationRead,
    status_code=status.HTTP_202_ACCEPTED,
)
async def patch_reservation(
    reservation_id: str,
    data: ReservationPatch,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
    session: AsyncSession = Depends(get_tenant_session),
) -> Any:
    """Partial update of reservation fields (non-status). Proxied to Restate."""
    await _validate_table_id(session, data.table_id, restaurant.id)
    payload = {
        **data.model_dump(mode="json", exclude_unset=True),
        "restaurant_id": restaurant.id,
    }
    await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "update",
        payload,
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    # Return refreshed state from DB so caller sees the committed result.
    result = await session.execute(
        select(Reservation).where(
            Reservation.id == reservation_id,
            Reservation.restaurant_id == restaurant.id,
        )
    )
    reservation = result.scalar_one_or_none()
    if reservation is None:
        from fastapi import HTTPException

        raise HTTPException(status_code=404, detail="Reservation not found")
    return reservation


@router.post(
    "/{reservation_id}/approve",
    response_model=ReservationActionResponse,
    status_code=status.HTTP_202_ACCEPTED,
)
async def approve_reservation(
    reservation_id: str,
    body: ReservationApprovalRequest | None = None,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    """Approve a pending reservation → confirmed."""
    payload = {"restaurant_id": restaurant.id}
    if body and body.comment:
        payload["comment"] = body.comment
    result = await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "approve",
        payload,
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationActionResponse(id=reservation_id, status=result.get("status", "confirmed"))


@router.post(
    "/{reservation_id}/reject",
    response_model=ReservationActionResponse,
    status_code=status.HTTP_202_ACCEPTED,
)
async def reject_reservation(
    reservation_id: str,
    body: ReservationApprovalRequest | None = None,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    """Reject a pending reservation → cancelled."""
    payload = {"restaurant_id": restaurant.id}
    if body and body.comment:
        payload["comment"] = body.comment
    result = await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "reject",
        payload,
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationActionResponse(id=reservation_id, status=result.get("status", "cancelled"))


@router.post(
    "/{reservation_id}/seat",
    response_model=ReservationActionResponse,
    status_code=status.HTTP_202_ACCEPTED,
)
async def seat_reservation(
    reservation_id: str,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    """Mark a confirmed reservation as seated."""
    result = await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "update_status",
        {"restaurant_id": restaurant.id, "status": "seated"},
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationActionResponse(id=reservation_id, status=result.get("status", "seated"))


@router.post(
    "/{reservation_id}/complete",
    response_model=ReservationActionResponse,
    status_code=status.HTTP_202_ACCEPTED,
)
async def complete_reservation(
    reservation_id: str,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    """Mark a seated reservation as completed."""
    result = await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "update_status",
        {"restaurant_id": restaurant.id, "status": "completed"},
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationActionResponse(id=reservation_id, status=result.get("status", "completed"))


@router.post(
    "/{reservation_id}/no-show",
    response_model=ReservationActionResponse,
    status_code=status.HTTP_202_ACCEPTED,
)
async def no_show_reservation(
    reservation_id: str,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    """Mark a confirmed reservation as no-show."""
    result = await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "update_status",
        {"restaurant_id": restaurant.id, "status": "no_show"},
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationActionResponse(id=reservation_id, status=result.get("status", "no_show"))


@router.post(
    "/{reservation_id}/served",
    response_model=ReservationActionResponse,
    status_code=status.HTTP_202_ACCEPTED,
)
async def served_reservation(
    reservation_id: str,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    """Mark a completed reservation as served (table cleared)."""
    result = await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "mark_served",
        {"restaurant_id": restaurant.id},
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationActionResponse(id=reservation_id, status=result.get("status", "completed"))


@router.post(
    "/{reservation_id}/dishes",
    response_model=ReservationRead,
    status_code=status.HTTP_200_OK,
)
async def submit_dishes(
    reservation_id: str,
    body: DishSubmissionRequest,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    """Submit dish selection for an existing reservation (admin, post-creation)."""
    result = await session.execute(
        select(Reservation).where(
            Reservation.id == reservation_id,
            Reservation.restaurant_id == restaurant.id,
        )
    )
    reservation = result.scalar_one_or_none()
    if reservation is None:
        raise HTTPException(status_code=404, detail="Reservation not found")

    settings = restaurant.settings or {}
    if not settings.get("dish_chooser_enabled"):
        raise HTTPException(status_code=400, detail="Dish chooser feature is not enabled")

    if reservation.notes and "[Dishes]" in reservation.notes:
        raise HTTPException(
            status_code=409,
            detail="Dishes have already been submitted for this reservation",
        )

    from app.services.dish_chooser import (
        format_dishes_note,
        validate_freetext_dishes,
        validate_menu_dishes,
    )

    max_dishes = int(settings.get("dish_chooser_max_dishes", 5))

    try:
        if body.dishes:
            dish_note = await validate_menu_dishes(
                [d.model_dump() for d in body.dishes],
                restaurant.id,
                reservation.party_size,
                max_dishes,
                session,
            )
        elif body.dishes_text:
            dish_note = validate_freetext_dishes(body.dishes_text)
        else:
            raise HTTPException(status_code=422, detail="No dishes provided")
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc

    new_notes = format_dishes_note(reservation.notes, dish_note)
    await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "update",
        {"notes": new_notes, "restaurant_id": restaurant.id},
        mode="object",
    )

    result = await session.execute(
        select(Reservation).where(
            Reservation.id == reservation_id,
            Reservation.restaurant_id == restaurant.id,
        )
    )
    updated = result.scalar_one_or_none()
    if updated is None:
        raise HTTPException(
            status_code=500,
            detail="Reservation updated but could not be retrieved",
        )
    return ReservationRead.model_validate(updated)


@router.post(
    "/{reservation_id}/move",
    response_model=ReservationActionResponse,
    status_code=status.HTTP_202_ACCEPTED,
)
async def move_reservation(
    reservation_id: str,
    body: MoveReservationRequest,
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    payload: dict[str, Any] = {"restaurant_id": restaurant.id}
    if body.table_id:
        payload["table_id"] = body.table_id
        payload["combination_id"] = None
    elif body.combination_id:
        payload["table_id"] = None
        payload["combination_id"] = body.combination_id
    else:
        raise HTTPException(status_code=422, detail="Either table_id or combination_id is required")

    result = await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "move_table",
        payload,
        mode="object",
    )
    invalidate_restaurant_stats_cache(restaurant.id)
    return ReservationActionResponse(id=reservation_id, status=result.get("status", "confirmed"))
