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

import logfire
from pydantic_ai import ModelRetry, RunContext
from sqlmodel import select

from app.agents.deps import AgentDeps
from app.agents.errors import get_tool_message
from app.dependencies import restate_proxy
from app.models.customer import Customer
from app.models.reservation import Reservation
from app.models.restaurant import Restaurant
from app.services.availability import compute_available_slots
from app.services.service_blocks import (
    find_block_for_time,
    resolve_service_blocks,
    snap_to_interval,
)
from app.services.table_allocation import has_any_available_table
from app.utils.tz import local_day_window, naive_utc_to_local, resolve_tz


def _dish_selection_advice(settings: dict[str, Any], party_size: int) -> dict[str, Any] | None:
    """Return a structured dish pre-selection hint for this party size.

    Returns ``None`` when no pre-selection is required so the agent does
    not have to reason about an irrelevant field.

    The hint shape is:
    ``{"required": True, "mode": "menu"|"freetext", "max_distinct": int}``.
    """
    # Local import to avoid a cycle with services.dish_chooser, which
    # imports SQLModel pieces at module load.
    from app.services.dish_chooser import is_dish_chooser_required

    if not is_dish_chooser_required(settings, party_size, "agent"):
        return None
    return {
        "required": True,
        "mode": "menu" if settings.get("dish_chooser_from_menu") else "freetext",
        "max_distinct": int(settings.get("dish_chooser_max_dishes", 5)),
    }


def parse_restaurant_local_datetime(
    date_str: str, time_str: str, restaurant: Restaurant | None
) -> datetime:
    """Parse date/time as restaurant-local and convert to UTC.

    Returns a tz-aware UTC datetime so the saga's wire format (aware ISO
    with ``+00:00`` suffix) is preserved across the agent → workflow
    boundary. Falls back to the project default tz when the restaurant
    row carries an unknown / missing zone.
    """
    tz = resolve_tz(getattr(restaurant, "timezone", None))
    naive = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
    return naive.replace(tzinfo=tz).astimezone(UTC)


async def _load_restaurant(ctx: RunContext[AgentDeps]) -> Restaurant | None:
    result = await ctx.deps.session.execute(
        select(Restaurant).where(Restaurant.id == ctx.deps.restaurant_id)
    )
    return result.scalar_one_or_none()


