import re
import secrets
from typing import Any

import httpx
import logfire
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import ValidationError
from sqlalchemy import func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import flag_modified
from sqlmodel import SQLModel, 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 get_restaurant_stats_cached, set_restaurant_stats_cached
from app.db.session import commit_write, get_session
from app.dependencies import get_restate_client, restate_proxy
from app.models.auth_mirror import Team
from app.models.conversation import Conversation, Message
from app.models.order import Order
from app.models.reservation import Reservation
from app.models.restaurant import Restaurant, RestaurantPatch, RestaurantRead
from app.schemas.agent_config import AgentSettings


def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> None:
    """Recursively merge override into base in-place."""
    for key, value in override.items():
        if key in base and isinstance(base[key], dict) and isinstance(value, dict):
            _deep_merge(base[key], value)
        else:
            base[key] = value


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


def _local_day_window(
    restaurant_timezone: str,
    now_utc: Any = None,
) -> tuple[Any, Any, Any]:
    """Return ``(day_start, day_end, week_start)`` in naive-UTC.

    The boundaries identify the same wall-clock instants as midnight /
    next midnight / monday-midnight in the restaurant's local timezone,
    converted to UTC and stripped of tzinfo so they line up with the
    naive-UTC ``reserved_at`` / ``end_time`` columns.
    """
    from datetime import UTC, datetime, timedelta

    from app.utils.tz import resolve_tz

    if now_utc is None:
        now_utc = datetime.now(UTC)

    tz = resolve_tz(restaurant_timezone)

    local_now = now_utc.astimezone(tz)
    local_day_start = local_now.replace(hour=0, minute=0, second=0, microsecond=0)
    local_day_end = local_day_start + timedelta(days=1)
    local_week_start = local_day_start - timedelta(days=local_day_start.weekday())

    return (
        local_day_start.astimezone(UTC).replace(tzinfo=None),
        local_day_end.astimezone(UTC).replace(tzinfo=None),
        local_week_start.astimezone(UTC).replace(tzinfo=None),
    )


class OnboardingCreate(SQLModel):
    name: str
    address: str
    phone: str
    email: str
    business_type: str
    team_id: str


async def _generate_slug(name: str, session: AsyncSession) -> str:
    base = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
    candidate = base
    for _ in range(5):
        result = await session.execute(select(Restaurant).where(Restaurant.slug == candidate))
        if result.scalars().first() is None:
            return candidate
        candidate = f"{base}-{secrets.token_hex(2)}"
    raise HTTPException(status_code=409, detail="Could not generate unique slug")


@router.post("/", response_model=RestaurantRead, status_code=status.HTTP_201_CREATED)
async def create_restaurant(
    payload: OnboardingCreate,
    current_user: CurrentUser = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
) -> Any:
    """Create a restaurant. SvelteKit must have already created the BA team
    via `auth.api.createTeam` and passed its id in `team_id`.

    We verify the team belongs to the user's active organization (defends
    against tampered or stale claims), then insert.
    """
    if not current_user.active_org_id:
        raise HTTPException(status_code=403, detail="No active organization")

    team_check = await session.execute(select(Team).where(Team.id == payload.team_id).limit(1))
    team = team_check.scalar_one_or_none()
    if team is None:
        raise HTTPException(status_code=400, detail="Team not found")
    if team.organization_id != current_user.active_org_id:
        raise HTTPException(
            status_code=403,
            detail="Team does not belong to active organization",
        )

    existing_team_use = await session.execute(
        select(Restaurant).where(Restaurant.team_id == payload.team_id).limit(1)
    )
    if existing_team_use.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=409,
            detail="Restaurant already exists for this team",
        )

    slug = await _generate_slug(payload.name, session)
    restaurant = Restaurant(
        name=payload.name,
        slug=slug,
        phone=payload.phone,
        team_id=payload.team_id,
        settings={
            "email": payload.email,
            "address_street": payload.address,
            "business_type": payload.business_type,
        },
    )
    session.add(restaurant)
    await commit_write(session)
    await session.refresh(restaurant)
    return restaurant


