"""
Autenticación de la API por clave.

La clave llega en `Authorization: Api-Key <clave>`. La búsqueda se hace por el
prefijo —que se guarda en claro y está indexado— y recién después se compara el
hash del valor completo. Sin el prefijo habría que hashear la clave recibida y
buscar por el hash, que también funciona, pero deja sin manera de mostrarle a
alguien *cuál* de sus claves está usando un pipeline.
"""

from datetime import timedelta

from django.utils import timezone
from rest_framework.authentication import BaseAuthentication, get_authorization_header

from apps.accounts.models import ApiKey
from apps.core.errors import Unauthenticated

#: Cada cuánto se refresca la marca de último uso. La API se llama desde un
#: pipeline de despliegue, muchas veces seguidas: escribir en cada petición
#: agregaría una escritura por llamada para ganar una precisión que a nadie le
#: sirve. Con esta resolución, «se usó hoy» sigue siendo cierto.
LAST_USED_RESOLUTION = timedelta(minutes=5)

KEYWORD = b'Api-Key'


class ApiKeyAuthentication(BaseAuthentication):
    """Resuelve la cuenta a partir de la clave, o rechaza con un motivo distinguible."""

    def authenticate(self, request):
        header = get_authorization_header(request).split()
        if not header or header[0].lower() != KEYWORD.lower():
            # Sin cabecera propia no es un rechazo: puede ser una vista que
            # también admite sesión. Devolver nada deja que DRF siga probando.
            return None

        if len(header) != 2:
            raise Unauthenticated(
                'La cabecera Authorization no tiene la forma esperada: Api-Key <clave>.'
            )

        plaintext = header[1].decode(errors='ignore')
        prefix = ApiKey.split(plaintext)
        if prefix is None:
            raise Unauthenticated('La clave no tiene el formato de una clave de esta plataforma.')

        key = (
            ApiKey.objects.filter(prefix=prefix).select_related('account', 'account__plan').first()
        )
        if key is None or not key.matches(plaintext):
            raise Unauthenticated('La clave no es válida.')

        if key.is_revoked:
            raise Unauthenticated('La clave fue revocada.', revoked_at=key.revoked_at.isoformat())

        if not key.account.is_active:
            raise Unauthenticated('La cuenta asociada a esta clave está desactivada.')

        _touch(key)
        return (key.account, key)

    def authenticate_header(self, request):
        """Sin esto DRF responde 403 en vez de 401 cuando falta la credencial."""
        return 'Api-Key'


def _touch(key: ApiKey) -> None:
    now = timezone.now()
    if key.last_used_at is not None and now - key.last_used_at < LAST_USED_RESOLUTION:
        return
    # Escritura directa sin pasar por save(): evita tocar updated_at y arrastrar
    # el resto de las columnas en una operación que ocurre en cada petición.
    ApiKey.objects.filter(pk=key.pk).update(last_used_at=now)
    key.last_used_at = now
