"""Table and Chair CRUD — includes chair auto-generation and toggle endpoints."""

import uuid
from datetime import date as date_type
from datetime import datetime, timedelta
from typing import Any, cast

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select

from app.auth.tenant import get_current_restaurant, get_tenant_session
from app.models.chair import Chair
from app.models.reservation import Reservation
from app.models.restaurant import Restaurant
from app.models.table import FloorTable
from app.models.table_combination import TableCombination
from app.models.zone import Zone
from app.schemas.chair import ChairRead, ChairUpdate
from app.schemas.table import (
    AvailabilityResponse,
    CombinationAvailabilityItem,
    FloorTableCreate,
    FloorTableRead,
    FloorTableUpdate,
    TableAvailabilityItem,
    TableStatus,
    TableStatusReservation,
)
from app.services.table_allocation import (
    _combo_has_time_conflict,
    _rank_combo_candidates,
    _table_has_time_conflict,
)
from app.services.table_combinations import is_in_combination
from app.services.tables import generate_chairs, has_future_reservations

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


# ── Helpers ──────────────────────────────────────────────────────────────────


async def _get_or_create_default_zone(session: AsyncSession, restaurant_id: str) -> Zone:
    """Return the first zone for the restaurant, creating a "Main" zone if none exist."""
    result = await session.execute(
        select(Zone)
        .where(Zone.restaurant_id == restaurant_id)
        .order_by(cast(Any, Zone.display_order))
        .limit(1)
    )
    zone = result.scalar_one_or_none()
    if zone is not None:
        return zone

    zone = Zone(
        id=str(uuid.uuid4()),
        restaurant_id=restaurant_id,
        name="Main",
        display_order=0,
    )
    session.add(zone)
    await session.flush()  # get id without committing
    return zone


async def _load_table_with_chairs(session: AsyncSession, table: FloorTable) -> FloorTableRead:
    """Build a FloorTableRead response with nested chairs."""
    chair_result = await session.execute(
        select(Chair).where(Chair.table_id == table.id).order_by(cast(Any, Chair.slot_index))
    )
    chairs = [
        ChairRead(
            id=c.id,
            table_id=c.table_id,
            slot_index=c.slot_index,
            side=c.side,
            enabled=c.enabled,
        )
        for c in chair_result.scalars().all()
    ]
    return FloorTableRead(
        id=table.id,
        restaurant_id=table.restaurant_id,
        zone_id=table.zone_id,
        label=table.label,
        x=table.x,
        y=table.y,
        width=table.width,
        height=table.height,
        shape=table.shape,
        rotation=table.rotation,
        slot_count=table.slot_count,
        chairs=chairs,
    )


# ── CRUD ─────────────────────────────────────────────────────────────────────