async def check_availability_impl(
    ctx: RunContext[AgentDeps],
    date: str,
    time: str,
    party_size: int,
) -> dict[str, Any]:
    session = ctx.deps.session
    restaurant_id = ctx.deps.restaurant_id
    lang = getattr(ctx.deps, "language", "nl")

    restaurant = await _load_restaurant(ctx)
    settings_data = (restaurant.settings or {}) if restaurant else {}

    try:
        requested_dt = parse_restaurant_local_datetime(date, time, restaurant)
    except ValueError as err:
        raise ModelRetry(get_tool_message("invalid_date_time", lang)) from err

    # Promote back to naive-local for block / slot comparisons (the block
    # times and opening hours are stored as restaurant-local ``time``).
    tz = resolve_tz(getattr(restaurant, "timezone", None))
    requested_local = requested_dt.astimezone(tz).replace(tzinfo=None)

    now = datetime.now(UTC)
    min_advance_hours = int(settings_data.get("min_advance_hours", 1))
    max_advance_days = int(settings_data.get("max_advance_days", 90))
    max_party_size = int(settings_data.get("max_party_size", 20))

    if party_size > max_party_size:
        return {"available": False, "reason": "party_too_large", "max_party_size": max_party_size}

    if requested_dt < now + timedelta(hours=min_advance_hours):
        return {"available": False, "reason": "too_soon", "min_advance_hours": min_advance_hours}

    if requested_dt > now + timedelta(days=max_advance_days):
        return {
            "available": False,
            "reason": "too_far_in_advance",
            "max_advance_days": max_advance_days,
        }

    requested_time = requested_local.time()

    # Resolve service blocks (handles overrides)
    blocks = await resolve_service_blocks(session, restaurant_id, requested_local.date())

    matching_block = find_block_for_time(blocks, requested_time)

    if matching_block is None:
        return {"available": False, "reason": "closed"}

    block = matching_block.block

    # Snap to nearest valid slot if time doesn't align with interval
    snapped_time = snap_to_interval(requested_time, matching_block)
    if snapped_time is None:
        # No valid slot near this time. The proactive alternatives prompt
        # tells the agent to follow up with ``find_available_slots`` — we
        # intentionally do not return raw block slot lists here to keep
        # the agent on that single proactive path and to avoid leaking
        # the full block schedule.
        return {"available": False, "reason": "no_valid_slot"}

    # Block window for the cover-count query — built in restaurant-local
    # from the block boundaries, then converted to naive-UTC so it lines
    # up with the stored ``reserved_at`` (naive-UTC).
    requested_local_aware = requested_local.replace(tzinfo=tz)
    block_start = (
        requested_local_aware.replace(
            hour=block.start_time.hour,
            minute=block.start_time.minute,
            second=0,
            microsecond=0,
        )
        .astimezone(UTC)
        .replace(tzinfo=None)
    )
    block_end_dt = (
        requested_local_aware.replace(
            hour=block.end_time.hour,
            minute=block.end_time.minute,
            second=0,
            microsecond=0,
        )
        .astimezone(UTC)
        .replace(tzinfo=None)
    )

    count_result = await session.execute(
        select(Reservation).where(
            Reservation.restaurant_id == restaurant_id,
            Reservation.status.in_(["confirmed", "pending"]),  # type: ignore[attr-defined]
            Reservation.reserved_at >= block_start,
            Reservation.reserved_at < block_end_dt,
        )
    )
    existing = list(count_result.scalars().all())
    booked_covers = sum(r.party_size for r in existing)
    max_covers = block.max_covers

    if max_covers is not None and booked_covers + party_size > max_covers:
        # Booked / max cover counts are internal capacity numbers; the
        # agent only needs to know the slot is unavailable so it can call
        # ``find_available_slots`` for alternatives.
        return {"available": False, "reason": "fully_booked"}

    # Snap to restaurant-local then drop to naive-UTC: ``has_any_available_table``
    # queries the naive-UTC ``reserved_at`` / ``end_time`` columns.
    snapped_local = requested_local_aware.replace(
        hour=snapped_time.hour, minute=snapped_time.minute, second=0, microsecond=0
    )
    snapped_reserved_at = snapped_local.astimezone(UTC).replace(tzinfo=None)
    calculated_end = snapped_reserved_at + timedelta(minutes=block.default_duration_minutes or 90)

    table_available = await has_any_available_table(
        session, restaurant_id, party_size, snapped_reserved_at, calculated_end
    )
    if not table_available:
        return {"available": False, "reason": "no_tables_available"}

    # Combo options, block IDs, cover counts, and block window times are
    # internal scheduling data — never exposed to the agent.  The agent
    # only needs to know the slot is bookable (optionally with the snapped
    # time so it can mention the small adjustment to the guest).
    result: dict[str, Any] = {
        "available": True,
        "time": snapped_time.strftime("%H:%M"),
        "snapped": snapped_time != requested_time,
    }
    advice = _dish_selection_advice(settings_data, party_size)
    if advice is not None:
        result["dish_selection"] = advice
    return result