@router.get("/me", response_model=RestaurantRead)
async def get_my_restaurant(
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> Any:
    return restaurant


@router.patch("/me", response_model=RestaurantRead)
async def patch_my_restaurant(
    payload: RestaurantPatch,
    restaurant: Restaurant = Depends(get_current_restaurant),
    session: AsyncSession = Depends(get_tenant_session),
) -> Any:
    """Partial update of top-level restaurant fields and/or settings JSONB."""
    # `restaurant` is loaded by the owner-pool `get_session` inside the
    # `get_current_restaurant` dep; mutating it requires it to be attached
    # to OUR session (the tenant-scoped `get_tenant_session`) so the UPDATE
    # runs under `authenticated` role + RLS. Without this `session.merge`,
    # `session.add(restaurant)` below raised
    # `sqlalchemy.exc.InvalidRequestError: Object is already attached to
    # session X (this is Y)` and the whole PATCH returned 500 — i.e. NO
    # restaurant-settings save through this endpoint persisted, ever.
    #
    # `load=False` skips the otherwise-redundant SELECT in the tenant
    # session: the upstream dep already SELECT'd the row and validated
    # JWT/team/restaurant alignment, so we trust the state and let the
    # next flush UPDATE based on what we mutate.
    restaurant = await session.merge(restaurant, load=False)
    data = payload.model_dump(exclude_unset=True)

    # Pull out settings-level fields
    settings_keys = {
        "email",
        "address_street",
        "address_city",
        "address_postcode",
        "address_country",
        "min_advance_hours",
        "max_party_size",
        "max_advance_days",
        "cancellation_policy",
        "takeaway_enabled",
        "delivery_radius_km",
        "min_order_amount",
        "in_service_mode",
        "in_service_duration_minutes",
        "notification_preferences",
        "reservation_mode",
        "default_language",
        "dish_chooser_enabled",
        "dish_chooser_min_party_size",
        "dish_chooser_max_dishes",
        "dish_chooser_from_menu",
    }
    settings_updates = {k: v for k, v in data.items() if k in settings_keys}
    # Extract nested settings dict (deep-merge), exclude from top-level
    nested_settings: dict[str, Any] | None = data.pop("settings", None)
    if nested_settings and "agents" in nested_settings:
        try:
            AgentSettings.model_validate(nested_settings["agents"])
        except ValidationError as exc:
            raise HTTPException(status_code=422, detail=exc.errors()) from exc
    top_level_updates = {k: v for k, v in data.items() if k not in settings_keys}

    # Apply top-level column updates
    for key, value in top_level_updates.items():
        setattr(restaurant, key, value)

    # Deep-merge settings JSONB (flat keys first, then nested dict)
    if settings_updates or nested_settings:
        current: dict[str, Any] = dict(restaurant.settings or {})
        current.update(settings_updates)
        if nested_settings:
            _deep_merge(current, nested_settings)
        restaurant.settings = current
        flag_modified(restaurant, "settings")

    session.add(restaurant)
    await session.commit()
    await session.refresh(restaurant)
    return restaurant


@router.delete("/{restaurant_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_restaurant(
    restaurant_id: str,
    current_user: CurrentUser = Depends(get_current_user),
    restaurant: Restaurant = Depends(get_current_restaurant),
    session: AsyncSession = Depends(get_tenant_session),
) -> None:
    if restaurant.id != restaurant_id:
        raise HTTPException(status_code=403, detail="Not your restaurant")
    if current_user.role not in ("owner", "admin"):
        raise HTTPException(
            status_code=403,
            detail="Only owners and admins may delete",
        )

    team_id = restaurant.team_id
    # Same cross-session attach hazard as `patch_my_restaurant` — see the
    # comment there. `session.delete(restaurant)` would raise on the
    # owner-session-attached instance without this re-attach.
    restaurant = await session.merge(restaurant, load=False)
    await session.delete(restaurant)
    await commit_write(session)
    logfire.info(
        "restaurant_deleted",
        restaurant_id=restaurant_id,
        team_id=team_id,
        requested_team_cleanup=True,
    )


@router.put("/{restaurant_id}/settings", response_model=RestaurantRead)
async def update_restaurant_settings(
    restaurant_id: str,
    settings_data: dict[str, Any],
    restaurant: Restaurant = Depends(get_current_restaurant),
    client: httpx.AsyncClient = Depends(get_restate_client),
) -> Any:
    if restaurant.id != restaurant_id:
        from fastapi import HTTPException

        raise HTTPException(status_code=403, detail="Not your restaurant")
    await restate_proxy(
        client,
        "RestaurantObject",
        restaurant_id,
        "update_settings",
        settings_data,
        mode="object",
    )
    return restaurant


@router.get("/stats")
async def get_restaurant_stats(
    restaurant: Restaurant = Depends(get_current_restaurant),
    session: AsyncSession = Depends(get_tenant_session),
) -> dict[str, Any]:
    from datetime import UTC, datetime

    cached_stats = get_restaurant_stats_cached(restaurant.id)
    if cached_stats is not None:
        return cached_stats

    today_start, today_end, week_start = _local_day_window(
        restaurant.timezone,
        now_utc=datetime.now(UTC),
    )

    reservations_today_result = await session.execute(
        select(func.count()).where(
            Reservation.restaurant_id == restaurant.id,
            Reservation.reserved_at >= today_start,
            Reservation.reserved_at < today_end,
            Reservation.status.in_(["confirmed", "pending"]),  # type: ignore[attr-defined]
        )
    )
    reservations_today = reservations_today_result.scalar() or 0

    reservations_week_result = await session.execute(
        select(func.count()).where(
            Reservation.restaurant_id == restaurant.id,
            Reservation.reserved_at >= week_start,
            Reservation.status.in_(["confirmed", "pending"]),  # type: ignore[attr-defined]
        )
    )
    reservations_week = reservations_week_result.scalar() or 0

    orders_today_result = await session.execute(
        select(func.count()).where(
            Order.restaurant_id == restaurant.id,
            Order.created_at >= today_start,
            Order.created_at < today_end,
            Order.status.in_(["paid", "preparing", "ready"]),  # type: ignore[attr-defined]
        )
    )
    orders_today = orders_today_result.scalar() or 0

    revenue_result = await session.execute(
        select(func.sum(Order.total_cents)).where(
            Order.restaurant_id == restaurant.id,
            Order.status == "paid",
        )
    )
    revenue_cents = revenue_result.scalar() or 0

    conversations_today_result = await session.execute(
        select(func.count()).where(
            Conversation.restaurant_id == restaurant.id,
            Conversation.created_at >= today_start,
            Conversation.created_at < today_end,
        )
    )
    conversations_today = conversations_today_result.scalar() or 0

    messages_today_result = await session.execute(
        select(func.count()).where(
            Message.restaurant_id == restaurant.id,
            Message.created_at >= today_start,
            Message.created_at < today_end,
        )
    )
    messages_today = messages_today_result.scalar() or 0

    stats = {
        "reservations_today": reservations_today,
        "reservations_this_week": reservations_week,
        "orders_today": orders_today,
        "revenue_total_cents": revenue_cents,
        "revenue_total_eur": f"{revenue_cents / 100:.2f}",
        "conversations_today": conversations_today,
        "messages_today": messages_today,
    }
    set_restaurant_stats_cached(restaurant.id, stats)
    return stats


_ALLOWED_TREND_DAYS = {7, 30, 90}


@router.get("/stats/reservations-trend")
async def get_reservations_trend(
    days: int = Query(30, description="Number of recent days to include"),
    restaurant: Restaurant = Depends(get_current_restaurant),
    session: AsyncSession = Depends(get_tenant_session),
) -> list[dict[str, Any]]:
    """Daily reservation counts for the last *days* days (restaurant-local)."""
    from datetime import UTC, datetime, timedelta

    from app.utils.tz import naive_utc_to_local, resolve_tz

    # Clamp to allowed preset values
    if days not in _ALLOWED_TREND_DAYS:
        days = 30

    tz = resolve_tz(restaurant.timezone)
    now_local = datetime.now(UTC).astimezone(tz).replace(tzinfo=None)
    # Lower bound for the DB query — picked from the local cursor floor
    # so the in-Python bucketing below sees every reservation that lands
    # on any of the requested local days.
    local_start = (now_local - timedelta(days=days)).replace(
        hour=0, minute=0, second=0, microsecond=0
    )
    fetch_floor = local_start.replace(tzinfo=tz).astimezone(UTC).replace(tzinfo=None)

    result = await session.execute(
        select(Reservation.reserved_at).where(
            Reservation.restaurant_id == restaurant.id,
            Reservation.reserved_at >= fetch_floor,
            Reservation.status.in_(["confirmed", "pending"]),  # type: ignore[attr-defined]
        )
    )
    counts_by_date: dict[str, int] = {}
    for (reserved_at,) in result.all():
        # Bucket by restaurant-local date — UTC bucketing misplaces
        # near-midnight reservations for any tz with a positive offset.
        local_date = naive_utc_to_local(reserved_at, tz).date()
        key = local_date.isoformat()
        counts_by_date[key] = counts_by_date.get(key, 0) + 1

    series: list[dict[str, Any]] = []
    cursor = local_start.date()
    end_date = now_local.date()
    while cursor <= end_date:
        iso = cursor.isoformat()
        series.append({"date": iso, "count": counts_by_date.get(iso, 0)})
        cursor += timedelta(days=1)
    return series


_REQUIRED_SOURCES = ["widget", "agent", "manual"]


@router.get("/stats/channel-breakdown")
async def get_channel_breakdown(
    restaurant: Restaurant = Depends(get_current_restaurant),
    session: AsyncSession = Depends(get_tenant_session),
) -> list[dict[str, Any]]:
    """Reservation count per source for the authenticated restaurant."""
    result = await session.execute(
        select(Reservation.source, func.count().label("count"))
        .where(Reservation.restaurant_id == restaurant.id)
        .group_by(Reservation.source)
    )
    rows = result.tuples().all()

    counts: dict[str, int] = {row[0]: row[1] for row in rows}

    # Ensure every required source is present (zero-fill missing ones)
    return [{"source": s, "count": counts.get(s, 0)} for s in _REQUIRED_SOURCES]
