"""
users/models.py
================
Modelos de usuarios, perfiles y tokens de seguridad.

Diseño de roles:
  - Un solo modelo CustomUser con campo role (admin/docente/alumno)
  - Perfiles separados (PerfilDocente, PerfilAlumno) con datos específicos
  - Esto evita herencia múltiple de tabla (más complejo) y mantiene
    la flexibilidad de agregar campos por rol sin afectar la tabla principal

Seguridad de datos personales:
  - Emails almacenados cifrados (AES-256-GCM)
  - email_hash para búsquedas sin descifrar
  - Contraseñas con bcrypt (Django lo maneja automáticamente)
  - Campos de bloqueo: login_attempts, locked_until
"""

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models
from django.utils import timezone
from django.conf import settings

from shared.mixins import TimestampedModel
from shared.utils.encryption import get_encryption_service


# ─────────────────────────────────────────────────────────────
# MANAGER DE USUARIO PERSONALIZADO
# ─────────────────────────────────────────────────────────────

class CustomUserManager(BaseUserManager):
    """
    Manager para CustomUser que usa email (cifrado) como identificador.
    Sobreescribe create_user y create_superuser de Django.
    """

    def create_user(self, email, password=None, **extra_fields):
        """
        Crea un usuario normal.
        El email se normaliza (lowercase) antes de cifrar.
        """
        if not email:
            raise ValueError('El email es obligatorio')
        
        enc = get_encryption_service()
        email_clean = email.lower().strip()
        
        user = self.model(
            email_encrypted=enc.encrypt(email_clean),
            email_hash=enc.hash(email_clean),
            **extra_fields
        )
        user.set_password(password)  # bcrypt automático de Django
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        """
        Crea un superusuario admin.
        """
        extra_fields.setdefault('role', 'admin')
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)
        extra_fields.setdefault('is_approved', True)
        return self.create_user(email, password, **extra_fields)

    def get_by_email(self, email: str):
        """
        Busca un usuario por email usando el hash (no descifra todos los registros).
        
        Returns: CustomUser o None
        """
        enc = get_encryption_service()
        email_hash = enc.hash(email.lower().strip())
        return self.filter(email_hash=email_hash).first()


# ─────────────────────────────────────────────────────────────
# MODELO PRINCIPAL DE USUARIO
# ─────────────────────────────────────────────────────────────

