"""Default-deny at the framework layer.

A route is protected because this middleware protects everything by default — not
because someone remembered to guard it. A new /api route is inaccessible until it
is given a session-checking dependency or deliberately listed as public below.

NEVER add a broad prefix exemption inside /api/ (an "/api/public/*" or "skip auth
for /api/*"). That inverted default is how API routes get left open to the
internet — a documented, repeated failure on this server.
"""

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse

from app import bw_view_as

SESSION_USER_KEY = "user"

# The only unauthenticated API surface. Each is exact-match, and each is here
# because rendering the signed-out screen needs it:
#   /api/bw/me — the SPA's "am I signed in?" probe (returns authenticated:false).
#   /api/meta  — version + vocabularies, shown on the signed-out screen too.
PUBLIC_API_PATHS = frozenset({"/api/bw/me", "/api/meta"})

# The sign-in machinery — browser redirects and the OAuth callback, not data.
PUBLIC_PATHS = frozenset(
    {"/healthz", "/auth/probe", "/auth/login", "/auth/callback", "/logout"}
)

# Hashed build assets and the SPA shell — static files carrying no data; the shell
# has to reach an anonymous visitor or nobody could see the sign-in screen.
PUBLIC_PREFIXES = ("/assets/",)


def _is_public(path: str) -> bool:
    if path in PUBLIC_PATHS or path in PUBLIC_API_PATHS:
        return True
    if path.startswith(PUBLIC_PREFIXES):
        return True
    if path.startswith("/api/") or path.startswith("/auth/"):
        return False  # default-deny: anything new under these needs a session
    return True  # the SPA shell — a static document carrying no data


# Mutations still allowed while impersonating READ-ONLY: ending the view-as and
# signing out. Everything else that changes state is refused structurally, so no
# individual route has to remember the check.
_READONLY_EXEMPT = frozenset({"/api/bw/view-as/stop", "/logout"})
_MUTATING = frozenset({"POST", "PUT", "PATCH", "DELETE"})


class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        if _is_public(request.url.path):
            return await call_next(request)
        if not request.session.get(SESSION_USER_KEY):
            return JSONResponse(
                status_code=401,
                content={
                    "error_code": "NOT_AUTHENTICATED",
                    "summary": "Sign in to continue.",
                    "details": "This request needs a signed-in session.",
                },
            )
        # View As, read-only mode: looking is fine, changing anything is not.
        if (
            request.method in _MUTATING
            and request.url.path not in _READONLY_EXEMPT
            and request.session.get(bw_view_as.ACTING_KEY)
            and not bw_view_as.can_write(request.session)
        ):
            return JSONResponse(
                status_code=403,
                content={
                    "error_code": "VIEW_AS_READ_ONLY",
                    "summary": "You are viewing as someone else, read-only.",
                    "details": "Return to yourself to make changes, or start "
                    "View As in act mode.",
                },
            )
        return await call_next(request)
