from typing import Any

import sqlalchemy as sa
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import col, select

from app.auth.tenant import get_current_restaurant, get_tenant_session
from app.models.reservation import Reservation
from app.models.restaurant import Restaurant
from app.models.service_block import ServiceBlock, ServiceBlockZone
from app.models.table import FloorTable
from app.models.zone import Zone
from app.schemas.service_block import ServiceBlockCreate, ServiceBlockRead, ServiceBlockUpdate

router = APIRouter(prefix="/service-blocks", tags=["service-blocks"])


def _validate_slot_interval(slot_interval_minutes: int | None) -> None:
    """Reject with 422 if slot_interval_minutes is not a positive multiple of 15."""
    if slot_interval_minutes is None:
        return
    if slot_interval_minutes <= 0 or slot_interval_minutes % 15 != 0:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="slot_interval_minutes must be a positive multiple of 15",
        )


async def _check_overlap(
    session: AsyncSession,
    restaurant_id: str,
    day_of_week: int,
    start_time: Any,
    end_time: Any,
    exclude_id: str | None = None,
) -> None:
    """Reject with 409 if a time range overlaps an existing block for the same day."""
    query = select(ServiceBlock).where(
        ServiceBlock.restaurant_id == restaurant_id,
        ServiceBlock.day_of_week == day_of_week,
        ServiceBlock.start_time < end_time,
        ServiceBlock.end_time > start_time,
    )
    if exclude_id is not None:
        query = query.where(ServiceBlock.id != exclude_id)
    result = await session.execute(query)
    if result.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Time range overlaps an existing block for this day",
        )


async def _validate_zone_ids(
    session: AsyncSession,
    restaurant_id: str,
    zone_ids: list[str],
) -> None:
    """Reject with 422 if any zone IDs don't belong to the restaurant."""
    if not zone_ids:
        return
    result = await session.execute(
        select(Zone.id).where(
            col(Zone.id).in_(zone_ids),
            Zone.restaurant_id == restaurant_id,
        )
    )
    found = {row for row in result.scalars().all()}
    missing = set(zone_ids) - found
    if missing:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail=f"Invalid zone IDs: {', '.join(sorted(missing))}",
        )


async def _check_zone_deselection_guard(
    session: AsyncSession,
    restaurant_id: str,
    day_of_week: int,
    start_time: Any,
    end_time: Any,
    removed_zone_ids: set[str],
) -> None:
    """Reject if future reservations exist in zones being removed."""
    if not removed_zone_ids:
        return
    from sqlalchemy import func

    # Compute restaurant-local now at SQL level per row
    local_now = func.timezone(Restaurant.timezone, func.now())
    # Find future reservations on tables in removed zones during this block's window.
    # PostgreSQL extract('dow') returns 0=Sunday; our day_of_week is 0=Monday.
    # Conversion: (day_of_week + 1) % 7 maps Monday(0)->1 ... Sunday(6)->0.
    result = await session.execute(
        select(Reservation.id, FloorTable.zone_id)
        .join(FloorTable, Reservation.table_id == FloorTable.id)  # type: ignore[arg-type]
        .join(Restaurant, Reservation.restaurant_id == Restaurant.id)  # type: ignore[arg-type]
        .where(
            Reservation.restaurant_id == restaurant_id,
            Reservation.status.in_(["pending", "confirmed"]),  # type: ignore[attr-defined]
            Reservation.reserved_at >= local_now,
            col(FloorTable.zone_id).in_(list(removed_zone_ids)),
            sa.extract("dow", Reservation.reserved_at) == (day_of_week + 1) % 7,  # type: ignore[arg-type]
            sa.cast(Reservation.reserved_at, sa.Time) >= start_time,
            sa.cast(Reservation.reserved_at, sa.Time) < end_time,
        )
    )
    conflicts = list(result.all())
    if conflicts:
        zone_counts: dict[str, int] = {}
        for _, zone_id in conflicts:
            zone_counts[zone_id] = zone_counts.get(zone_id, 0) + 1
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "message": "Cannot remove zones with future reservations",
                "conflicting_zones": zone_counts,
            },
        )


async def _get_zone_ids_for_block(session: AsyncSession, block_id: str) -> list[str]:
    """Return the open_zone_ids for a single block."""
    result = await session.execute(
        select(ServiceBlockZone.zone_id).where(ServiceBlockZone.service_block_id == block_id)
    )
    return list(result.scalars().all())