class CustomUser(AbstractBaseUser, PermissionsMixin, TimestampedModel):
    """
    Usuario personalizado de EduPlay.
    
    Reemplaza al User de Django con:
      - Email cifrado como identificador (en lugar de username)
      - Campo role para control de acceso por rol
      - Campos de seguridad: intentos de login, bloqueo, sesión activa
      - is_approved: para aprobación de docentes por el admin
    """

    class Role(models.TextChoices):
        """Roles disponibles en el sistema."""
        ADMIN   = 'admin',   'Administrador'
        DOCENTE = 'docente', 'Docente'
        ALUMNO  = 'alumno',  'Alumno'

    # ── Identificación ──
    # El email se almacena CIFRADO — para buscar se usa email_hash
    email_encrypted = models.TextField(
        verbose_name='Email (cifrado)',
        help_text='Email cifrado con AES-256-GCM. Descifrar antes de mostrar.'
    )
    email_hash = models.CharField(
        max_length=64,
        unique=True,
        verbose_name='Hash del email',
        help_text='SHA-256 del email normalizado para búsquedas. No es el email real.'
    )

    # ── Rol y estado ──
    role = models.CharField(
        max_length=10,
        choices=Role.choices,
        default=Role.ALUMNO,
        verbose_name='Rol',
        db_index=True  # Se filtra frecuentemente por rol
    )
    is_active = models.BooleanField(
        default=True,
        verbose_name='Activo',
        help_text='Usuario puede iniciar sesión'
    )
    is_approved = models.BooleanField(
        default=False,
        verbose_name='Aprobado',
        help_text='Para docentes: aprobado por el admin. '
                  'Para alumnos: True automáticamente al registrarse.'
    )
    is_staff = models.BooleanField(
        default=False,
        help_text='Puede acceder al admin de Django'
    )

    # ── Nickname público (para ranking y podio) ──
    nickname = models.CharField(
        max_length=80,
        blank=True,
        verbose_name='Nickname',
        help_text='Nombre público visible en rankings. Opcional.'
    )

    # ── Seguridad: control de intentos y bloqueo ──
    login_attempts = models.PositiveSmallIntegerField(
        default=0,
        verbose_name='Intentos de login fallidos'
    )
    locked_until = models.DateTimeField(
        null=True, blank=True,
        verbose_name='Bloqueado hasta',
        help_text='Si es futuro, el usuario está bloqueado temporalmente'
    )

    # ── Token de sesión activa (para unicidad de sesión) ──
    # Se actualiza en cada login, se invalida en logout
    session_token = models.CharField(
        max_length=64,
        blank=True, null=True,
        verbose_name='Token de sesión activa',
        help_text='Solo una sesión activa por usuario a la vez'
    )

    # ── Configuración de Django Auth ──
    USERNAME_FIELD  = 'email_hash'  # Campo usado como "username" internamente
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    class Meta:
        verbose_name = 'Usuario'
        verbose_name_plural = 'Usuarios'
        db_table = 'users_customuser'

    def __str__(self):
        return f'{self.role} #{self.id}'

    # ── Propiedades de acceso al email descifrado ──
    @property
    def email(self) -> str:
        """Retorna el email descifrado. Usar solo cuando necesario mostrar."""
        if not self.email_encrypted:
            return ''
        return get_encryption_service().decrypt(self.email_encrypted)

    @email.setter
    def email(self, value: str):
        """Cifra el email al asignar."""
        if value:
            enc = get_encryption_service()
            clean = value.lower().strip()
            self.email_encrypted = enc.encrypt(clean)
            self.email_hash = enc.hash(clean)

    # ── Seguridad: gestión de bloqueo ──
    @property
    def is_locked(self) -> bool:
        """True si el usuario está bloqueado por intentos fallidos."""
        if self.locked_until is None:
            return False
        return timezone.now() < self.locked_until

    def register_failed_login(self):
        """
        Registra un intento de login fallido.
        Si supera MAX_LOGIN_ATTEMPTS, bloquea la cuenta.
        """
        max_attempts = settings.SECURITY_CONFIG['MAX_LOGIN_ATTEMPTS']
        lockout_minutes = settings.SECURITY_CONFIG['LOCKOUT_DURATION_MINUTES']

        self.login_attempts += 1
        if self.login_attempts >= max_attempts:
            self.locked_until = timezone.now() + timezone.timedelta(minutes=lockout_minutes)

        self.save(update_fields=['login_attempts', 'locked_until'])

    def reset_login_attempts(self):
        """Resetea los intentos al loguearse correctamente."""
        if self.login_attempts > 0 or self.locked_until:
            self.login_attempts = 0
            self.locked_until = None
            self.save(update_fields=['login_attempts', 'locked_until'])


# ─────────────────────────────────────────────────────────────
# PERFIL DE DOCENTE
# ─────────────────────────────────────────────────────────────

class PerfilDocente(TimestampedModel):
    """
    Datos específicos del rol docente.
    Separado del CustomUser para mantener la tabla principal liviana
    y evitar columnas nulas para roles que no las necesitan.
    """

    user = models.OneToOneField(
        CustomUser,
        on_delete=models.CASCADE,
        related_name='perfil_docente',
        verbose_name='Usuario'
    )

    # ── Datos institucionales ──
    # Nombre completo cifrado (dato personal sensible)
    nombre_completo_encrypted = models.TextField(
        verbose_name='Nombre completo (cifrado)'
    )
    institucion = models.CharField(
        max_length=200,
        verbose_name='Institución'
    )
    departamento = models.CharField(
        max_length=200,
        verbose_name='Departamento / Escuela'
    )
    cargo = models.CharField(
        max_length=100,
        blank=True,
        verbose_name='Cargo'
    )

    # ── Aprobación ──
    aprobado_por = models.ForeignKey(
        CustomUser,
        null=True, blank=True,
        on_delete=models.SET_NULL,
        related_name='docentes_aprobados',
        verbose_name='Aprobado por (admin)'
    )
    aprobado_en = models.DateTimeField(
        null=True, blank=True,
        verbose_name='Fecha de aprobación'
    )
    motivo_rechazo = models.TextField(
        blank=True,
        verbose_name='Motivo de rechazo',
        help_text='Visible para el docente rechazado'
    )

    class Meta:
        verbose_name = 'Perfil de docente'
        verbose_name_plural = 'Perfiles de docentes'
        db_table = 'users_perfilDocente'

    def __str__(self):
        return f'Docente {self.user_id}'

    @property
    def nombre_completo(self) -> str:
        """Retorna nombre descifrado."""
        return get_encryption_service().decrypt(self.nombre_completo_encrypted)

    @nombre_completo.setter
    def nombre_completo(self, value: str):
        """Cifra el nombre al asignar."""
        self.nombre_completo_encrypted = get_encryption_service().encrypt(value)