async def find_available_slots_impl(
    ctx: RunContext[AgentDeps],
    date: str,
    party_size: int,
) -> dict[str, Any]:
    """Return every bookable start time on *date* for *party_size*.

    Designed as the proactive follow-up to ``check_availability`` when the
    requested slot is unavailable: the agent calls this to recover concrete
    alternative times instead of asking the guest to guess.
    """
    lang = getattr(ctx.deps, "language", "nl")
    try:
        target_date = DateType.fromisoformat(date)
    except ValueError as err:
        raise ModelRetry(get_tool_message("invalid_date_time", lang)) from err

    restaurant = await _load_restaurant(ctx)
    if restaurant is None:
        return {
            "date": date,
            "party_size": party_size,
            "available_slots": [],
            "reason": "restaurant_not_found",
        }

    # Combo options are internal table-layout details — the agent must
    # never describe a specific table combination to a guest, so they
    # are stripped here.  The shared helper still returns them for the
    # public availability endpoint.
    result = await compute_available_slots(ctx.deps.session, restaurant, target_date, party_size)
    return {
        "date": date,
        "party_size": party_size,
        "available_slots": result["available_slots"],
        "reason": result["reason"],
    }


async def create_reservation_impl(
    ctx: RunContext[AgentDeps],
    guest_name: str,
    guest_email: str,
    guest_phone: str | None,
    date: str,
    time: str,
    party_size: int,
    notes: str | None = None,
    dishes: list[dict[str, Any]] | None = None,
    dishes_text: str | None = None,
) -> dict[str, Any]:
    guest_name = guest_name.strip().title()
    if not guest_name:
        return {"error": "Guest name is required"}

    guest_email = guest_email.strip().lower()

    # On WhatsApp the caller's phone is always known from the sender ID.
    # Use it as fallback so the customer record is always phone-linked.
    caller = ctx.deps.caller
    if not guest_phone and caller.channel == "whatsapp" and caller.identity_key:
        guest_phone = caller.identity_key
    lang = getattr(ctx.deps, "language", "nl")

    restaurant = await _load_restaurant(ctx)

    settings_data = (restaurant.settings or {}) if restaurant else {}

    # ── Dish chooser pre-validation ──────────────────────────────────
    advice = _dish_selection_advice(settings_data, party_size)
    if advice is not None and not dishes and not dishes_text:
        if advice["mode"] == "menu":
            instruction = (
                "Use search_menu to find dish options, then call "
                "create_reservation again with `dishes` (a list of "
                "{menu_item_name, quantity} entries summing to "
                f"{party_size}, at most {advice['max_distinct']} distinct items)."
            )
        else:
            instruction = (
                "Ask the guest in chat which dishes they would like for "
                f"the {party_size} guests (free-text is fine), then call "
                "create_reservation again with their reply passed in "
                "`dishes_text`."
            )
        return {
            "error": "dish_selection_required",
            "mode": advice["mode"],
            "max_distinct": advice["max_distinct"],
            "party_size": party_size,
            "message": (
                f"For a group of {party_size} we ask guests to pre-select dishes "
                f"before we confirm the reservation. {instruction} "
                "Phrase your reply to the guest as our request — first person "
                "plural ('we', 'our'), never 'the restaurant requires…'."
            ),
        }

    requires_confirmation = settings_data.get("reservation_mode", "auto") == "manual"

    try:
        reserved_at = parse_restaurant_local_datetime(date, time, restaurant)
    except ValueError as err:
        raise ModelRetry(get_tool_message("invalid_date_time", lang)) from err

    session = ctx.deps.session
    restaurant_id = ctx.deps.restaurant_id
    client = ctx.deps.http_client

    # Convert to naive UTC for DB comparison (stored without tzinfo)
    reserved_at_naive = reserved_at.replace(tzinfo=None)

    existing_reservation_result = await session.execute(
        select(Reservation)
        .where(
            Reservation.restaurant_id == restaurant_id,
            Reservation.guest_email == guest_email,
            Reservation.guest_name == guest_name,
            Reservation.party_size == party_size,
            Reservation.reserved_at == reserved_at_naive,
            Reservation.status.in_(["pending", "confirmed"]),  # type: ignore[attr-defined]
        )
        .limit(1)
    )
    existing_reservation = existing_reservation_result.scalar_one_or_none()
    if existing_reservation is not None:
        return {
            "reservation_id": existing_reservation.id,
            "status": existing_reservation.status,
            "reserved_at": existing_reservation.reserved_at.isoformat(),
            "idempotent_reuse": True,
            "requires_confirmation": requires_confirmation,
        }

    customer_result = await session.execute(
        select(Customer).where(
            Customer.restaurant_id == restaurant_id,
            Customer.email == guest_email,
        )
    )
    customer = customer_result.scalar_one_or_none()

    if customer is None:
        from app.models.customer import Customer as CustomerModel

        customer_id = str(uuid.uuid4())
        session.add(
            CustomerModel(
                id=customer_id,
                restaurant_id=restaurant_id,
                name=guest_name,
                email=guest_email,
                phone=guest_phone,
            )
        )
        await session.commit()  # Commit so the Restate handler (separate DB session) can see it
    else:
        customer_id = customer.id
        # Sync customer record with latest details provided during this
        # reservation.  The agent already confirmed these with the guest.
        changed = False
        if guest_name and customer.name != guest_name:
            customer.name = guest_name
            changed = True
        if guest_email and customer.email != guest_email:
            customer.email = guest_email
            changed = True
        if guest_phone and customer.phone != guest_phone:
            customer.phone = guest_phone
            changed = True
        if changed:
            session.add(customer)
            await session.commit()

    # ── Resolve agent dish names to menu item IDs ─────────────────────
    resolved_dishes = None
    if dishes:
        from app.models.menu_item import MenuItem

        resolved_dishes = []
        for dish in dishes:
            name = dish.get("menu_item_name", "").strip()
            qty = dish.get("quantity", 1)
            if not name:
                continue
            item_result = await session.execute(
                select(MenuItem)
                .where(
                    MenuItem.restaurant_id == restaurant_id,
                    MenuItem.is_active == True,  # noqa: E712
                    MenuItem.name.ilike(f"%{name}%"),  # type: ignore[union-attr]
                )
                .limit(1)
            )
            item = item_result.scalar_one_or_none()
            if item is None:
                return {
                    "error": "menu_item_not_found",
                    "message": f"Could not find active menu item matching '{name}'",
                }
            resolved_dishes.append({"menu_item_id": item.id, "quantity": qty})

    reservation_id = str(uuid.uuid4())
    payload = {
        "restaurant_id": restaurant_id,
        "customer_id": customer_id,
        "guest_name": guest_name,
        "guest_email": guest_email,
        "guest_phone": guest_phone,
        "party_size": party_size,
        "reserved_at": reserved_at.isoformat(),
        "notes": notes,
        "dishes": resolved_dishes,
        "dishes_text": dishes_text,
        "source": "agent",
    }
    # Durable write via Restate Virtual Object — exactly-once semantics.
    await restate_proxy(
        client,
        "ReservationWorkflow",
        reservation_id,
        "create_reservation",
        payload,
        mode="object",
    )

    return {
        "reservation_id": reservation_id,
        "status": "accepted",
        "reserved_at": reserved_at.isoformat(),
        "idempotent_reuse": False,
        "requires_confirmation": requires_confirmation,
    }


