"""Route dependencies — where authentication becomes authorization.

AuthMiddleware proves *who* is calling. These dependencies decide *whether they
may*. The split matters: a signed-in but not-invited user must still reach
/api/me (to be told they are not invited), so the invite check cannot live in
the middleware.

Any route serving real app data depends on `current_user`, which is
invite-gated. `superadmin` routes depend on `current_superadmin`.
"""

from __future__ import annotations

from fastapi import Depends, HTTPException, Request
from sqlalchemy import select
from sqlalchemy.orm import Session

from .auth import get_authenticated_user
from .db import get_db
from .errors import AUTH_FORBIDDEN, AUTH_NOT_INVITED, AUTH_SESSION_MISSING
from .models.identity import User
from .services.roles import is_admitted, is_superadmin


def _deny(code: str, message: str, status: int) -> HTTPException:
    """HTTPException carrying the structured error shape the SPA switches on."""
    return HTTPException(
        status_code=status, detail={"error_code": code, "message": message}
    )


def current_account(
    request: Request, db: Session = Depends(get_db)
) -> User:
    """The signed-in account row. Authenticated only — NOT admission-checked.

    Use this only where a not-invited user is legitimately allowed (i.e. telling
    them they are not invited). Everything else uses `current_user`.
    """
    auth = get_authenticated_user(request)
    if auth is None:
        raise _deny(AUTH_SESSION_MISSING, "authentication required", 401)
    user = db.scalar(
        select(User).where(User.external_username == auth.username)
    )
    if user is None:
        # A valid signature for an account that no longer exists — a deleted
        # user whose cookie is still live. Treat as signed out.
        raise _deny(AUTH_SESSION_MISSING, "authentication required", 401)
    return user


def current_user(account: User = Depends(current_account)) -> User:
    """The signed-in AND invited user. The dependency for real app routes.

    Role is read from the database on every request rather than carried in the
    session cookie, so revoking access takes effect on the next request instead
    of at cookie expiry.
    """
    if not is_admitted(account.role):
        raise _deny(
            AUTH_NOT_INVITED,
            "this account has not been invited to dailysplice",
            403,
        )
    return account


def current_superadmin(user: User = Depends(current_user)) -> User:
    if not is_superadmin(user.role):
        raise _deny(AUTH_FORBIDDEN, "this action requires a super admin", 403)
    return user