# ─────────────────────────────────────────────────────────────
# PERFIL DE ALUMNO
# ─────────────────────────────────────────────────────────────

class PerfilAlumno(TimestampedModel):
    """
    Datos específicos del rol alumno.
    """

    user = models.OneToOneField(
        CustomUser,
        on_delete=models.CASCADE,
        related_name='perfil_alumno',
        verbose_name='Usuario'
    )

    # ── Nombre completo cifrado ──
    nombre_completo_encrypted = models.TextField(
        verbose_name='Nombre completo (cifrado)'
    )

    # ── Trazabilidad de registro ──
    # ¿Qué docente generó el código con el que se registró este alumno?
    invitado_por = models.ForeignKey(
        CustomUser,
        null=True, blank=True,
        on_delete=models.SET_NULL,
        related_name='alumnos_invitados',
        verbose_name='Docente que invitó'
    )

    class Meta:
        verbose_name = 'Perfil de alumno'
        verbose_name_plural = 'Perfiles de alumnos'
        db_table = 'users_perfilAlumno'

    def __str__(self):
        return f'Alumno {self.user_id}'

    @property
    def nombre_completo(self) -> str:
        return get_encryption_service().decrypt(self.nombre_completo_encrypted)

    @nombre_completo.setter
    def nombre_completo(self, value: str):
        self.nombre_completo_encrypted = get_encryption_service().encrypt(value)


# ─────────────────────────────────────────────────────────────
# TOKEN DE REGISTRO DE ALUMNO
# ─────────────────────────────────────────────────────────────

class RegistrationToken(TimestampedModel):
    """
    Token de registro seguro para invitar alumnos.
    
    Flujo completo:
      1. Docente genera token → se crea este registro con token_hash
      2. Docente comparte el token_plain con el alumno
      3. Alumno se registra con token_plain → sistema verifica el hash
      4. Al usar: used_at se marca, used_by apunta al alumno
      5. Token expirado o usado → se rechaza
    
    NUNCA almacenar token_plain en BD — solo el hash.
    """

    docente = models.ForeignKey(
        CustomUser,
        on_delete=models.CASCADE,
        related_name='registration_tokens',
        verbose_name='Docente que generó el token'
    )

    # SHA-256 del token. Nunca el token real.
    token_hash = models.CharField(
        max_length=64,
        unique=True,
        verbose_name='Hash del token (SHA-256)',
        help_text='Hash del token. El token real se entrega al alumno y nunca se almacena.'
    )

    # Sección destino — el alumno quedará inscrito aquí al registrarse
    seccion = models.ForeignKey(
        'academic.Seccion',
        null=True, blank=True,
        on_delete=models.SET_NULL,
        related_name='registration_tokens',
        verbose_name='Sección destino'
    )

    # Metadatos de uso
    expires_at = models.DateTimeField(
        verbose_name='Expira en',
        help_text='Token inválido después de esta fecha/hora'
    )
    used_at = models.DateTimeField(
        null=True, blank=True,
        verbose_name='Usado en'
    )
    used_by = models.OneToOneField(
        CustomUser,
        null=True, blank=True,
        on_delete=models.SET_NULL,
        related_name='registration_token_used',
        verbose_name='Alumno que usó el token'
    )

    class Meta:
        verbose_name = 'Token de registro'
        verbose_name_plural = 'Tokens de registro'
        db_table = 'users_registrationtoken'
        indexes = [
            models.Index(fields=['token_hash']),
            models.Index(fields=['docente', 'used_at']),
        ]

    def __str__(self):
        status = 'usado' if self.used_at else ('expirado' if self.is_expired else 'activo')
        return f'Token de {self.docente_id} [{status}]'

    @property
    def is_expired(self) -> bool:
        """True si el token expiró por tiempo."""
        return timezone.now() > self.expires_at

    @property
    def is_used(self) -> bool:
        """True si el token ya fue utilizado."""
        return self.used_at is not None

    @property
    def is_valid(self) -> bool:
        """True solo si no está usado Y no expiró."""
        return not self.is_used and not self.is_expired

    def mark_as_used(self, alumno: CustomUser):
        """
        Marca el token como usado. Llamar al completar el registro del alumno.
        Es atómico — si falla, el registro tampoco se completa.
        """
        self.used_at = timezone.now()
        self.used_by = alumno
        self.save(update_fields=['used_at', 'used_by'])