async def cancel_reservation_impl(
    ctx: RunContext[AgentDeps],
    reservation_id: str,
) -> dict[str, Any]:
    caller = ctx.deps.caller
    session = ctx.deps.session
    restaurant_id = ctx.deps.restaurant_id
    client = ctx.deps.http_client
    lang = getattr(ctx.deps, "language", "nl")

    if caller.channel == "dashboard":
        auth_decision = "allowed"
    elif not caller.verified:
        auth_decision = "denied"
    else:
        auth_decision = "scoped"

    logfire.info(
        "reservation_tool_access",
        caller_channel=caller.channel,
        caller_verified=caller.verified,
        caller_customer_id=caller.customer_id,
        restaurant_id=restaurant_id,
        tool_name="cancel_reservation",
        authorization_decision=auth_decision,
        reservation_id=reservation_id,
    )

    if caller.channel != "dashboard" and not caller.verified:
        return {"error": "verification_required"}

    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 ModelRetry(get_tool_message("reservation_not_found", lang))

    # Authorization: chat channels (whatsapp/website/…) may only act on
    # reservations they own. We require BOTH a linked customer on the caller
    # AND a matching customer on the reservation. The previous check used
    # only `!=`, which silently allowed `None != None` to fall through —
    # letting a WhatsApp/web caller with no linked Customer cancel
    # *orphan* reservations (walk-ins or pre-link bookings) belonging to
    # the restaurant.
    if caller.channel != "dashboard" and (
        caller.customer_id is None or reservation.customer_id != caller.customer_id
    ):
        return {"error": "forbidden"}

    if reservation.status == "cancelled":
        return {"reservation_id": reservation_id, "status": "already_cancelled"}

    # Durable status transition via Restate Virtual Object.
    await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "update_status",
        {"status": "cancelled", "restaurant_id": restaurant_id},
        mode="object",
    )

    if reservation.customer_id:
        customer_result = await session.execute(
            select(Customer).where(Customer.id == reservation.customer_id)
        )
        customer = customer_result.scalar_one_or_none()

        if customer:
            try:
                from app.email.scaleway import ScalewayEmailClient
                from app.models.restaurant import Restaurant as RestaurantModel

                rest_result = await session.execute(
                    select(RestaurantModel).where(RestaurantModel.id == restaurant_id)
                )
                restaurant = rest_result.scalar_one_or_none()
                if restaurant:
                    email_client = ScalewayEmailClient(client=client)
                    await email_client.send_reservation_cancellation(
                        reservation, customer, restaurant
                    )
            except Exception as exc:
                logfire.error("cancellation_email_failed", error=str(exc))

    return {"reservation_id": reservation_id, "status": "cancelled"}


