"""
shared/utils/tokens.py
=======================
Servicio de generación de tokens seguros para:
  - Código de registro de alumno (48 bits hex, uso único, 72h)
  - Código de acceso efímero a sección (8 chars alfanumérico, 20 min)
  - Tokens de verificación de email

¿Por qué no usar strings simples?
  Los strings predecibles (ALU-2026-LR-047) pueden ser adivinados.
  Los tokens criptográficos son imposibles de adivinar por fuerza bruta.

Seguridad del código de registro:
  - 48 bits de entropía = 281 billones de combinaciones posibles
  - Almacenado hasheado (bcrypt) en BD — si la BD es comprometida,
    los códigos no pueden ser usados directamente
  - Expiración de 72 horas — limita la ventana de ataque
  - Uso único — se invalida inmediatamente al ser usado
  - Vinculado al docente — no es transferible entre docentes
"""

import os
import secrets
import hashlib
import string
import logging
from datetime import timedelta
from django.utils import timezone

logger = logging.getLogger('eduplay')


class TokenService:
    """Servicio de generación y validación de tokens seguros."""

    # Caracteres para el código de acceso (excluye ambiguos: 0,O,I,1,l)
    CODE_CHARS = string.ascii_uppercase.replace('O', '').replace('I', '') + \
                 string.digits.replace('0', '').replace('1', '')

    @staticmethod
    def generate_registration_token() -> tuple[str, str]:
        """
        Genera un token de registro seguro para un alumno.
        
        Returns:
            tuple: (token_plain, token_hash)
              - token_plain: el código que se entrega al alumno (formato: REG-XXXXXX-XXXXXX-XXXXXX)
              - token_hash:  el hash SHA-256 que se almacena en BD (no reversible)
        
        El token_plain NUNCA se almacena en BD — solo el hash.
        Flujo:
          1. Docente genera token → se muestra token_plain → se guarda token_hash
          2. Alumno ingresa token_plain → se hashea → se compara con token_hash en BD
        """
        # 18 bytes = 144 bits de entropía → más que suficiente
        raw_bytes = os.urandom(18)
        hex_str = raw_bytes.hex().upper()
        
        # Formato legible: REG-XXXXXX-XXXXXX-XXXXXX (18 chars hex = 3 grupos de 6)
        token_plain = f"REG-{hex_str[0:6]}-{hex_str[6:12]}-{hex_str[12:18]}"
        
        # Hash SHA-256 del token (determinístico para comparación futura)
        token_hash = hashlib.sha256(token_plain.encode('utf-8')).hexdigest()
        
        return token_plain, token_hash

    @staticmethod
    def verify_registration_token(token_plain: str, token_hash: str) -> bool:
        """
        Verifica que un token_plain corresponda al token_hash almacenado.
        Usa comparación de tiempo constante para prevenir timing attacks.
        """
        expected_hash = hashlib.sha256(token_plain.encode('utf-8')).hexdigest()
        # secrets.compare_digest: comparación en tiempo constante
        return secrets.compare_digest(expected_hash, token_hash)

    @classmethod
    def generate_access_code(cls, length: int = 8) -> str:
        """
        Genera un código de acceso efímero para que alumnos se unan a una sección.
        
        Características:
          - 8 caracteres alfanuméricos sin ambiguos
          - Se muestra durante 20 minutos en clase
          - No necesita hash — se almacena en texto en BD (es temporal)
          - No es de uso único: varios alumnos lo usan durante los 20 min
        
        Returns:
            str: Código en mayúsculas (ej: 'A3X9KPQM')
        """
        return ''.join(secrets.choice(cls.CODE_CHARS) for _ in range(length))

    @staticmethod
    def generate_email_verification_token() -> str:
        """
        Genera un token URL-safe para verificación de email.
        32 bytes = 256 bits de entropía.
        """
        return secrets.token_urlsafe(32)

    @staticmethod
    def is_token_expired(created_at, expiry_hours: int = 72) -> bool:
        """
        Verifica si un token ha expirado dado su fecha de creación.
        
        Args:
            created_at: datetime de creación del token
            expiry_hours: horas de validez (default: 72h para registro alumno)
        
        Returns:
            bool: True si el token expiró
        """
        expiry_time = created_at + timedelta(hours=expiry_hours)
        return timezone.now() > expiry_time
