# chat_n8n.py
import httpx
import logging
import os
from datetime import datetime
from typing import Dict, Any
import json
import html
import re
import asyncio
from dotenv import load_dotenv

# Configurar logging específico
logger = logging.getLogger(__name__)


# ============================================================================
# CONFIGURACIÓN DE Variables de Entorno
# ============================================================================

script_dir = os.path.dirname(os.path.abspath(__file__))  # Directorio del script
crm_dir = os.path.dirname(script_dir)  # Sube un nivel hasta crm
env_path = os.path.join(crm_dir, "configuraciones", ".env")  # Ahora sí apunta bien

load_dotenv(env_path)

# ============================================================================
# CONFIGURACIÓN DE SEGURIDAD
# ============================================================================

# URLs y configuración desde variables de entorno
WEBHOOK_URL = os.getenv("CHAT_IA_CATIA")
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT"))
MAX_RETRIES = int(os.getenv("MAX_RETRIES"))
MAX_PAYLOAD_SIZE = int(os.getenv("MAX_PAYLOAD_SIZE"))  # 1MB


# ============================================================================
# VALIDACIÓN Y SANITIZACIÓN
# ============================================================================

def sanitize_input_basic(value: str) -> str:
    """Sanitización básica de entrada - Bloquea patrones maliciosos conocidos"""
    if not isinstance(value, str):
        raise ValueError("El valor debe ser una cadena de texto")
    
    # Patrones de inyección más comunes y críticos
    malicious_patterns = [
        r"(?i)(<script[^>]*>.*?</script>)",  # Scripts
        r"(?i)(javascript:|vbscript:)",      # Protocolos maliciosos
        r"(?i)\b(union\s+select|select\s+.*\s+from)\b",  # SQL injection básico
        r"(?i)\b(or\s+\d+\s*[=<>]\s*\d+|and\s+\d+\s*[=<>]\s*\d+)",  # SQL injection clásico
        r"(?i)\b(drop|delete|update|insert)\s+(table|into|from)\b",  # SQL DML peligroso
        r"(?i)(exec\s*\(|execute\s*\()",     # Ejecución de comandos
        r"(?i)(<iframe|<object|<embed)",      # Elementos HTML peligrosos
        r"(\.\./){2,}",                       # Path traversal
        r"(?i)(sleep\s*\(|benchmark\s*\(|waitfor\s+delay)",  # Time-based attacks
        # Combinaciones peligrosas de caracteres especiales
        r"[<>\"'`](?=.*[{}[\]\\;|&])"
    ]
    
    for pattern in malicious_patterns:
        if re.search(pattern, value):
            logger.warning(f"Patrón malicioso bloqueado en entrada para valor: {value[:50]}...")
            raise ValueError("Contenido no permitido detectado")
    
    # Limpiar y escapar
    cleaned = html.escape(value.strip())
    
    # Validar longitud específica por campo
    max_length = 2000 if len(value) > 500 else 500  # Más flexible
    if len(cleaned) > max_length:
        raise ValueError(f"Contenido demasiado largo (máximo {max_length} caracteres)")
    
    return cleaned

def validate_input_schema(data: dict) -> tuple[bool, str]:
    """Validar esquema de entrada"""
    required_fields = {"nombre": str, "pregunta": str}
    optional_fields = {
        "sessionId": str, "idCliente": str, "threadId": str,
        "asistenteIdOpenIa": str, "instrucciones": str, "id_asistente": str,
        "empresa_modelo": str, "audio_nom": str, "volumeUp": str,
        "humanOn": str, "espqr": str, "ruta_script": str, "archivoBase64": str,
        "timestamp": str
    }
    
    # Verificar campos requeridos
    for field, expected_type in required_fields.items():
        if field not in data:
            return False, f"Campo requerido faltante: {field}"
        if not isinstance(data[field], expected_type):
            return False, f"Tipo incorrecto para {field}, esperado {expected_type.__name__}"
        if not data[field].strip():
            return False, f"Campo {field} no puede estar vacío"
    
    # Verificar campos opcionales si están presentes
    for field, expected_type in optional_fields.items():
        if field in data and data[field] is not None:
            if not isinstance(data[field], expected_type):
                return False, f"Tipo incorrecto para {field}, esperado {expected_type.__name__}"
    
    return True, "Esquema válido"