async def modify_reservation_impl(
    ctx: RunContext[AgentDeps],
    reservation_id: str,
    date: str | None = None,
    time: str | None = None,
    party_size: int | None = None,
    notes: str | None = None,
) -> dict[str, Any]:
    """Modify an existing reservation's date, time, party_size, or notes."""
    caller = ctx.deps.caller
    session = ctx.deps.session
    restaurant_id = ctx.deps.restaurant_id
    client = ctx.deps.http_client
    lang = getattr(ctx.deps, "language", "nl")

    if caller.channel == "dashboard":
        auth_decision = "allowed"
    elif not caller.verified:
        auth_decision = "denied"
    else:
        auth_decision = "scoped"

    logfire.info(
        "reservation_tool_access",
        caller_channel=caller.channel,
        caller_verified=caller.verified,
        caller_customer_id=caller.customer_id,
        restaurant_id=restaurant_id,
        tool_name="modify_reservation",
        authorization_decision=auth_decision,
        reservation_id=reservation_id,
    )

    if caller.channel != "dashboard" and not caller.verified:
        return {"error": "verification_required"}

    # Verify reservation exists and belongs to this restaurant
    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 ModelRetry(get_tool_message("reservation_not_found", lang))

    if caller.channel != "dashboard" and (
        caller.customer_id is None or reservation.customer_id != caller.customer_id
    ):
        return {"error": "forbidden"}

    if reservation.status == "cancelled":
        raise ModelRetry(get_tool_message("reservation_cancelled", lang))

    restaurant = await _load_restaurant(ctx)
    settings_data = (restaurant.settings or {}) if restaurant else {}
    requires_confirmation = settings_data.get("reservation_mode", "auto") == "manual"

    # Determine what's changing
    slot_changing = date is not None or time is not None or party_size is not None

    update_payload: dict[str, Any] = {"restaurant_id": restaurant_id}

    if slot_changing:
        # Stored ``reserved_at`` is naive-UTC; promote to restaurant-local
        # to default the date/time fields the agent didn't override.
        tz = resolve_tz(getattr(restaurant, "timezone", None))
        current_local = naive_utc_to_local(reservation.reserved_at, tz)

        new_date = date or current_local.strftime("%Y-%m-%d")
        new_time = time or current_local.strftime("%H:%M")
        new_party_size = party_size if party_size is not None else reservation.party_size

        # Run availability check for the new slot
        availability = await check_availability_impl(ctx, new_date, new_time, new_party_size)
        if not availability.get("available"):
            raise ModelRetry(get_tool_message("slot_unavailable", lang))

        try:
            new_reserved_at = parse_restaurant_local_datetime(new_date, new_time, restaurant)
        except ValueError as err:
            raise ModelRetry(get_tool_message("invalid_date_time", lang)) from err

        update_payload["reserved_at"] = new_reserved_at.isoformat()
        if party_size is not None:
            update_payload["party_size"] = party_size

    if notes is not None:
        update_payload["notes"] = notes

    # Proxy update through Restate
    await restate_proxy(
        client,
        "ReservationObject",
        reservation_id,
        "update",
        update_payload,
        mode="object",
    )

    updated = []
    if date is not None:
        updated.append("date")
    if time is not None:
        updated.append("time")
    if party_size is not None:
        updated.append("party_size")
    if notes is not None:
        updated.append("notes")

    return {
        "reservation_id": reservation_id,
        "status": "modified",
        "updated_fields": updated,
        "requires_confirmation": requires_confirmation,
    }


