"""API views managing the mobile (Capacitor) Django session lifecycle.

Counterpart of `core.authentication.views`: the OIDC callback hands a one-time
token to the mobile app via deep link, and the app calls the exchange endpoint
from its native HTTP layer to obtain the session cookie used by all subsequent
API calls. The logout endpoint ends that session without touching the IdP one.
"""

import base64
import hashlib
import secrets
from importlib import import_module

from django.conf import settings
from django.contrib.auth import SESSION_KEY
from django.contrib.auth import logout as auth_logout
from django.core.cache import cache
from django.middleware.csrf import get_token

from drf_spectacular.utils import extend_schema
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.views import APIView

from core.authentication.views import MOBILE_AUTH_TOKEN_CACHE_PREFIX


def _s256(code_verifier):
    """Compute the RFC 7636 S256 transform of a PKCE code verifier."""
    digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
    return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")


class MobileSessionExchangeView(APIView):
    """Exchange the one-time token minted by the OIDC callback for a session cookie.

    Anonymous by design: the request is authenticated by the single-use token
    and the PKCE verifier generated by the app before opening the system browser.
    Throttled per IP — a legitimate login exchanges exactly once, so the cap
    only slows down brute-force guessing of the token.
    """

    authentication_classes = []
    permission_classes = [AllowAny]
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = "mobile_auth_exchange"

    # Excluded from the OpenAPI schema: this endpoint is part of the mobile
    # auth bootstrap (like /authenticate/), not of the Orval-generated client.
    @extend_schema(exclude=True)
    def post(self, request):
        """Verify the token and PKCE verifier, then attach the session to the response."""
        # JSON bodies may be a non-object or carry non-string values, which
        # would crash .get() or _s256() with a 500 instead of a controlled
        # rejection.
        data = request.data if isinstance(request.data, dict) else {}
        token = data.get("token", "")
        code_verifier = data.get("code_verifier", "")
        if (
            not isinstance(token, str)
            or not isinstance(code_verifier, str)
            or not token
            or not code_verifier
        ):
            return Response(
                {"detail": "Missing token or code_verifier."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        cache_key = f"{MOBILE_AUTH_TOKEN_CACHE_PREFIX}:{token}"
        payload = cache.get(cache_key)
        # Single use: consume the token before verifying it so a failed attempt
        # cannot be retried with another verifier. get() then delete() is not
        # atomic, so two concurrent exchanges may both read the payload — but
        # the backend deletes the key exactly once, so only the request whose
        # delete() returns True is allowed to proceed.
        consumed = cache.delete(cache_key)
        if (
            payload is None
            or not consumed
            or not secrets.compare_digest(
                _s256(code_verifier), payload["code_challenge"]
            )
        ):
            return Response(
                {"detail": "Invalid or expired token."},
                status=status.HTTP_403_FORBIDDEN,
            )

        engine = import_module(settings.SESSION_ENGINE)
        session = engine.SessionStore(payload["session_key"])
        if session.get(SESSION_KEY) is None:
            return Response(
                {"detail": "Session expired."},
                status=status.HTTP_403_FORBIDDEN,
            )

        # Attach the session to the underlying HttpRequest (not the DRF proxy)
        # so SessionMiddleware emits the Set-Cookie header on this response.
        http_request = request._request  # noqa: SLF001 pylint: disable=protected-access
        http_request.session = session
        http_request.session.modified = True
        # With CSRF_USE_SESSIONS this stores the CSRF secret in the session and
        # returns the token the app echoes in the X-CSRFToken header; the native
        # HTTP jar replays the session cookie, so the check validates server-side
        # without depending on a csrf cookie the jar would not resend.
        csrf_token = get_token(http_request)

        return Response({"csrf_token": csrf_token})


class MobileLogoutView(APIView):
    """Invalidate the Django session without the RP-initiated IdP logout.

    The web logout (`/logout/`) redirects to the identity provider with an
    `id_token_hint`, which terminates the IdP session and therefore cross-app
    SSO. Mobile logout must only flush the server-side Django session: the app
    clears its local cookie jar, and the IdP session stays alive for the other
    apps of the suite.

    Anonymous requests are a no-op (204): the app calls this best-effort, and
    logging out an already expired session must not fail.
    """

    # Excluded from the OpenAPI schema: this endpoint is part of the mobile
    # auth bootstrap (like /authenticate/), not of the Orval-generated client.
    @extend_schema(exclude=True)
    def post(self, request):
        """Flush the server-side session bound to the request cookie."""
        auth_logout(request)
        return Response(status=status.HTTP_204_NO_CONTENT)