def validate_webhook_response(response_data: Dict[str, Any]) -> bool:
    """
    Validación permisiva: Solo bloquea amenazas REALES y EJECUTABLES.
    Permite menciones de código, ejemplos, documentación, etc.
    """
    try:
        response_str = json.dumps(response_data)
        
        # Solo bloquear si hay código JavaScript/HTML EJECUTABLE con contexto peligroso
        critical_patterns = [
            # Script CON contenido sospechoso (no vacío, no comentado)
            r'<script[^>]*>(?!\s*//|/\*|\s*</script>)[^<]{10,}</script>',
            
            # Eventos JavaScript inline con código real
            r'on(?:load|error|click|mouse\w+)\s*=\s*["\'](?!#|return false)[^"\']{5,}["\']',
            
            # Iframe con URL externa (no ejemplos, no about:blank)
            r'<iframe[^>]*src\s*=\s*["\'](?!about:blank|javascript:void|#|data:text/html,<html>)https?://(?!example\.com|localhost)[^"\']+["\']',
        ]
        
        for pattern in critical_patterns:
            if re.search(pattern, response_str, re.IGNORECASE):
                logger.warning(f"⚠️ Contenido ejecutable detectado en respuesta")
                return False
        
        return True  # Permitir todo lo demás
        
    except Exception as e:
        logger.error(f"Error en validación: {e}")
        return True  # Permitir en caso de error


def detect_anomalies(nombre: str, pregunta: str) -> list[str]:
    """Detectar patrones anómalos en la entrada"""
    anomalies = []
    
    # Pregunta muy larga
    if len(pregunta) > 1500:
        anomalies.append("pregunta_muy_larga")
    
    # Muchos caracteres especiales
    special_chars = len(re.findall(r'[^\w\s]', pregunta))
    if special_chars > len(pregunta) * 0.3:
        anomalies.append("muchos_caracteres_especiales")
    
    # Contenido repetitivo
    words = pregunta.split()
    if len(words) > 10 and len(set(words)) < len(words) * 0.4:
        anomalies.append("contenido_repetitivo")
    
    # Nombre sospechoso
    if len(nombre) > 100 or len(re.findall(r'[^\w\s]', nombre)) > 5:
        anomalies.append("nombre_sospechoso")
    
    return anomalies

# ============================================================================
# FUNCIÓN PRINCIPAL CON SEGURIDAD MEJORADA
# ============================================================================