async def find_reservation_impl(
    ctx: RunContext[AgentDeps],
    customer_email: str | None = None,
    customer_name: str | None = None,
    date: str | None = None,
) -> dict[str, Any]:
    caller = ctx.deps.caller
    session = ctx.deps.session
    restaurant_id = ctx.deps.restaurant_id

    if caller.channel == "dashboard":
        authorization_decision = "allowed"
    elif not caller.verified:
        authorization_decision = "denied"
    elif caller.customer_id is not None:
        authorization_decision = "scoped"
    else:
        authorization_decision = "scoped"

    logfire.info(
        "reservation_tool_access",
        caller_channel=caller.channel,
        caller_verified=caller.verified,
        caller_customer_id=caller.customer_id,
        restaurant_id=restaurant_id,
        tool_name="find_reservation",
        authorization_decision=authorization_decision,
    )

    if caller.channel == "dashboard":
        pass
    elif not caller.verified:
        return {
            "error": "verification_required",
            "message": "Please verify your identity before accessing reservation information.",
        }
    elif caller.customer_id is not None:
        query = select(Reservation).where(
            Reservation.restaurant_id == restaurant_id,
            Reservation.customer_id == caller.customer_id,
        )
        if date:
            try:
                parsed_date = datetime.strptime(date, "%Y-%m-%d").date()
            except ValueError:
                parsed_date = None
            if parsed_date is not None:
                # The agent thinks in restaurant-local dates ("today",
                # "tomorrow"); reserved_at is stored naive-UTC. Convert the
                # date to a naive-UTC ``[start, end)`` window via the
                # restaurant's timezone so reservations near local midnight
                # land in the correct bucket.
                restaurant_for_tz = await _load_restaurant(ctx)
                tz = resolve_tz(getattr(restaurant_for_tz, "timezone", None))
                day_start, day_end = local_day_window(parsed_date, tz)
                query = query.where(
                    Reservation.reserved_at >= day_start,
                    Reservation.reserved_at < day_end,
                )
        result = await session.execute(query.order_by(Reservation.reserved_at.desc()).limit(20))  # type: ignore[attr-defined]
        reservations = list(result.scalars().all())

        if not reservations:
            return {"reservations": [], "message": "Geen reserveringen gevonden"}

        return {
            "reservations": [
                {
                    "reservation_id": r.id,
                    "reserved_at": r.reserved_at.isoformat(),
                    "party_size": r.party_size,
                    "status": r.status,
                    "guest_name": r.guest_name,
                }
                for r in reservations
            ]
        }
    else:
        return {"reservations": [], "message": "Geen reserveringen gevonden"}

    query = select(Reservation).where(Reservation.restaurant_id == restaurant_id)

    if customer_email:
        query = query.where(Reservation.guest_email == customer_email)
    if customer_name:
        query = query.where(
            Reservation.guest_name.ilike(f"%{customer_name}%")  # type: ignore[attr-defined]
        )
    if date:
        try:
            parsed_date = datetime.strptime(date, "%Y-%m-%d").date()
        except ValueError:
            parsed_date = None
        if parsed_date is not None:
            # See the comment above — naive-UTC window keyed off the
            # restaurant timezone, not the server timezone.
            restaurant_for_tz = await _load_restaurant(ctx)
            tz = resolve_tz(getattr(restaurant_for_tz, "timezone", None))
            day_start, day_end = local_day_window(parsed_date, tz)
            query = query.where(
                Reservation.reserved_at >= day_start,
                Reservation.reserved_at < day_end,
            )

    result = await session.execute(query.order_by(Reservation.reserved_at.desc()).limit(20))  # type: ignore[attr-defined]
    reservations = list(result.scalars().all())

    if not reservations:
        return {"reservations": [], "message": "Geen reserveringen gevonden"}

    return {
        "reservations": [
            {
                "reservation_id": r.id,
                "reserved_at": r.reserved_at.isoformat(),
                "party_size": r.party_size,
                "status": r.status,
                "guest_name": r.guest_name,
                "guest_email": r.guest_email,
            }
            for r in reservations
        ]
    }


