"""Tenant scoping dependency — resolves the authenticated user's Restaurant.

Two-layer protection:

1. **Application layer** (`get_current_restaurant`): validates that the JWT's
   `activeTeamId` / `activeOrganizationId` claims correspond to a real
   team-org-restaurant chain and that the user is actually a member. This
   guards against tampered or stale JWTs, orphaned teams, and missing
   `teamMember` rows. Runs against the owner pool because it must read
   `team` / `teamMember` / `restaurant` rows that span multiple tenants.

2. **Database layer** (`get_tenant_session`): yields a session that, on
   every transaction begin, runs `SET LOCAL ROLE authenticated` (drops to
   Neon's NOBYPASSRLS role) and injects the validated JWT claims into
   `request.jwt.claims`. The `pg_session_jwt` extension consumes those
   claims via `auth.session()`, and RLS policies enforce
   `restaurant_id = (auth.session() ->> 'activeRestaurantId')`. Missing
   or absent claim → fail-closed (zero rows).

Access model (per `organization-tenancy` capability spec):
- The JWT carries `sub`, `email`, `emailVerified`, `activeOrganizationId`,
  `activeTeamId`, `activeRestaurantId`, `role`.
- `activeTeamId` corresponds 1:1 with `restaurant.team_id`;
  `activeRestaurantId` is the resolved `restaurant.id`, embedded by BA's
  `definePayload` so RLS policies stay simple equalities without a
  team→restaurant join.
- Access is granted if EITHER:
  (a) `role` is `owner` or `admin` AND the team belongs to the active org, OR
  (b) a `teamMember` row binds the user to the team.

Raises 403 on: missing active team/org, team-org mismatch, non-admin without
teamMember row. Raises 404 if the team has no linked restaurant (data drift).
"""

from collections.abc import AsyncGenerator

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

from app.auth.better_auth import CurrentUser, get_current_user
from app.db.session import async_session_factory, get_session
from app.models.auth_mirror import Team, TeamMember
from app.models.restaurant import Restaurant

_ADMIN_ROLES: frozenset[str] = frozenset({"owner", "admin"})


async def get_current_restaurant(
    current_user: CurrentUser = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
) -> Restaurant:
    """Resolves and access-checks the active Restaurant for the current request.

    Runs against the owner pool because the team / membership / restaurant
    lookups span tables that are not tenant-scoped (Team and TeamMember are
    auth-mirror tables; Restaurant joins by team_id, not restaurant_id).
    The RLS layer enforces tenant isolation downstream once
    `get_tenant_session` yields a session with the JWT claims injected.
    """
    if not current_user.active_team_id or not current_user.active_org_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="No active restaurant selected. Complete onboarding or switch restaurants.",
        )

    # Step 1: Look up the team. Verify it belongs to the org claimed in the JWT
    # (defends against a tampered or stale JWT).
    team_result = await session.execute(
        select(Team).where(Team.id == current_user.active_team_id).limit(1)
    )
    team = team_result.scalar_one_or_none()
    if team is None:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Active team not found.",
        )
    if team.organization_id != current_user.active_org_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Active team does not belong to active organization.",
        )

    # Step 2: Access check. Owner / admin can act on any team in their org;
    # member roles require an explicit teamMember binding.
    if current_user.role not in _ADMIN_ROLES:
        member_check = await session.execute(
            select(TeamMember)
            .where(
                TeamMember.team_id == current_user.active_team_id,
                TeamMember.user_id == current_user.id,
            )
            .limit(1)
        )
        if member_check.scalar_one_or_none() is None:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="User is not a member of the active restaurant's team.",
            )

    # Step 3: Resolve the restaurant from the team. 1:1 by restaurant.team_id.
    restaurant_result = await session.execute(
        select(Restaurant).where(Restaurant.team_id == current_user.active_team_id).limit(1)
    )
    restaurant = restaurant_result.scalar_one_or_none()
    if restaurant is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Restaurant not found for active team.",
        )

    # Defense-in-depth sanity: the JWT's activeRestaurantId, if present,
    # MUST match what we just resolved. A mismatch implies BA issued the
    # JWT before the restaurant was created/renamed (stale token) or
    # tampering. Reject so RLS never receives a claim that disagrees with
    # the app-layer truth.
    if (
        current_user.active_restaurant_id is not None
        and current_user.active_restaurant_id != restaurant.id
    ):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="JWT activeRestaurantId does not match resolved restaurant.",
        )

    return restaurant


async def get_tenant_session(
    current_user: CurrentUser = Depends(get_current_user),
    restaurant: Restaurant = Depends(get_current_restaurant),
) -> AsyncGenerator[AsyncSession, None]:
    """Yields a session that runs every query under the `authenticated` role
    with the request's validated BA JWT claims injected into
    `pg_session_jwt`.

    The transaction-level `SET LOCAL ROLE authenticated` +
    `set_config('request.jwt.claims', …)` is wired through the
    `after_begin` event hook in `app.db.session`, keyed off
    `session.info["jwt_claims"]`. The hook re-applies on every new
    transaction within the session (including post-commit re-begins and
    savepoints), so mid-request commits cannot leak cross-tenant rows.

    The claims we inject are reconstructed from the already-validated
    `CurrentUser` rather than the raw JWT — that way the DB sees a
    stable, app-trusted shape regardless of how BA's payload evolves.
    `restaurant` is depended on transitively (it raises 403/404 for any
    invalid claim) so by the time we reach this point we trust the
    `active_restaurant_id`.
    """
    claims = {
        "sub": current_user.id,
        "role": current_user.role,
        "activeOrganizationId": current_user.active_org_id,
        "activeTeamId": current_user.active_team_id,
        "activeRestaurantId": restaurant.id,
    }
    async with async_session_factory() as session:
        session.info["jwt_claims"] = claims
        yield session