async def enviar_chat_n8n_basico(datos: Dict[str, Any]) -> Dict[str, Any]:
    """
    Función básica para enviar chat a n8n con medidas de seguridad implementadas
    """
    try:
        # PASO 1: VALIDACIÓN DE ESQUEMA
        schema_valid, schema_error = validate_input_schema(datos)
        if not schema_valid:
            logger.warning(f"Esquema inválido: {schema_error}")
            return create_safe_error(f"Datos inválidos: {schema_error}", "INVALID_SCHEMA")

        # PASO 2: EXTRAER Y SANITIZAR CAMPOS BÁSICOS
        try:
            clean_nombre = sanitize_input_basic(datos["nombre"])
            clean_pregunta = sanitize_input_basic(datos["pregunta"])
        except ValueError as e:
            logger.warning(f"Entrada rechazada por seguridad: {e}")
            return create_safe_error("Datos de entrada no válidos", "INVALID_INPUT")

        # PASO 3: VALIDACIONES ADICIONALES DE LONGITUD
        if len(clean_nombre) > 100:
            return create_safe_error("Nombre demasiado largo (máximo 100 caracteres)", "NAME_TOO_LONG")

        # PASO 4: DETECTAR ANOMALÍAS
        anomalies = detect_anomalies(clean_nombre, clean_pregunta)
        if anomalies:
            logger.warning(f"Anomalías detectadas: {anomalies}")
            # Solo bloquear si hay múltiples anomalías críticas
            critical_anomalies = ["muchos_caracteres_especiales", "contenido_repetitivo"]
            if len([a for a in anomalies if a in critical_anomalies]) >= 2:
                return create_safe_error("Contenido sospechoso detectado", "SUSPICIOUS_CONTENT")

        # PASO 5: SANITIZAR CAMPOS OPCIONALES
        safe_fields = {}
        optional_fields = [
            "sessionId", "idCliente", "threadId", "asistenteIdOpenIa", 
            "instrucciones", "id_asistente", "empresa_modelo", "audio_nom",
            "volumeUp", "humanOn", "espqr", "ruta_script", "archivoBase64"
        ]
        
        for field in optional_fields:
            if field in datos and datos[field]:
                try:
                    safe_fields[field] = sanitize_input_basic(str(datos[field]))
                except ValueError:
                    logger.warning(f"Campo {field} contiene contenido no válido, usando valor por defecto")
                    safe_fields[field] = get_default_value(field)
            else:
                safe_fields[field] = get_default_value(field)

        # PASO 6: CONSTRUCCIÓN SEGURA DEL PAYLOAD
        payload = {
            "nombre": clean_nombre,
            "pregunta": clean_pregunta,
            "timestamp": datos.get("timestamp") or datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "sessionId": safe_fields["sessionId"],
            "idCliente": safe_fields["idCliente"] or f"user_{int(datetime.now().timestamp())}",
            "threadId": safe_fields["threadId"] or f"thread_{int(datetime.now().timestamp())}",
            "asistenteIdOpenIa": safe_fields["asistenteIdOpenIa"] or "default",
            "instrucciones": safe_fields["instrucciones"] or "Eres un asistente especializado en temas catastrales y tributarios.",
            "id_asistente": safe_fields["id_asistente"] or "1",
            "empresa_modelo": safe_fields["empresa_modelo"] or "catastro",
            "audio_nom": safe_fields["audio_nom"],
            "volumeUp": safe_fields["volumeUp"],
            "humanOn": safe_fields["humanOn"],
            "espqr": safe_fields["espqr"],
            "ruta_script": safe_fields["ruta_script"],
            "archivoBase64": safe_fields["archivoBase64"] or "no"
        }

        # PASO 7: VALIDAR TAMAÑO DEL PAYLOAD
        payload_size = len(json.dumps(payload))
        if payload_size > MAX_PAYLOAD_SIZE:
            logger.warning(f"Payload demasiado grande: {payload_size} bytes")
            return create_safe_error("Solicitud demasiado grande", "PAYLOAD_TOO_LARGE")

        # PASO 8: VERIFICAR CONFIGURACIÓN
        if not WEBHOOK_URL:
            logger.error("URL del webhook no configurada")
            return create_safe_error("Variable de entorno 'CHAT_IA_CATIA' no encontrada o vacía", "MISSING_WEBHOOK_URL")

        # PASO 9: LOGGING DE SEGURIDAD
        session_id_masked = safe_fields["sessionId"][:8] + "***" if safe_fields["sessionId"] else "no_session"
        logger.info(f"📤 Procesando solicitud para sesión: {session_id_masked}")

        # PASO 10: REALIZAR SOLICITUD CON REINTENTOS Y TIMEOUT
        response_data = None
        last_error = None

        for attempt in range(MAX_RETRIES):
            try:
                async with httpx.AsyncClient(
                    timeout=httpx.Timeout(REQUEST_TIMEOUT),
                    limits=httpx.Limits(max_connections=5)
                ) as client:
                    
                    response = await client.post(
                        WEBHOOK_URL,
                        json=payload,
                        headers={
                            "Content-Type": "application/json",
                            "Accept": "application/json",
                            "User-Agent": "SecureCatiaWeb-Backend/2.0",
                            "X-Request-ID": f"req_{int(datetime.now().timestamp())}"
                        }
                    )

                # MANEJO DE CÓDIGOS DE ESTADO
                if response.status_code == 429:  # Rate limited
                    logger.warning("Rate limit del webhook alcanzado")
                    if attempt < MAX_RETRIES - 1:
                        await asyncio.sleep(2 ** attempt)  # Backoff exponencial
                        continue
                    return create_safe_error("Servicio temporalmente ocupado", "RATE_LIMITED")

                if response.status_code != 200:
                    logger.error(f"Error HTTP del webhook: {response.status_code}")
                    error_details = response.text[:200] if hasattr(response, 'text') else "Sin detalles"
                    return create_safe_error(
                        f"Error HTTP: {response.status_code}",
                        "HTTP_ERROR",
                        error_details
                    )

                # PARSEAR RESPUESTA CON VALIDACIÓN
                try:
                    response_data = response.json()
                except json.JSONDecodeError as e:
                    logger.error(f"Respuesta JSON inválida del webhook: {e}")
                    return create_safe_error("Respuesta inválida de n8n", "INVALID_JSON")

                # VALIDACIÓN DE SEGURIDAD DE LA RESPUESTA
                if not validate_webhook_response(response_data):
                    logger.warning("Respuesta del webhook falló validación de seguridad")
                    return create_safe_error("Respuesta no válida del servicio", "UNSAFE_RESPONSE")

                # Éxito - salir del loop de reintentos
                break

            except httpx.TimeoutException:
                last_error = "Timeout del servicio"
                logger.warning(f"Timeout en intento {attempt + 1}")
            except httpx.ConnectError:
                last_error = "Error de conexión"
                logger.warning(f"Error de conexión en intento {attempt + 1}")
            except httpx.RequestError as e:
                last_error = f"Error de petición: {str(e)}"
                logger.warning(f"Error de petición en intento {attempt + 1}: {e}")

            # Esperar antes del siguiente intento
            if attempt < MAX_RETRIES - 1:
                await asyncio.sleep(1.5)

        # VERIFICAR SI TODOS LOS INTENTOS FALLARON
        if response_data is None:
            logger.error(f"Todos los reintentos fallaron: {last_error}")
            return create_safe_error_by_type(last_error)

        # PASO 11: PREPARAR RESPUESTA SEGURA
        mensaje = extract_safe_message(response_data)
        safe_data = sanitize_response_data(response_data)

        logger.info(f"✅ Respuesta exitosa para sesión: {session_id_masked}")
        
        return {
            "success": True,
            "message": mensaje,
            "data": safe_data if isinstance(safe_data, dict) else {"output": safe_data}
        }

    except Exception as e:
        logger.exception(f"Error inesperado en enviar_chat_n8n_basico: {str(e)}")
        return create_safe_error("Error interno al procesar el mensaje de chat", "INTERNAL_ERROR")