async def get_restaurant_info_impl(
    ctx: RunContext[AgentDeps],
) -> dict[str, Any]:
    session = ctx.deps.session
    restaurant_id = ctx.deps.restaurant_id

    from app.models.restaurant import Restaurant as RestaurantModel

    rest_result = await session.execute(
        select(RestaurantModel).where(RestaurantModel.id == restaurant_id)
    )
    restaurant = rest_result.scalar_one_or_none()
    if not restaurant:
        return {"error": "Restaurant niet gevonden"}

    from app.models.service_block import ServiceBlock

    blocks_result = await session.execute(
        select(ServiceBlock)
        .where(ServiceBlock.restaurant_id == restaurant_id, ServiceBlock.is_active == True)  # noqa: E712
        .order_by(ServiceBlock.day_of_week, ServiceBlock.start_time)  # type: ignore[arg-type]
    )
    blocks = list(blocks_result.scalars().all())

    day_names = ["maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag", "zondag"]
    opening_hours: dict[str, Any] = {}
    for block in blocks:
        if block.block_type != "open":
            continue
        day = day_names[block.day_of_week]
        hours = f"{block.start_time.strftime('%H:%M')} - {block.end_time.strftime('%H:%M')}"
        opening_hours.setdefault(day, []).append(hours)

    settings_data = restaurant.settings or {}
    # Allowlist of guest-relevant settings.  Everything else in
    # ``restaurant.settings`` (agent config, custom prompts, feature
    # flags, table-layout config, internal capacity numbers,
    # in-service mode, notification preferences, …) is internal and
    # MUST NOT be exposed to the model.
    address = {
        "street": settings_data.get("address_street"),
        "city": settings_data.get("address_city"),
        "postcode": settings_data.get("address_postcode"),
        "country": settings_data.get("address_country"),
    }
    info: dict[str, Any] = {
        "name": restaurant.name,
        "phone": restaurant.phone,
        "timezone": restaurant.timezone,
        "opening_hours": opening_hours,
    }
    if any(address.values()):
        info["address"] = address
    if settings_data.get("description"):
        info["description"] = settings_data["description"]
    if settings_data.get("cancellation_policy"):
        info["cancellation_policy"] = settings_data["cancellation_policy"]
    return info
