"""
Autenticacion por token de acceso.

Antes usaba `rest_framework.authtoken`, cuyo token apunta a un `User`. Al
canjear la clave por un token se perdia de vista *que credencial* se habia
presentado, y con ella toda posibilidad de autorizar: la vista sabia que
usuario era, pero no de quien era la clave, que alcances tenia ni desde que
direcciones podia enviar.

Ahora el token cuelga de la clave (`keys.AccessToken`), asi que la identidad
llega intacta hasta la vista y hasta la fila de auditoria.
"""

from __future__ import annotations

import logging

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed

from core.utils.http import get_client_ip
from keys.models.access_token import AccessToken

logger = logging.getLogger("core.auth")


class TokenAuthentication(BaseAuthentication):
    """
    Autenticacion por token de un solo uso.

    Devolver `None` significa para DRF "este autenticador no aplica", y deja
    la peticion como anonima en lugar de rechazarla. Eso solo es correcto
    cuando falta del todo la cabecera; si llega una cabecera pero esta mal
    formada, hay que rechazarla explicitamente.
    """

    keyword = "Bearer"

    def authenticate(self, request):
        auth = request.META.get("HTTP_AUTHORIZATION")
        if not auth:
            # Sin cabecera: la peticion queda anonima y la clase de permiso
            # (IsAuthenticated) es la que la rechaza con 401.
            return None

        parts = auth.split()
        if len(parts) != 2 or parts[0] != self.keyword:
            raise AuthenticationFailed("Invalid TOKEN")

        # El token es de un solo uso: se consume al autenticar.
        clave = AccessToken.consume(parts[1])
        if clave is None:
            # Un solo mensaje para todos los casos de fallo (inexistente,
            # caducado, clave revocada despues de emitirlo, cuenta
            # desactivada): distinguirlos da pistas a quien prueba tokens.
            logger.warning(
                "Intento de autenticacion con token invalido",
                extra={"source_ip": get_client_ip(request)},
            )
            raise AuthenticationFailed("Invalid TOKEN")

        # La identidad de la credencial viaja con la peticion: la usan la
        # autorizacion por alcances, el filtrado por cuenta, la politica de
        # remitentes y la auditoria.
        request.api_key_record = clave
        request.api_client = clave

        return (clave.account.user, clave)

    def authenticate_header(self, request):
        # Hace que DRF responda 401 en lugar de 403 cuando falta la cabecera.
        return self.keyword