# ============================================================================
# FUNCIONES DE UTILIDAD SEGURAS
# ============================================================================

def create_safe_error(message: str, error_code: str = "UNKNOWN_ERROR", details: str = None) -> dict:
    """Crear respuesta de error segura"""
    response = {
        "success": False,
        "message": html.escape(str(message)[:200]),
        "error_code": error_code
    }
    
    if details:
        response["details"] = html.escape(str(details)[:500])
    
    return response

def create_safe_error_by_type(error_message: str) -> dict:
    """Crear error específico basado en el tipo de error"""
    if "timeout" in error_message.lower():
        return create_safe_error(
            "Timeout: El servidor n8n no respondió en el tiempo esperado",
            "TIMEOUT"
        )
    elif "conexión" in error_message.lower():
        return create_safe_error(
            "Error de conexión: No se pudo conectar con el servidor n8n",
            "CONNECTION_ERROR"
        )
    else:
        return create_safe_error(
            f"Error de conexión con n8n: {error_message}",
            "REQUEST_ERROR"
        )

def get_default_value(field: str) -> str:
    """Obtener valor por defecto para campos opcionales"""
    defaults = {
        "audio_nom": "",
        "volumeUp": "false",
        "humanOn": "false",
        "espqr": "",
        "ruta_script": "",
        "archivoBase64": "no",
        "asistenteIdOpenIa": "default",
        "instrucciones": "Eres un asistente especializado en temas catastrales y tributarios.",
        "id_asistente": "1",
        "empresa_modelo": "catastro"
    }
    return defaults.get(field, "")

def extract_safe_message(response_data: Any) -> str:
    """Extraer mensaje seguro de la respuesta"""
    if isinstance(response_data, dict):
        message = (
            response_data.get("output") or 
            response_data.get("message") or 
            response_data.get("respuesta") or
            "Respuesta procesada correctamente"
        )
    else:
        message = str(response_data)
    
    # Sanitizar el mensaje
    return html.escape(str(message)[:1000])  # Limitar longitud

def sanitize_response_data(data: Any) -> Any:
    """Sanitizar datos de respuesta para prevenir XSS"""
    if isinstance(data, str):
        return html.escape(data)
    elif isinstance(data, dict):
        # Sanitizar solo campos de texto, preservar estructura
        sanitized = {}
        for key, value in data.items():
            if isinstance(value, str):
                sanitized[key] = html.escape(value)
            elif isinstance(value, (dict, list)):
                sanitized[key] = sanitize_response_data(value)
            else:
                sanitized[key] = value
        return sanitized
    elif isinstance(data, list):
        return [sanitize_response_data(item) for item in data]
    return data