async def _sync_block_zones(
    session: AsyncSession,
    block_id: str,
    zone_ids: list[str],
    restaurant_id: str,
) -> None:
    """Replace all ServiceBlockZone rows for the given block."""
    await session.execute(
        sa.delete(ServiceBlockZone).where(ServiceBlockZone.service_block_id == block_id)  # type: ignore[arg-type]
    )
    for zone_id in zone_ids:
        session.add(
            ServiceBlockZone(
                service_block_id=block_id,
                zone_id=zone_id,
                restaurant_id=restaurant_id,
            )
        )


def _block_to_read(block: ServiceBlock, open_zone_ids: list[str]) -> dict[str, Any]:
    """Convert a ServiceBlock ORM instance + zone IDs into a ServiceBlockRead-compatible dict."""
    return {
        "id": block.id,
        "restaurant_id": block.restaurant_id,
        "day_of_week": block.day_of_week,
        "name": block.name,
        "block_type": block.block_type,
        "start_time": block.start_time,
        "end_time": block.end_time,
        "max_covers": block.max_covers,
        "default_duration_minutes": block.default_duration_minutes,
        "is_active": block.is_active,
        "display_order": block.display_order,
        "slot_interval_minutes": block.slot_interval_minutes,
        "open_zone_ids": open_zone_ids,
    }


@router.get("/", response_model=list[ServiceBlockRead])
async def list_service_blocks(
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> list[Any]:
    result = await session.execute(
        select(ServiceBlock)
        .where(ServiceBlock.restaurant_id == restaurant.id)
        .order_by(ServiceBlock.day_of_week, ServiceBlock.display_order)  # type: ignore[arg-type]
    )
    blocks = list(result.scalars().all())
    if not blocks:
        return []

    # Batch-fetch zone assignments for all blocks
    block_ids = [b.id for b in blocks]
    zone_result = await session.execute(
        select(ServiceBlockZone.service_block_id, ServiceBlockZone.zone_id).where(
            col(ServiceBlockZone.service_block_id).in_(block_ids)
        )
    )
    zone_map: dict[str, list[str]] = {bid: [] for bid in block_ids}
    for sb_id, z_id in zone_result.all():
        zone_map[sb_id].append(z_id)

    return [_block_to_read(b, zone_map.get(b.id, [])) for b in blocks]


@router.post("/", response_model=ServiceBlockRead, status_code=status.HTTP_201_CREATED)
async def create_service_block(
    payload: ServiceBlockCreate,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> Any:
    _validate_slot_interval(payload.slot_interval_minutes)
    await _check_overlap(
        session, restaurant.id, payload.day_of_week, payload.start_time, payload.end_time
    )

    # Exclude open_zone_ids — not a column on ServiceBlock
    block_data = payload.model_dump(exclude={"open_zone_ids"})
    block = ServiceBlock(**block_data, restaurant_id=restaurant.id)
    session.add(block)
    await session.commit()
    await session.refresh(block)

    zone_ids = payload.open_zone_ids
    if zone_ids:
        await _validate_zone_ids(session, restaurant.id, zone_ids)
        await _sync_block_zones(session, block.id, zone_ids, restaurant.id)
        await session.commit()

    return _block_to_read(block, zone_ids)


@router.put("/{block_id}", response_model=ServiceBlockRead)
async def update_service_block(
    block_id: str,
    payload: ServiceBlockUpdate,
    session: AsyncSession = Depends(get_tenant_session),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> Any:
    result = await session.execute(
        select(ServiceBlock).where(
            ServiceBlock.id == block_id, ServiceBlock.restaurant_id == restaurant.id
        )
    )
    block = result.scalar_one_or_none()
    if block is None:
        raise HTTPException(status_code=404, detail="Service block not found")

    _validate_slot_interval(payload.slot_interval_minutes)

    await _check_overlap(
        session,
        restaurant.id,
        payload.day_of_week,
        payload.start_time,
        payload.end_time,
        exclude_id=block_id,
    )

    # --- Zone handling ---
    old_zone_ids = set(await _get_zone_ids_for_block(session, block_id))
    new_zone_ids = set(payload.open_zone_ids)
    removed_zone_ids = old_zone_ids - new_zone_ids

    if removed_zone_ids:
        await _check_zone_deselection_guard(
            session,
            restaurant.id,
            payload.day_of_week,
            payload.start_time,
            payload.end_time,
            removed_zone_ids,
        )

    if new_zone_ids:
        await _validate_zone_ids(session, restaurant.id, list(new_zone_ids))

    # --- Update block fields (exclude open_zone_ids) ---
    for key, value in payload.model_dump(exclude={"open_zone_ids"}).items():
        setattr(block, key, value)

    session.add(block)

    # --- Sync zone rows ---
    await _sync_block_zones(session, block_id, payload.open_zone_ids, restaurant.id)
    await session.commit()
    await session.refresh(block)

    return _block_to_read(block, payload.open_zone_ids)


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

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