@router.get("/", response_model=list[FloorTableRead])
async def list_tables(
    zone_id: str | None = None,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> list[Any]:
    query = select(FloorTable).where(FloorTable.restaurant_id == restaurant.id)
    if zone_id is not None:
        query = query.where(FloorTable.zone_id == zone_id)
    result = await session.execute(query)
    tables = list(result.scalars().all())

    responses: list[FloorTableRead] = []
    for table in tables:
        responses.append(await _load_table_with_chairs(session, table))
    return responses


@router.get("/status", response_model=list[TableStatus])
async def get_table_statuses(
    date: str,
    time: str,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> list[Any]:
    try:
        query_date = date_type.fromisoformat(date)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail="Invalid date format. Use YYYY-MM-DD") from exc

    try:
        query_time = datetime.strptime(time, "%H:%M").time()
    except ValueError as exc:
        raise HTTPException(status_code=422, detail="Invalid time format. Use HH:MM") from exc

    query_datetime = datetime.combine(query_date, query_time)

    table_result = await session.execute(
        select(FloorTable).where(FloorTable.restaurant_id == restaurant.id)
    )
    tables = list(table_result.scalars().all())

    day_start = datetime(query_date.year, query_date.month, query_date.day, 0, 0, 0)
    day_end = datetime(query_date.year, query_date.month, query_date.day, 23, 59, 59)
    reservation_result = await session.execute(
        select(Reservation)
        .where(
            Reservation.restaurant_id == restaurant.id,
            Reservation.reserved_at >= day_start,
            Reservation.reserved_at <= day_end,
        )
        .order_by(cast(Any, Reservation.reserved_at).asc())
    )
    reservations = list(reservation_result.scalars().all())

    settings = restaurant.settings or {}
    in_service_mode = settings.get("in_service_mode", "auto")
    duration_value = settings.get("in_service_duration_minutes", 90)
    try:
        in_service_duration_minutes = int(duration_value)
    except (TypeError, ValueError):
        in_service_duration_minutes = 90

    # Build mapping of table_id → combo reservations so combination-booked tables show as occupied
    combo_result = await session.execute(
        select(TableCombination).where(TableCombination.restaurant_id == restaurant.id)
    )
    combinations = list(combo_result.scalars().all())

    table_to_combo_reservations: dict[str, list[Reservation]] = {}
    for combo in combinations:
        combo_reservations = [r for r in reservations if r.combination_id == combo.id]
        for tid in combo.table_ids:
            table_to_combo_reservations.setdefault(tid, []).extend(combo_reservations)

    responses: list[TableStatus] = []
    for table in tables:
        status = "available"
        matched: Reservation | None = None

        # Include direct table reservations + combination reservations; deduplicate
        direct = [r for r in reservations if r.table_id == table.id]
        combo_res = table_to_combo_reservations.get(table.id, [])
        seen_ids: set[str] = set()
        table_reservations: list[Reservation] = []
        for r in direct + combo_res:
            if r.id not in seen_ids:
                seen_ids.add(r.id)
                table_reservations.append(r)
        for reservation in table_reservations:
            if (
                reservation.status in {"confirmed", "pending", "seated"}
                and reservation.reserved_at <= query_datetime < reservation.end_time
            ):
                matched = reservation
                status = "occupied" if reservation.status == "seated" else "reserved"
                break

            if reservation.status == "completed":
                anchor = reservation.completed_at or reservation.end_time

                # If staff already marked the table as served/cleared, skip entirely
                if reservation.served_at:
                    continue

                # Occupied window: [reserved_at, completed_at) — guest was seated
                if reservation.reserved_at <= query_datetime < anchor:
                    matched = reservation
                    status = "occupied"
                    break

                # In-service window: [completed_at, completed_at + duration)
                in_service_end = anchor + timedelta(minutes=in_service_duration_minutes)
                if in_service_mode in {"auto", "manual"} and (
                    anchor <= query_datetime < in_service_end
                ):
                    matched = reservation
                    status = "in_service"
                    break

        responses.append(
            TableStatus(
                table_id=table.id,
                table_label=table.label,
                status=status,
                reservation=(
                    TableStatusReservation(
                        id=matched.id,
                        guest_name=matched.guest_name,
                        party_size=matched.party_size,
                        reserved_at=matched.reserved_at,
                        end_time=matched.end_time,
                        status=matched.status,
                    )
                    if matched is not None
                    else None
                ),
            )
        )

    return responses


@router.get("/availability/", response_model=AvailabilityResponse)
async def get_table_availability(
    party_size: int,
    start: str,
    duration_minutes: int,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> Any:
    """Return available tables and combinations for walk-in assignment."""
    try:
        start_dt = datetime.fromisoformat(start).replace(tzinfo=None)
    except ValueError as exc:
        raise HTTPException(
            status_code=422, detail="Invalid start datetime. Use ISO format."
        ) from exc

    end_dt = start_dt + timedelta(minutes=duration_minutes)

    # Fetch all tables with their enabled chair count (capacity) and
    # combo membership count (how many combinations each table belongs to).
    combo_membership = (
        select(func.count())
        .where(
            TableCombination.restaurant_id == restaurant.id,
            TableCombination.table_ids.op("@>")(func.jsonb_build_array(FloorTable.id)),  # type: ignore[attr-defined]
        )
        .correlate(FloorTable)
        .scalar_subquery()
        .label("combo_membership")
    )

    table_result = await session.execute(
        select(
            FloorTable.id,
            FloorTable.label,
            func.count(Chair.id).label("capacity"),
            combo_membership,
        )
        .outerjoin(Chair, (Chair.table_id == FloorTable.id) & (Chair.enabled == True))  # noqa: E712
        .where(FloorTable.restaurant_id == restaurant.id)
        .group_by(FloorTable.id, FloorTable.label)
    )
    table_rows = table_result.all()

    table_items: list[tuple[TableAvailabilityItem, int]] = []
    for row in table_rows:
        tid, label, capacity, membership = row[0], row[1], row[2], row[3]
        if capacity < party_size:
            available = False
        else:
            available = not await _table_has_time_conflict(session, tid, start_dt, end_dt)
        table_items.append(
            (
                TableAvailabilityItem(
                    table_id=tid, table_label=label, capacity=capacity, available=available
                ),
                membership,
            )
        )

    # Sort: available first, then fewest combo memberships, then smallest
    # capacity (best fit). Mirrors the automatic allocation heuristic.
    table_items.sort(key=lambda t: (not t[0].available, t[1], t[0].capacity))

    # Fetch all combinations
    combo_result = await session.execute(
        select(TableCombination).where(TableCombination.restaurant_id == restaurant.id)
    )
    combos = list(combo_result.scalars().all())

    combo_items: list[CombinationAvailabilityItem] = []
    for combo in combos:
        if combo.combined_capacity < party_size:
            available = False
        else:
            available = not await _combo_has_time_conflict(session, combo, start_dt, end_dt)
        combo_items.append(
            CombinationAvailabilityItem(
                id=combo.id,
                name=combo.name,
                table_ids=combo.table_ids,
                combined_capacity=combo.combined_capacity,
                available=available,
            )
        )

    # Sort combos: available first, then by overlap score (fewest shared
    # tables with other combos), then smallest capacity.  Reuse the ranking
    # logic from the allocation service to build the overlap map.
    ranked_combos = _rank_combo_candidates(
        [c for c in combos if c.combined_capacity >= party_size], combos
    )
    combo_rank = {c.id: idx for idx, c in enumerate(ranked_combos)}
    # Undersized combos get a rank after all valid candidates.
    fallback_rank = len(ranked_combos)
    combo_items.sort(
        key=lambda ci: (
            not ci.available,
            combo_rank.get(ci.id, fallback_rank),
            ci.combined_capacity,
        )
    )

    return AvailabilityResponse(tables=[t for t, _ in table_items], combinations=combo_items)


@router.post("/", response_model=FloorTableRead, status_code=status.HTTP_201_CREATED)
async def create_table(
    payload: FloorTableCreate,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> Any:
    # Resolve zone — auto-create default if not provided
    zone_id = payload.zone_id
    if zone_id is None:
        zone = await _get_or_create_default_zone(session, restaurant.id)
        zone_id = zone.id
    else:
        # Validate zone belongs to restaurant
        zone_result = await session.execute(
            select(Zone).where(Zone.id == zone_id, Zone.restaurant_id == restaurant.id)
        )
        if zone_result.scalar_one_or_none() is None:
            raise HTTPException(status_code=404, detail="Zone not found")

    table = FloorTable(
        **payload.model_dump(exclude={"zone_id"}),
        zone_id=zone_id,
        restaurant_id=restaurant.id,
    )
    session.add(table)
    await session.flush()  # need table.id for chairs

    # Auto-generate chairs
    chairs = generate_chairs(table)
    for chair in chairs:
        session.add(chair)

    await session.commit()
    await session.refresh(table)
    return await _load_table_with_chairs(session, table)


@router.put("/{table_id}", response_model=FloorTableRead)
async def update_table(
    table_id: str,
    payload: FloorTableUpdate,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> Any:
    result = await session.execute(
        select(FloorTable).where(
            FloorTable.id == table_id, FloorTable.restaurant_id == restaurant.id
        )
    )
    table = result.scalar_one_or_none()
    if table is None:
        raise HTTPException(status_code=404, detail="Table not found")

    # Check if shape or slot_count changed — triggers chair regeneration
    shape_changed = payload.shape != table.shape
    slot_count_changed = payload.slot_count != table.slot_count
    needs_regen = shape_changed or slot_count_changed

    if needs_regen and await has_future_reservations(session, table_id):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Cannot change shape or seat count while future reservations exist.",
        )

    if needs_regen and await is_in_combination(session, table_id):
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=(
                "Cannot change shape or seat count while table is part of a combination."
                " Remove from combinations first."
            ),
        )

    # Validate zone if provided
    if payload.zone_id is not None:
        zone_result = await session.execute(
            select(Zone).where(Zone.id == payload.zone_id, Zone.restaurant_id == restaurant.id)
        )
        if zone_result.scalar_one_or_none() is None:
            raise HTTPException(status_code=404, detail="Zone not found")

    # Apply updates
    update_data = payload.model_dump(exclude_unset=True)
    for key, value in update_data.items():
        if value is not None or key != "zone_id":  # skip None zone_id
            setattr(table, key, value)

    if needs_regen:
        # Wipe existing chairs
        old_chairs = await session.execute(select(Chair).where(Chair.table_id == table_id))
        for old_chair in old_chairs.scalars().all():
            await session.delete(old_chair)
        await session.flush()

        # Regenerate from new config
        new_chairs = generate_chairs(table)
        for chair in new_chairs:
            session.add(chair)

    session.add(table)
    await session.commit()
    await session.refresh(table)
    return await _load_table_with_chairs(session, table)


@router.delete("/{table_id}", status_code=status.HTTP_200_OK)
async def delete_table(
    table_id: str,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> dict[str, str]:
    result = await session.execute(
        select(FloorTable).where(
            FloorTable.id == table_id, FloorTable.restaurant_id == restaurant.id
        )
    )
    table = result.scalar_one_or_none()
    if table is None:
        raise HTTPException(status_code=404, detail="Table not found")

    # Cascade delete chairs
    chair_result = await session.execute(select(Chair).where(Chair.table_id == table_id))
    for chair in chair_result.scalars().all():
        await session.delete(chair)

    await session.delete(table)
    await session.commit()
    return {"status": "deleted"}


# ── Chair toggle (nested under /tables) ──────────────────────────────────────


@router.patch(
    "/{table_id}/chairs/{chair_id}",
    response_model=ChairRead,
)
async def toggle_chair(
    table_id: str,
    chair_id: str,
    payload: ChairUpdate,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> Any:
    # Verify table belongs to restaurant
    table_result = await session.execute(
        select(FloorTable).where(
            FloorTable.id == table_id, FloorTable.restaurant_id == restaurant.id
        )
    )
    if table_result.scalar_one_or_none() is None:
        raise HTTPException(status_code=404, detail="Table not found")

    # Verify chair belongs to table
    result = await session.execute(
        select(Chair).where(Chair.id == chair_id, Chair.table_id == table_id)
    )
    chair = result.scalar_one_or_none()
    if chair is None:
        raise HTTPException(status_code=404, detail="Chair not found")

    chair.enabled = payload.enabled
    session.add(chair)
    await session.commit()
    await session.refresh(chair)
    return chair
