"""
shared/utils/encryption.py
===========================
Servicio de cifrado AES-256 para datos personales sensibles en BD.
Cifra: emails, nombres completos, teléfonos y otros datos personales.

¿Por qué cifrar en BD?
  Si la BD es comprometida, los datos cifrados son ilegibles sin la clave.
  La clave vive en variables de entorno, nunca en el código ni en la BD.

Estrategia de búsqueda:
  Los campos cifrados no se pueden buscar directamente.
  Para búsquedas se almacena el hash SHA-256 del valor original
  en un campo separado (ej: email_hash).

Uso:
    enc = EncryptionService()
    cifrado = enc.encrypt("correo@ejemplo.com")
    original = enc.decrypt(cifrado)
    hash_val = enc.hash("correo@ejemplo.com")
"""

import base64
import hashlib
import os
import logging
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from django.conf import settings

logger = logging.getLogger('eduplay')


class EncryptionService:
    """
    Servicio singleton de cifrado AES-256-GCM.
    
    AES-GCM es preferido sobre AES-CBC porque:
      - Provee autenticación integrada (detecta manipulación)
      - No requiere padding manual
      - Más resistente a ataques de oracle
    
    Cada cifrado genera un nonce (número de uso único) aleatorio de 12 bytes
    que se almacena junto al texto cifrado para el descifrado.
    """

    def __init__(self):
        # Decodificar la clave desde base64 (se generó como 32 bytes random)
        key_b64 = settings.ENCRYPTION_KEY
        if not key_b64:
            raise ValueError('ENCRYPTION_KEY no configurada en .env')
        
        key_bytes = base64.b64decode(key_b64)
        if len(key_bytes) != 32:
            raise ValueError('ENCRYPTION_KEY debe ser de exactamente 32 bytes (256 bits)')
        
        self._aesgcm = AESGCM(key_bytes)

    def encrypt(self, plaintext: str) -> str:
        """
        Cifra texto plano con AES-256-GCM.
        
        Retorna: base64(nonce + ciphertext) como string.
        El nonce (12 bytes) va prefijado al texto cifrado.
        """
        if not plaintext:
            return plaintext
        
        # Nonce aleatorio de 12 bytes — nunca reutilizar el mismo nonce con la misma clave
        nonce = os.urandom(12)
        ciphertext = self._aesgcm.encrypt(nonce, plaintext.encode('utf-8'), None)
        
        # Combinar nonce + ciphertext y codificar en base64 para almacenar como texto
        combined = nonce + ciphertext
        return base64.b64encode(combined).decode('utf-8')

    def decrypt(self, encrypted_value: str) -> str:
        """
        Descifra un valor cifrado con encrypt().
        
        Raises: Exception si el valor fue manipulado (GCM tag inválido).
        """
        if not encrypted_value:
            return encrypted_value
        
        try:
            combined = base64.b64decode(encrypted_value.encode('utf-8'))
            nonce      = combined[:12]
            ciphertext = combined[12:]
            plaintext  = self._aesgcm.decrypt(nonce, ciphertext, None)
            return plaintext.decode('utf-8')
        except Exception as e:
            logger.error(f'Error al descifrar valor: {e}')
            raise ValueError('No se pudo descifrar el valor — posible manipulación') from e

    def hash(self, value: str) -> str:
        """
        Genera un hash SHA-256 del valor para búsquedas en BD.
        
        El hash es determinístico (mismo input → mismo output) lo que
        permite buscar por email sin descifrar todos los registros.
        NO es reversible (no se puede obtener el original del hash).
        """
        if not value:
            return ''
        return hashlib.sha256(value.lower().strip().encode('utf-8')).hexdigest()

    @staticmethod
    def generate_key() -> str:
        """
        Genera una nueva clave de 32 bytes codificada en base64.
        Usar una vez para generar ENCRYPTION_KEY y guardar en .env.
        
        Uso desde consola:
            python -c "from shared.utils.encryption import EncryptionService; print(EncryptionService.generate_key())"
        """
        return base64.b64encode(os.urandom(32)).decode('utf-8')


# Instancia global — se crea una vez al iniciar Django
# Patrón Singleton: evita crear múltiples instancias del servicio
_encryption_service = None

def get_encryption_service() -> EncryptionService:
    """
    Retorna la instancia global del servicio de cifrado.
    Lazy initialization: se crea solo cuando se necesita por primera vez.
    """
    global _encryption_service
    if _encryption_service is None:
        _encryption_service = EncryptionService()
    return _encryption_service
