# Archivo: CotizEspSolicitud.py
# Ruta: src\App\Ventas_Module\Cotiz\CotizEspSolicitud.py
# Descripción: Módulo principal para el manejo de cotizaciones especiales
# Autor: Equipo de Desarrollo IGSA
# Fecha: 2025

from flask import app, render_template, request, session, jsonify, redirect, url_for
from functools import wraps
import logging,json
from datetime import datetime
from typing import List, Dict
from pathlib import Path
import os

from Consultas_SQL.Operaciones.Ingenieria.Cotiz.CotizCreatedIngSQL import consultar_docs_por_oportunidad_y_tarea
from config import IS_PRODUCTION

from App.Utilities_module.DocsManagement import verificar_configuracion, upload_document_to_existing_docs
from App.Utilities_module.DocsManagement import upload_document
from App.Utilities_module.CRMManagement import CRMManager
from Consultas_SQL.Ventas.Cotiz.CotizEspSolicitudSQL import (
    buscar_oportunidad_crm,
    insert_CRM_CONTACT,
    verificar_oportunidad_existente,
    crear_actualizar_oportunidad,
    obtener_quotation_type_id,
    crear_formulario_head,
    crear_formulario_details,
    obtener_seller_user_id,
    obtener_nombre_usuario,
    crear_tarea_cotizacion,
    debe_crear_tarea,
    actualizar_formulario_con_docs,
    get_FormID_by_TaskID,
    insert_CRM_CONTACT
)



# Configurar logging
logger = logging.getLogger('cotizaciones_especiales')

def cotiz_esp_solicitud(app, mail):
    """
    Función principal que registra todas las rutas del módulo de cotizaciones especiales
    
    Args:
        app: Instancia de Flask
        mail: Instancia del sistema de mail
    """

    @app.route("/Ventas/Cotiz/EspecialSolicitud/GetConfig")
    def get_cotiz_config():
        """
        Endpoint para que el frontend consulte el modo de operación (Prototipo o Real).
        """
        try:
            return jsonify({
                "success": True,
                "PROTOTIPO_MODE": app.config.get('PROTOTIPO'),
                "PROTOTIPO_INTERFAZ" : app.config.get('PROTOTIPO_INTERFAZ'),
                "EMAIL_PROTOTYPE_MODE": app.config.get('EMAIL_PROTOTYPE_MODE'),
                #"EMAIL_PROTOTYPE_TOADRESS": app.config.get('EMAIL_PROTOTYPE_TOADRESS')
            })
        except Exception as e:
            logger.error(f"❌ Error al obtener configuracion: {str(e)}")
            return jsonify({"success": False, "error": str(e)}), 500
    

    ''' // Inicianizar PROTOTIPO_MODE
    @app.route("/Ventas/Cotiz/EspecialSolicitud/BuscarOportunidad", methods=['POST'])
    def buscar_oportunidad():
        """
        Busca una oportunidad en el CRM o permite crear una nueva
        
        Returns:
            JSON con resultado de búsqueda
        """
        try:
            # Obtener datos de la petición
            data = request.get_json()
            
            if not data:
                logger.warning("Petición sin datos JSON")
                return jsonify({
                    "success": False,
                    "error": {
                        "message": "Petición inválida - no se recibieron datos",
                        "code": "INVALID_REQUEST",
                        "alert_type": "warning"
                    }
                }), 400
            
            CRM_OpportunityNumber = float(data['opportunity_number'])
            logger.info(f"Buscando oportunidad: {CRM_OpportunityNumber}")
            
            # Buscar oportunidad en el CRM
            oportunidad = buscar_oportunidad_crm(CRM_OpportunityNumber)
            
            if oportunidad:
                # Oportunidad encontrada
                logger.info(f"Oportunidad {CRM_OpportunityNumber} encontrada exitosamente")
                return jsonify({
                    "success": True,
                    "data": oportunidad,
                    "message": "Oportunidad encontrada exitosamente"
                }), 200
            else:
                # Oportunidad no encontrada
                logger.warning(f"Oportunidad {CRM_OpportunityNumber} no encontrada en la base de datos")
                return jsonify({
                    "success": False,
                    "error": {
                        "message": f"La oportunidad {CRM_OpportunityNumber} no existe en el sistema o está inactiva",
                        "code": "OPPORTUNITY_NOT_FOUND",
                        "alert_type": "info",
                        "details": {
                            "opportunity_number": CRM_OpportunityNumber
                        }
                    }
                }), 404

        except KeyError as e:
            logger.error(f"Campo faltante en la petición: {str(e)}")
            return jsonify({
                "success": False,
                "error": {
                    "message": "Datos incompletos en la petición",
                    "code": "MISSING_FIELD",
                    "alert_type": "warning",
                    "details": {
                        "missing_field": str(e).replace("'", "")
                    }
                }
            }), 400
            
        except ValueError as e:
            logger.error(f"Error de conversión de datos: {str(e)}")
            return jsonify({
                "success": False,
                "error": {
                    "message": "El número de oportunidad debe ser un valor numérico válido",
                    "code": "INVALID_FORMAT",
                    "alert_type": "warning",
                    "details": {
                        "original_error": str(e)
                    }
                }
            }), 400
            
        except ConnectionError as e:
            logger.error(f"Error de conexión en búsqueda de oportunidad: {str(e)}")
            return jsonify({
                "success": False,
                "error": {
                    "message": "Error de conexión con la base de datos. Intente nuevamente en unos momentos.",
                    "code": "DATABASE_CONNECTION_ERROR",
                    "alert_type": "error"
                }
            }), 500
            
        except Exception as e:
            # Log completo para desarrolladores, mensaje genérico para usuarios
            logger.error(f"Error inesperado en búsqueda de oportunidad: {str(e)}", exc_info=True)
            return jsonify({
                "success": False,
                "error": {
                    "message": "Error interno del servidor. Si el problema persiste, contacte al administrador.",
                    "code": "INTERNAL_SERVER_ERROR",
                    "alert_type": "error",
                    "timestamp": datetime.now().isoformat()
                }
            }), 500            
    '''

    @app.route("/Ventas/Cotiz/EspecialSolicitud/GetConfig")
    def is_prototipo():
        from flask import current_app
        """
        Devuelve True o False dependiendo del valor de PROTOTIPO en config.py
        """
        return jsonify({
            "prototipo": bool(current_app.config.get("PROTOTIPO", False))
        })
        

    @app.route("/Ventas/Cotiz/EspecialSolicitud/InsertContact", methods=["POST"])
    def insert_contact_endpoint():
        """
        Inserta un contacto en Q_CRM y devuelve el OpportunityNumber generado.
        """

        # Leer JSON del frontend
        data = request.get_json(silent=True)

        if not data:
            return jsonify({
                "success": False,
                "error": "Se esperaba JSON válido."
            }), 400

        contactName = data.get("contactName")
        contactNumber = data.get("contactNumber")
        contactEmail = data.get("contactEmail")

        # Validación básica
        if not contactName or not contactNumber or not contactEmail:
            return jsonify({
                "success": False,
                "error": "Faltan campos obligatorios."
            }), 400

        try:
            # Llamar a la función que ya tienes
            new_opportunity = insert_CRM_CONTACT(
                contactName,
                contactNumber,
                contactEmail
            )

            if new_opportunity is None:
                return jsonify({
                    "success": False,
                    "error": "No se pudo crear la oportunidad."
                }), 500

            # Respuesta final
            return jsonify({
                "success": True,
                "opportunityNumber": new_opportunity
            }), 200

        except Exception as e:
            logger.error(f"Error en InsertContact: {str(e)}")
            return jsonify({
                "success": False,
                "error": str(e)
            }), 500


    @app.route("/Ventas/Cotiz/EspecialSolicitud/BuscarOportunidad", methods=['POST'])
    def buscar_oportunidad():
        """
        Busca una oportunidad. Usa la BD local si PROTOTIPO=True.
        Se conecta al CRM real si PROTOTIPO=False.
        """
        try:
            data = request.get_json()
            opportunity_number_raw = data['opportunity_number']

            # 1. Determinar el modo de operación usando el objeto 'app'
            if app.config.get('PROTOTIPO'):
                
                # --- MODO PROTOTIPO (BD Local) ---
                logger.info(f"⚠️ MODO PROTOTIPO: Buscando oportunidad local SQL: {opportunity_number_raw}")
                
                # Convertir a float para usar la función SQL original
                numero_oportunidad_float = float(opportunity_number_raw)
                oportunidad = buscar_oportunidad_crm(numero_oportunidad_float)
                
                if oportunidad:
                    return jsonify({
                        "success": True, 
                        "data": oportunidad, 
                        "message": "Oportunidad (prototipo) encontrada en BD local."
                    }), 200
                else:
                    return jsonify({
                        "success": False, 
                        "error": {
                            "message": f"La oportunidad {opportunity_number_raw} no existe en la BD de prototipos.", 
                            "code": "PROTOTYPE_NOT_FOUND", 
                            "alert_type": "info"
                        }
                    }), 404
            
            else:
                # --- MODO REAL (CRM) ---
                logger.info(f"✅ MODO REAL: Buscando oportunidad en CRM: {opportunity_number_raw}")


                # ⬇️ ==================================================
                # ⬇️ INICIO DEL CÓDIGO DE DEBUG (usando logger.info)
                # ⬇️ ==================================================
                logger.info("--- DEBUG: Verificando app.config ANTES de llamar a CRMManager ---")
                
                # 1. Verifica la clave que SÍ funciona (como control)
                logger.info(f"DEBUG: Valor de PROTOTIPO: {app.config.get('PROTOTIPO')}")
                
                # 2. Verifica la clave que falla
                logger.info(f"DEBUG: Valor de BC_TENANT_ID: {app.config.get('BC_TENANT_ID')}")
                
                # 3. Verifica una clave de la clase hija (DevelopmentConfig)
                logger.info(f"DEBUG: Valor de BC_ENV_NAME: {app.config.get('BC_ENV_NAME')}")
                
                logger.info("--- DEBUG: Fin de la verificación ---")
                # ⬆️ ==================================================

                logger.info("Intentando crear CRMManager...")
                crm_manager = CRMManager(config=app.config) # Esta es la línea que falla
                logger.info("CRMManager creado exitosamente.")

                
                # 1. Inicializar CRM Manager pasando el objeto app.config
                #crm_manager = CRMManager(config=app.config)
                
                # 2. Obtener el contexto completo
                contexto_crm = crm_manager.get_opportunity_context(opportunity_number_raw)

                # 3. Mapear la respuesta del CRM al formato esperado por el frontend
                oportunidad_mapeada = {
                    "CRM_OpportunityNumber": contexto_crm.get("No"),
                    "CRM_ContactName": contexto_crm.get("ContactName"),
                    "CRM_ContactType": contexto_crm.get("ContactType"),
                    "CRM_AssignedSalesperson": contexto_crm.get("SalespersonName"),
                    "CRM_ContactEmail": contexto_crm.get("ContactEMail"),
                    "CRM_ContactAdress": contexto_crm.get("Address"),
                    "CRM_ContactColonia": contexto_crm.get("Colony"),
                    "CRM_ContactCity": contexto_crm.get("City"),
                    "CRM_ContactState": contexto_crm.get("County"),
                    "CRM_ContactCountry": "MX", 
                    "CRM_ContactZip": contexto_crm.get("PostCode"),
                    "CRM_ContactNumber": contexto_crm.get("Phone"),
                    # Campos que podrían no estar en el contexto
                    "CRM_ContactID": "", 
                    "CRM_ContactLegalIdentifier": ""
                }
                
                logger.info(f"Oportunidad {opportunity_number_raw} encontrada y mapeada desde CRM.")
                return jsonify({
                    "success": True, 
                    "data": oportunidad_mapeada, 
                    "message": "Oportunidad encontrada exitosamente en el CRM"
                }), 200

        # 4. Manejo de errores específicos del CRM (ValueError)
        except ValueError as e:
            logger.warning(f"Oportunidad no encontrada en CRM: {str(e)}")
            return jsonify({
                "success": False,
                "error": {
                    "message": str(e),
                    "code": "CRM_OPPORTUNITY_NOT_FOUND",
                    "alert_type": "info"
                }
            }), 404
            
        except KeyError as e:
            logger.error(f"Campo faltante en la petición: {str(e)}")
            return jsonify({
                "success": False,
                "error": {
                    "message": "Datos incompletos en la petición",
                    "code": "MISSING_FIELD",
                    "alert_type": "warning",
                    "details": {
                        "missing_field": str(e).replace("'", "")
                    }
                }
            }), 400
            
        except ConnectionError as e:
            logger.error(f"Error de conexión en búsqueda de oportunidad: {str(e)}")
            return jsonify({
                "success": False,
                "error": {
                    "message": "Error de conexión con la base de datos. Intente nuevamente en unos momentos.",
                    "code": "DATABASE_CONNECTION_ERROR",
                    "alert_type": "error"
                }
            }), 500
            
        except Exception as e:
            # Log completo para desarrolladores, mensaje genérico para usuarios
            logger.error(f"Error inesperado en búsqueda de oportunidad: {str(e)}", exc_info=True)
            return jsonify({
                "success": False,
                "error": {
                    "message": "Error interno del servidor. Si el problema persiste, contacte al administrador.",
                    "code": "INTERNAL_SERVER_ERROR",
                    "alert_type": "error",
                    "timestamp": datetime.now().isoformat()
                }
            }), 500   
    
    @app.route("/Ventas/Cotiz/EspecialSolicitud/Enviar", methods=['POST'])
    def enviar_solicitud():
        """
        Procesa el envío de solicitud de cotización especial
        ACTUALIZADO: Manejo mejorado de FormData con archivos
        
        Returns:
            JSON con resultado del procesamiento
        """
        try:
            # Verificar si es multipart/form-data (con archivos) o application/json
            if request.content_type and 'multipart/form-data' in request.content_type:
                # Procesar FormData con archivos
                data = procesar_form_data_con_archivos_v3(request)
            else:
                # Procesar JSON tradicional
                data = request.get_json()
            
            if not data:
                logger.warning("Petición sin datos en envío de solicitud")
                return jsonify({
                    "success": False,
                    "error": {
                        "message": "Petición inválida - no se recibieron datos",
                        "code": "INVALID_REQUEST",
                        "alert_type": "warning"
                    }
                }), 400
            
            # Obtener UserID de la sesión
            user_id = session.get('user_id')
            
            if not user_id:
                logger.warning("Usuario no autenticado intentando enviar solicitud")
                return jsonify({
                    "success": False,
                    "error": {
                        "message": "Usuario no autenticado",
                        "code": "NOT_AUTHENTICATED",
                        "alert_type": "warning"
                    }
                }), 401
            
            logger.info(f"Procesando solicitud de cotización especial para UserID: {user_id}")
            
            # Validar estructura de datos
            validation_result = validar_estructura_solicitud(data)
            if not validation_result['valid']:
                return jsonify({
                    "success": False,
                    "error": {
                        "message": validation_result['message'],
                        "code": "VALIDATION_ERROR",
                        "alert_type": "warning"
                    }
                }), 400
            
            # Procesar según el flujo del diagrama
            resultado_procesamiento = procesar_solicitud_cotizacion(data, user_id)
            
            if resultado_procesamiento['success']:
                logger.info(f"Solicitud procesada exitosamente para UserID: {user_id}")
                
                # Mensaje personalizado según si hay documentos
                message = "Solicitud de cotización enviada exitosamente"
                if resultado_procesamiento.get('archivos_subidos', 0) > 0:
                    archivos_count = resultado_procesamiento.get('archivos_subidos', 0)
                    message += f" con {archivos_count} documento(s)"
                
                return jsonify({
                    "success": True,
                    "message": message,
                    "data": {
                        "opportunity_id": resultado_procesamiento['opportunity_id'],
                        "forms_created": resultado_procesamiento['forms_created'],
                        "tasks_generated": resultado_procesamiento['tasks_generated'],
                        "docs_ids": resultado_procesamiento.get('docs_ids', []),
                        "archivos_subidos": resultado_procesamiento.get('archivos_subidos', 0)
                    }
                }), 200
            else:
                return jsonify({
                    "success": False,
                    "error": {
                        "message": resultado_procesamiento['message'],
                        "code": resultado_procesamiento['code'],
                        "alert_type": resultado_procesamiento['alert_type']
                    }
                }), resultado_procesamiento['status_code']

        except Exception as e:
            logger.error(f"Error inesperado en envío de solicitud: {str(e)}", exc_info=True)
            return jsonify({
                "success": False,
                "error": {
                    "message": "Error interno del servidor. Si el problema persiste, contacte al administrador.",
                    "code": "INTERNAL_SERVER_ERROR",
                    "alert_type": "error",
                    "timestamp": datetime.now().isoformat()
                }
            }), 500


    @app.route("/Ventas/Cotiz/EspecialSolicitud/DiagnosticoArchivos", methods=['GET'])
    def diagnostico_archivos():
        """
        Endpoint para verificar la configuración del sistema de archivos
        Solo para desarrollo/testing
        """
        try:
            # Verificar configuración
            config_result = verificar_configuracion()
            
            return jsonify({
                "success": True,
                "diagnostico": config_result,
                "timestamp": datetime.now().isoformat()
            }), 200
            
        except Exception as e:
            logger.error(f"Error en diagnóstico: {str(e)}")
            return jsonify({
                "success": False,
                "error": str(e)
            }), 500


def validar_estructura_solicitud(data):
    """
    Valida la estructura de los datos recibidos
    
    Args:
        data (dict): Datos de la solicitud
    
    Returns:
        dict: Resultado de validación
    """
    try:
        # Validar que existan las secciones principales
        if 'oportunidad' not in data:
            return {'valid': False, 'message': 'Faltan datos de la oportunidad'}
        
        if 'formularioGeneral' not in data:
            return {'valid': False, 'message': 'Faltan datos del formulario general'}
        
        if 'formulariosEspecificos' not in data:
            return {'valid': False, 'message': 'Faltan datos de formularios específicos'}
        
        # Validar campos obligatorios de oportunidad
        oportunidad = data['oportunidad']
        campos_obligatorios = ['CRM_OpportunityNumber', 'CRM_ContactName', 'CRM_ContactEmail']
        
        for campo in campos_obligatorios:
            if not oportunidad.get(campo, '').strip():
                return {'valid': False, 'message': f'El campo {campo} es obligatorio'}
        
        # Validar que haya al menos un formulario específico
        if not data['formulariosEspecificos'] or len(data['formulariosEspecificos']) == 0:
            return {'valid': False, 'message': 'Debe seleccionar al menos un tipo de formulario para cotización'}
        
        return {'valid': True, 'message': 'Validación exitosa'}
        
    except Exception as e:
        logger.error(f"Error en validación de estructura: {str(e)}")
        return {'valid': False, 'message': 'Error interno en validación de datos'}

def procesar_solicitud_cotizacion(data, user_id):
    """
    Procesa la solicitud siguiendo el flujo del diagrama
    ACTUALIZADO: Gestión de documentos DESPUÉS de crear formularios + ENVÍO DE CORREO
    
    Args:
        data (dict): Datos de la solicitud
        user_id (int): ID del usuario
    
    Returns:
        dict: Resultado del procesamiento
    """
    try:
        # 1. Verificar si existe oportunidad en Q_OpportunityCRM
        opportunity_number = data['oportunidad']['CRM_OpportunityNumber']
        
        try:
            oportunidad_existente = verificar_oportunidad_existente(opportunity_number)
        except ValueError as e:
            # Manejar errores de validación de status
            error_msg = str(e)
            
            if error_msg.startswith("OPPORTUNITY_CLOSED:"):
                status = error_msg.split(":")[1]
                logger.warning(f"Intento de procesar oportunidad cerrada {opportunity_number} con status {status}")
                return {
                    'success': False,
                    'message': 'El número de oportunidad ingresado ya terminó su proceso, por favor ingresar un no. de oportunidad válido',
                    'code': 'OPPORTUNITY_CLOSED',
                    'alert_type': 'warning',
                    'status_code': 400
                }
            
            elif error_msg.startswith("OPPORTUNITY_IN_PROCESS:"):
                status = error_msg.split(":")[1]
                logger.warning(f"Intento de procesar oportunidad en proceso {opportunity_number} con status {status}")
                return {
                    'success': False,
                    'message': 'Hay una solicitud previa de ingeniería por lo que no se puede generar una nueva. Comuníquese con ingeniería para que cancelen las tareas y puedan volver a asignar nuevas tareas',
                    'code': 'OPPORTUNITY_IN_PROCESS',
                    'alert_type': 'info',
                    'status_code': 409
                }
            
            else:
                raise e
        
        if oportunidad_existente:
            nueva_version = oportunidad_existente['Version'] + 1
            opportunity_id = f"{opportunity_number}-{nueva_version}"
            logger.info(f"Creando nueva versión {nueva_version} para oportunidad {opportunity_number}")
        else:
            nueva_version = 1
            opportunity_id = f"{opportunity_number}-{nueva_version}"
            logger.info(f"Creando nueva oportunidad {opportunity_number} versión {nueva_version}")
        
        # 2. Crear/actualizar registro en Q_OpportunityCRM
        resultado_oportunidad = crear_actualizar_oportunidad(
            data['oportunidad'], 
            user_id, 
            nueva_version
        )
        
        if not resultado_oportunidad['success']:
            return resultado_oportunidad
        
        # 3. Procesar formularios y crear registros PRIMERO
        forms_created = []
        tasks_generated = []
        
        # Procesar formulario general
        resultado_general = procesar_formulario_general(
            data['formularioGeneral'],
            opportunity_id,
            user_id
        )
        
        if resultado_general['success']:
            forms_created.append(resultado_general['form_id'])
            tasks_generated.extend(resultado_general['tasks'])

        # ver tareas
        logger.info(f"Tareas generadas tras formulario general: {tasks_generated}")
        
        # Procesar formularios específicos
        for formulario in data['formulariosEspecificos']:
            resultado_form = procesar_formulario_especifico(
                formulario,
                opportunity_id,
                user_id
            )
            
            if resultado_form['success']:
                forms_created.append(resultado_form['form_id'])
                tasks_generated.extend(resultado_form['tasks'])
        
        # 4. PROCESAR DOCUMENTOS DESPUÉS DE TENER FormIDs
        total_archivos_subidos = 0
        docs_ids_generados = []
        
        if 'archivos' in data and data['archivos'] and len(data['archivos']) > 0:
            resultado_docs = procesar_documentos_con_formids_v2(
                data['archivos'], 
                opportunity_id, 
                forms_created, 
                user_id
            )
            
            if resultado_docs['success']:
                total_archivos_subidos = resultado_docs['total_archivos_subidos']
                docs_ids_generados = resultado_docs['docs_ids']
                logger.info(f"Documentos procesados para {opportunity_id}: {total_archivos_subidos} archivos")
            else:
                logger.warning(f"Error procesando documentos para {opportunity_id}: {resultado_docs['message']}")
        
        # 🆕 5. ENVIAR CORREO DE NOTIFICACIÓN A INGENIERÍA
        try:
            resultado_correo = enviar_notificacion_cotizacion_especial_v2(
                data=data,
                opportunity_id=opportunity_id,
                opportunity_number=opportunity_number,
                nueva_version=nueva_version,
                forms_created=forms_created,
                tasks_generated=tasks_generated,
                user_id=user_id,
                total_archivos_subidos=total_archivos_subidos
            )
            
            if not resultado_correo['success']:
                # Log del error pero no fallar el proceso principal
                logger.warning(f"Error enviando correo de notificación para {opportunity_id}: {resultado_correo['mensaje']}")
            else:
                logger.info(f"Correo de notificación enviado exitosamente para {opportunity_id}: {resultado_correo['mensaje']}")
                
        except Exception as e:
            # No fallar el proceso principal por errores de correo
            logger.error(f"Error inesperado enviando correo para {opportunity_id}: {str(e)}")
        
        # 6. Enviar notificaciones adicionales (opcional)
        # enviar_notificaciones_solicitud(opportunity_id, tasks_generated)
        
        logger.info(f"Solicitud procesada exitosamente: {opportunity_id}, Forms: {len(forms_created)}, Tasks: {len(tasks_generated)}, Archivos: {total_archivos_subidos}")
        
        return {
            'success': True,
            'opportunity_id': opportunity_id,
            'forms_created': forms_created,
            'tasks_generated': tasks_generated,
            'archivos_subidos': total_archivos_subidos,
            'docs_ids': docs_ids_generados
        }
        
    except Exception as e:
        logger.error(f"Error en procesamiento de solicitud: {str(e)}")
        return {
            'success': False,
            'message': 'Error interno al procesar la solicitud',
            'code': 'PROCESSING_ERROR',
            'alert_type': 'error',
            'status_code': 500
        }

def procesar_formulario_general(formulario_data: dict, opportunity_id: str, user_id: int) -> dict:
    """
    Procesa el formulario general (siempre presente)
    
    Args:
        formulario_data (Dict): Datos del formulario general
        opportunity_id (str): ID de la oportunidad
        user_id (int): ID del usuario
    
    Returns:
        Dict: Resultado del procesamiento
    """
    try:
        # Obtener QuotationTypeID para formulario general
        quotation_type_id = obtener_quotation_type_id('General')
        
        # Crear registro en Q_SpQ_FormsHead
        resultado_head = crear_formulario_head(opportunity_id, quotation_type_id, user_id)
        
        if not resultado_head['success']:
            return resultado_head
        
        form_id = resultado_head['form_id']
        
        # Crear registros en Q_SpQ_FormsDetail
        if 'preguntas' in formulario_data and formulario_data['preguntas']:
            resultado_details = crear_formulario_details(form_id, formulario_data['preguntas'])
            
            if not resultado_details['success']:
                return resultado_details
        
        # 🆕 VERIFICAR SI DEBE CREAR TAREA SEGÚN CONFIGURACIÓN
        debe_crear = debe_crear_tarea(quotation_type_id)
        tasks = []
        
        if debe_crear:
            # Crear tarea para ingeniería
            seller_user_id = obtener_seller_user_id(user_id)
            user_name = obtener_nombre_usuario(user_id)
            
            resultado_task = crear_tarea_cotizacion(
                seller_user_id,
                form_id,
                opportunity_id,
                user_name
            )
            
            if not resultado_task['success']:
                return resultado_task
            
            tasks = [resultado_task['task_id']]
            logger.info(f"Formulario general procesado con tarea: {form_id}")
        else:
            logger.info(f"Formulario general procesado SIN tarea: {form_id}")
        
        return {
            'success': True,
            'form_id': form_id,
            'tasks': tasks
        }
        
    except Exception as e:
        logger.error(f"Error al procesar formulario general: {str(e)}")
        return {
            'success': False,
            'message': 'Error al procesar formulario general',
            'code': 'PROCESSING_ERROR',
            'alert_type': 'error'
        }

def procesar_formulario_especifico(formulario_data: dict, opportunity_id: str, user_id: int) -> dict:
    """
    Procesa un formulario específico (plantas, UPS, etc.)
    
    Args:
        formulario_data (Dict): Datos del formulario específico
        opportunity_id (str): ID de la oportunidad
        user_id (int): ID del usuario
    
    Returns:
        Dict: Resultado del procesamiento
    """
    try:
        # Obtener QuotationTypeID según el tipo de formulario
        tipo_formulario = formulario_data.get('tipo', 'General')
        quotation_type_id = obtener_quotation_type_id(tipo_formulario)
        
        # Crear registro en Q_SpQ_FormsHead
        resultado_head = crear_formulario_head(opportunity_id, quotation_type_id, user_id)
        
        if not resultado_head['success']:
            return resultado_head
        
        form_id = resultado_head['form_id']
        
        # Crear registros en Q_SpQ_FormsDetail si hay preguntas
        if 'preguntas' in formulario_data and formulario_data['preguntas']:
            resultado_details = crear_formulario_details(form_id, formulario_data['preguntas'])
            
            if not resultado_details['success']:
                return resultado_details
        
        # 🆕 VERIFICAR SI DEBE CREAR TAREA SEGÚN CONFIGURACIÓN
        debe_crear = debe_crear_tarea(quotation_type_id)
        tasks = []
        
        if debe_crear:
            # Crear tarea para ingeniería
            seller_user_id = obtener_seller_user_id(user_id)
            user_name = obtener_nombre_usuario(user_id)
            
            resultado_task = crear_tarea_cotizacion(
                seller_user_id,
                form_id,
                opportunity_id,
                user_name
            )
            
            if not resultado_task['success']:
                return resultado_task
            
            tasks = [resultado_task['task_id']]
            logger.info(f"Formulario específico procesado con tarea: {form_id} (tipo: {tipo_formulario})")
        else:
            logger.info(f"Formulario específico procesado SIN tarea: {form_id} (tipo: {tipo_formulario})")
        
        return {
            'success': True,
            'form_id': form_id,
            'tasks': tasks
        }
        
    except Exception as e:
        logger.error(f"Error al procesar formulario específico: {str(e)}")
        return {
            'success': False,
            'message': f'Error al procesar formulario {formulario_data.get("nombre", "específico")}',
            'code': 'PROCESSING_ERROR',
            'alert_type': 'error'
        }

def procesar_documentos_con_formids_v2(archivos_formularios: List[Dict], opportunity_id: str, 
                                    forms_created: List[str], user_id: int) -> Dict:
    """
    Versión mejorada para procesar documentos asociándolos con FormIDs específicos
    
    Args:
        archivos_formularios (List[Dict]): Lista de formularios con sus archivos
        opportunity_id (str): ID de la oportunidad
        forms_created (list[str]): Lista de FormIDs creados
        user_id (int): ID del usuario
    
    Returns:
        dict: Resultado del procesamiento con estadísticas detalladas
    """
    try:
        # Obtener nombre del usuario
        created_by = obtener_nombre_usuario(user_id)
        
        # 🆕 ESTADÍSTICAS GLOBALES MEJORADAS
        estadisticas_globales = {
            'formularios_procesados': 0,
            'formularios_exitosos': 0,
            'formularios_fallidos': 0,
            'total_archivos_intentados': 0,
            'total_archivos_exitosos': 0,
            'total_archivos_fallidos': 0,
            'tamaño_total_mb': 0,
            'tiempo_inicio': datetime.now(),
            'errores_por_formulario': [],
            'docs_ids_generados': [],
            'mapeo_formid_docs': {}
        }
        
        docs_ids_generados = []
        errores_procesamiento = []
        formularios_procesados = []
        
        # Mapeo de tipos de formulario a FormID
        mapeo_formularios = {
            'SolicVis': 'FormSolicVis', # ← SOLVIS
            'PTTransNac': 'FormPTTransNac', # ← FPETABTRANS
            'PTTransUL': 'FormPTTransUL', # ← FPETABTRANSUL
            'Aire': 'FormAire', # ← Aire
            'SistCritUPS': 'FormSistCritUPS', # ← SistCritUPS
            'General': 'FormGral',
            'Bess': 'FormBess', # Step
            'Solar': 'FormSolar', # ← SOLARR
            'EnergyCogeneration': 'FormEneCog', # ← EnergyCogeneration
            'Imess': 'FormImess', # ← Imess
            'ProyecEsp': 'FormProyecEsp' # ← PROYESP
        }
        
        logger.info(f"🚀 Procesando documentos para {len(archivos_formularios)} formulario(s)")
        logger.info(f"📋 FormIDs disponibles: {forms_created}")
        
        for formulario_data in archivos_formularios:
            estadisticas_globales['formularios_procesados'] += 1
            
            try:
                tipo_formulario = formulario_data.get('formularioTipo', '')
                nombre_modulo = formulario_data.get('formularioNombre', '')
                archivos = formulario_data.get('archivos', [])
                total_size = formulario_data.get('totalSize', 0)
                
                logger.info(f"📝 Procesando formulario: {tipo_formulario} ({len(archivos)} archivos)")
                
                if not archivos or len(archivos) == 0:
                    logger.info(f"⏭️ {tipo_formulario}: sin archivos, omitiendo")
                    continue
                
                estadisticas_globales['total_archivos_intentados'] += len(archivos)
                
                # Buscar el FormID correspondiente
                form_id_buscado = None
                sufijo_esperado = mapeo_formularios.get(tipo_formulario, nombre_modulo)
                
                if sufijo_esperado is not None:
                    for form_id in forms_created:
                        if form_id.endswith(sufijo_esperado):
                            form_id_buscado = form_id
                            break
                else:
                    logger.warning(f"El sufijo esperado para el formulario '{tipo_formulario}' es None.")
                
                if not form_id_buscado:
                    error_msg = f"No se encontró FormID para tipo {tipo_formulario} (sufijo esperado: {sufijo_esperado})"
                    logger.error(f"❌ {error_msg}")
                    errores_procesamiento.append({
                        'formulario': tipo_formulario,
                        'error': error_msg,
                        'tipo_error': 'form_id_no_encontrado'
                    })
                    estadisticas_globales['formularios_fallidos'] += 1
                    estadisticas_globales['total_archivos_fallidos'] += len(archivos)
                    continue
                
                logger.info(f"🎯 FormID encontrado: {form_id_buscado}")
                
                # 🆕 PROCESAR ARCHIVOS CON FUNCIÓN MEJORADA
                resultado_upload = upload_form_files_v2(
                    form_id=form_id_buscado,
                    opportunity_id=opportunity_id,
                    files=archivos,
                    user_id=user_id,
                    created_by=created_by
                )
                
                if resultado_upload['success']:
                    archivos_exitosos = len(resultado_upload['archivos_subidos'])
                    archivos_fallidos = len(resultado_upload['archivos_fallidos'])
                    docs_id = resultado_upload['docs_id']
                    
                    estadisticas_globales['total_archivos_exitosos'] += archivos_exitosos
                    estadisticas_globales['total_archivos_fallidos'] += archivos_fallidos
                    estadisticas_globales['tamaño_total_mb'] += resultado_upload['estadisticas']['tamaño_total_mb']
                    
                    # 🆕 VINCULAR DocsID con FormID
                    if docs_id:
                        actualizar_resultado = actualizar_formulario_con_docs(form_id_buscado, docs_id)
                        if actualizar_resultado['success']:
                            logger.info(f"🔗 FormID {form_id_buscado} vinculado con DocsID {docs_id}")
                            estadisticas_globales['mapeo_formid_docs'][form_id_buscado] = docs_id
                        else:
                            logger.warning(f"⚠️ No se pudo vincular FormID {form_id_buscado} con DocsID {docs_id}")
                    
                    # Recopilar DocsIDs únicos
                    if docs_id and docs_id not in docs_ids_generados:
                        docs_ids_generados.append(docs_id)
                        estadisticas_globales['docs_ids_generados'].append(docs_id)
                    
                    # 🆕 Guardar info completa del formulario procesado
                    formularios_procesados.append({
                        'form_id': form_id_buscado,
                        'tipo_formulario': tipo_formulario,
                        'docs_id': docs_id,
                        'archivos_exitosos': archivos_exitosos,
                        'archivos_fallidos': archivos_fallidos,
                        'download_urls': resultado_upload.get('download_urls', []),
                        'estadisticas': resultado_upload.get('estadisticas', {})
                    })
                    
                    estadisticas_globales['formularios_exitosos'] += 1
                    
                    logger.info(f"✅ {tipo_formulario}: {archivos_exitosos} archivos subidos, DocsID: {docs_id}")
                    
                    # Agregar errores parciales si los hay
                    if archivos_fallidos > 0:
                        errores_procesamiento.append({
                            'formulario': tipo_formulario,
                            'error': f"{archivos_fallidos} archivos fallaron",
                            'tipo_error': 'archivos_parciales_fallidos',
                            'archivos_fallidos': resultado_upload['archivos_fallidos']
                        })
                        
                else:
                    error_msg = f"Error subiendo archivos para {tipo_formulario}: {resultado_upload['message']}"
                    logger.error(f"❌ {error_msg}")
                    errores_procesamiento.append({
                        'formulario': tipo_formulario,
                        'error': error_msg,
                        'tipo_error': 'upload_completo_fallido',
                        'detalles': resultado_upload
                    })
                    estadisticas_globales['formularios_fallidos'] += 1
                    estadisticas_globales['total_archivos_fallidos'] += len(archivos)
                
            except Exception as e:
                error_msg = f"Error procesando formulario {formulario_data.get('formularioTipo', 'desconocido')}: {str(e)}"
                logger.error(error_msg, exc_info=True)
                errores_procesamiento.append({
                    'formulario': formulario_data.get('formularioTipo', 'desconocido'),
                    'error': error_msg,
                    'tipo_error': 'excepcion_procesamiento'
                })
                estadisticas_globales['formularios_fallidos'] += 1
                estadisticas_globales['total_archivos_fallidos'] += len(formulario_data.get('archivos', []))
        
        # 🆕 FINALIZAR ESTADÍSTICAS GLOBALES
        estadisticas_globales['tiempo_fin'] = datetime.now()
        estadisticas_globales['duracion_total_segundos'] = (
            estadisticas_globales['tiempo_fin'] - estadisticas_globales['tiempo_inicio']
        ).total_seconds()
        estadisticas_globales['errores_por_formulario'] = errores_procesamiento
        
        # Evaluar resultado general
        if estadisticas_globales['total_archivos_exitosos'] == 0 and len(errores_procesamiento) > 0:
            return {
                'success': False,
                'message': f'No se pudo subir ningún archivo. {len(errores_procesamiento)} errores en formularios',
                'code': 'UPLOAD_FAILED',
                'estadisticas': estadisticas_globales,
                'errores': errores_procesamiento
            }
        
        # Mensaje de éxito con estadísticas
        success_message = f"Procesados {estadisticas_globales['total_archivos_exitosos']} archivos"
        
        if estadisticas_globales['formularios_exitosos'] > 0:
            success_message += f" en {estadisticas_globales['formularios_exitosos']} formulario(s)"
        
        if len(errores_procesamiento) > 0:
            success_message += f" con {len(errores_procesamiento)} advertencia(s)"
        
        # 📊 LOG DE ESTADÍSTICAS FINALES
        logger.info(f"📊 === RESUMEN GLOBAL DE PROCESAMIENTO ===")
        logger.info(f"   📋 Formularios procesados: {estadisticas_globales['formularios_procesados']}")
        logger.info(f"   ✅ Formularios exitosos: {estadisticas_globales['formularios_exitosos']}")
        logger.info(f"   ❌ Formularios fallidos: {estadisticas_globales['formularios_fallidos']}")
        logger.info(f"   📄 Archivos intentados: {estadisticas_globales['total_archivos_intentados']}")
        logger.info(f"   ✅ Archivos exitosos: {estadisticas_globales['total_archivos_exitosos']}")
        logger.info(f"   ❌ Archivos fallidos: {estadisticas_globales['total_archivos_fallidos']}")
        logger.info(f"   📏 Tamaño total: {estadisticas_globales['tamaño_total_mb']:.2f} MB")
        logger.info(f"   ⏱️ Duración total: {estadisticas_globales['duracion_total_segundos']:.2f} segundos")
        logger.info(f"   🆔 DocsIDs generados: {docs_ids_generados}")
        
        return {
            'success': True,
            'message': success_message,
            'total_archivos_subidos': estadisticas_globales['total_archivos_exitosos'],
            'docs_ids': docs_ids_generados,
            'errores': errores_procesamiento,
            'formularios_procesados': formularios_procesados,
            'estadisticas': estadisticas_globales
        }
        
    except Exception as e:
        error_msg = f"Error general procesando documentos: {str(e)}"
        logger.error(error_msg, exc_info=True)
        return {
            'success': False,
            'message': 'Error interno procesando documentos',
            'code': 'PROCESSING_ERROR',
            'technical_error': error_msg,
            'estadisticas': {
                'error_general': error_msg,
                'formularios_procesados': 0,
                'total_archivos_exitosos': 0,
                'total_archivos_fallidos': len(archivos_formularios) if archivos_formularios else 0
            }
        }

def procesar_form_data_con_archivos_v3(request) -> dict:
    """
    Versión mejorada para procesar FormData con mejor manejo de archivos
    
    Args:
        request: Objeto request de Flask
    
    Returns:
        Dict: Datos estructurados incluyendo archivos organizados por formulario
    """
    try:
        # Obtener datos JSON del FormData
        json_data = request.form.get('data')
        if json_data:
            data = json.loads(json_data)
        else:
            data = {}
        
        # 🆕 PROCESAMIENTO MEJORADO DE ARCHIVOS POR FORMULARIO
        archivos_formularios = []
        
        # Mapeo de tipos de formularios
        form_modules = {
            'SolicVis': 'FormSolicVis', # ← SOLVIS
            'PTTransNac': 'FormPTTransNac', # ← FPETABTRANS
            'PTTransUL': 'FormPTTransUL',  # ← FPETABTRANSUL
            'Aire': 'FormAire', # ← Aire
            'SistCritUPS': 'FormSistCritUPS', # ← SistCritUPS
            'Bess': 'FormBess', # Step
            'Solar': 'FormSolar', # ← SOLARR
            'EnergyCogeneration': 'FormEneCog', # ← EnergyCogeneration
            'Imess': 'FormImess', # ← Imess
            'ProyecEsp': 'FormProyecEsp' # ← PROYESP
        }
        
        logger.info("🔍 Procesando archivos de FormData...")
        
        for tipo_form, modulo in form_modules.items():
            # Buscar archivos con patrón: files_[tipo][]
            field_name = f'files_{tipo_form}[]'
            archivos_tipo = request.files.getlist(field_name)
            
            logger.debug(f"🔍 Buscando archivos en campo: {field_name}")
            logger.debug(f"🔍 Archivos encontrados: {len(archivos_tipo)}")
            
            # 🆕 VALIDACIÓN MEJORADA DE ARCHIVOS
            archivos_validos = []
            archivos_invalidos = []
            
            for i, archivo in enumerate(archivos_tipo):
                # Validación básica
                if not archivo:
                    logger.warning(f"❌ Archivo {i+1} en {tipo_form}: objeto archivo vacío")
                    archivos_invalidos.append(f"Archivo {i+1}: objeto vacío")
                    continue
                
                if not archivo.filename or archivo.filename.strip() == '':
                    logger.warning(f"❌ Archivo {i+1} en {tipo_form}: sin nombre de archivo")
                    archivos_invalidos.append(f"Archivo {i+1}: sin nombre")
                    continue
                
                # Validación de tamaño
                archivo.seek(0, os.SEEK_END)
                file_size = archivo.tell()
                archivo.seek(0)  # Regresar al inicio
                
                if file_size == 0:
                    logger.warning(f"❌ Archivo {archivo.filename} en {tipo_form}: archivo vacío")
                    archivos_invalidos.append(f"{archivo.filename}: archivo vacío")
                    continue
                
                if file_size > 2 * 1024 * 1024:  # 2MB
                    size_mb = file_size / (1024 * 1024)
                    logger.warning(f"❌ Archivo {archivo.filename} en {tipo_form}: muy grande ({size_mb:.2f}MB)")
                    archivos_invalidos.append(f"{archivo.filename}: muy grande ({size_mb:.2f}MB)")
                    continue
                
                # Validación de extensión
                extension = Path(archivo.filename).suffix.lower()
                extensiones_permitidas = {'.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.xlsx', '.xls'}
                
                if extension not in extensiones_permitidas:
                    logger.warning(f"❌ Archivo {archivo.filename} en {tipo_form}: extensión no permitida ({extension})")
                    archivos_invalidos.append(f"{archivo.filename}: extensión no permitida ({extension})")
                    continue
                
                # 🆕 VALIDACIÓN DE CONTENIDO BÁSICA
                try:
                    # Leer los primeros bytes para validación básica
                    archivo.seek(0)
                    header = archivo.read(1024)
                    archivo.seek(0)
                    
                    # Verificar que no está corrupto (contiene bytes válidos)
                    if len(header) == 0:
                        logger.warning(f"❌ Archivo {archivo.filename} en {tipo_form}: contenido vacío")
                        archivos_invalidos.append(f"{archivo.filename}: contenido vacío")
                        continue
                        
                except Exception as e:
                    logger.error(f"❌ Error leyendo archivo {archivo.filename}: {str(e)}")
                    archivos_invalidos.append(f"{archivo.filename}: error de lectura")
                    continue
                
                # Archivo válido
                archivos_validos.append(archivo)
                logger.info(f"✅ Archivo válido: {archivo.filename} ({file_size/1024:.1f} KB)")
            
            # Reportar resultados de validación
            if archivos_invalidos:
                logger.warning(f"⚠️ {tipo_form}: {len(archivos_invalidos)} archivos inválidos:")
                for error in archivos_invalidos:
                    logger.warning(f"   - {error}")
            
            if archivos_validos:
                total_size = sum(
                    archivo.seek(0, os.SEEK_END) or archivo.tell() or 0 
                    for archivo in archivos_validos
                    if archivo.seek(0) is None  # seek(0) regresa None, pero necesitamos regresar al inicio
                )
                
                # Regresar todos los archivos al inicio
                for archivo in archivos_validos:
                    archivo.seek(0)
                
                archivos_formularios.append({
                    'formularioTipo': tipo_form,
                    'formularioNombre': modulo,
                    'archivos': archivos_validos,
                    'cantidad': len(archivos_validos),
                    'totalSize': total_size,
                    'archivosInvalidos': archivos_invalidos
                })
                
                logger.info(f"📎 {tipo_form}: {len(archivos_validos)} archivos válidos ({total_size/1024:.1f} KB total)")
        
        # Agregar archivos a los datos si existen
        if archivos_formularios:
            data['archivos'] = archivos_formularios
            total_archivos = sum(form['cantidad'] for form in archivos_formularios)
            total_size = sum(form['totalSize'] for form in archivos_formularios)
            
            logger.info(f"📁 Total procesado: {total_archivos} archivos válidos ({total_size/1024/1024:.2f} MB)")
        else:
            logger.info("📄 No se encontraron archivos válidos en el FormData")
        
        return data
        
    except json.JSONDecodeError as e:
        logger.error(f"❌ Error decodificando JSON del FormData: {str(e)}")
        return {}
    except Exception as e:
        logger.error(f"❌ Error procesando FormData con archivos: {str(e)}", exc_info=True)
        return {}

def upload_form_files_v2(form_id: str, opportunity_id: str, files: list, 
                        user_id: int, created_by: str) -> dict:
    """
    Versión mejorada para subir múltiples archivos de un formulario
    
    Args:
        form_id (str): ID del formulario
        opportunity_id (str): ID de la oportunidad
        files (list): Lista de archivos FileStorage
        user_id (int): ID del usuario
        created_by (str): Nombre del usuario
    
    Returns:
        dict: Resultado del procesamiento con estadísticas detalladas
    """
    try:
        if not files or len(files) == 0:
            return {
                'success': True,
                'message': 'No hay archivos para procesar',
                'archivos_subidos': [],
                'archivos_fallidos': [],
                'docs_id': None,
                'estadisticas': {
                    'total_intentados': 0,
                    'total_exitosos': 0,
                    'total_fallidos': 0,
                    'tamaño_total': 0
                }
            }
        
        logger.info(f"🚀 Iniciando upload de {len(files)} archivos para FormID {form_id}")
        
        # Construir ruta base para formularios
        ruta_completa = f"Ventas/Formularios/{opportunity_id}/{form_id}"
        origen = "Q_SpQ_FormsHead"
        
        # 🆕 CREAR UN SOLO DocsHead PARA TODO EL FORMULARIO
        from Consultas_SQL.Utilities.DocsManagementSQL import crear_docs_head
        
        docs_head_result = crear_docs_head(user_id, origen, created_by)
        if not docs_head_result['success']:
            return {
                'success': False,
                'message': f'Error creando grupo de documentos: {docs_head_result["message"]}',
                'code': 'DOCS_HEAD_ERROR'
            }
        
        docs_id = docs_head_result['docs_id']
        logger.info(f"📋 DocsHead {docs_id} creado para FormID {form_id}")
        
        # 🆕 ESTADÍSTICAS MEJORADAS
        estadisticas = {
            'total_intentados': len(files),
            'total_exitosos': 0,
            'total_fallidos': 0,
            'tamaño_total': 0,
            'tiempo_inicio': datetime.now(),
            'archivos_por_tipo': {},
            'errores_por_tipo': {}
        }
        
        archivos_subidos = []
        archivos_fallidos = []
        
        for index, file_obj in enumerate(files, 1):
            try:
                # Validar que el archivo no esté vacío
                if not file_obj or not file_obj.filename:
                    error_msg = f"Archivo {index} está vacío o sin nombre"
                    logger.warning(f"⚠️ {error_msg}")
                    archivos_fallidos.append({
                        'archivo_original': f'archivo_{index}',
                        'error': error_msg,
                        'tipo_error': 'archivo_vacio'
                    })
                    estadisticas['errores_por_tipo']['archivo_vacio'] = estadisticas['errores_por_tipo'].get('archivo_vacio', 0) + 1
                    continue
                
                # Obtener información del archivo
                file_obj.seek(0, os.SEEK_END)
                file_size = file_obj.tell()
                file_obj.seek(0)
                
                extension = Path(file_obj.filename).suffix.lower()
                estadisticas['archivos_por_tipo'][extension] = estadisticas['archivos_por_tipo'].get(extension, 0) + 1
                
                logger.info(f"📄 Procesando archivo {index}/{len(files)}: {file_obj.filename} ({file_size/1024:.1f} KB)")
                
                # Crear descripción específica para formularios
                description = f"Archivo de {form_id} - Oportunidad {opportunity_id}"
                
                # 🆕 USAR upload_document_to_existing_docs para reutilizar DocsHead
                resultado = upload_document_to_existing_docs(
                    file_obj=file_obj,
                    docs_id=docs_id,
                    title=file_obj.filename,
                    description=description,
                    ruta_completa=ruta_completa
                )
                
                if resultado['success']:
                    archivos_subidos.append({
                        'archivo_original': file_obj.filename,
                        'doc_line_id': resultado['data']['doc_line_id'],
                        'download_url': resultado['data']['download_url'],
                        'docs_id': docs_id,
                        'remote_path': resultado['data']['remote_path'],
                        'file_size': file_size,
                        'file_type': extension,
                        'upload_order': index
                    })
                    
                    estadisticas['total_exitosos'] += 1
                    estadisticas['tamaño_total'] += file_size
                    
                    logger.info(f"✅ Archivo {index} subido: {file_obj.filename} -> {resultado['data']['download_url']}")
                else:
                    error_msg = resultado.get('message', 'Error desconocido en upload')
                    archivos_fallidos.append({
                        'archivo_original': file_obj.filename,
                        'error': error_msg,
                        'tipo_error': 'upload_failed',
                        'file_size': file_size
                    })
                    
                    estadisticas['errores_por_tipo']['upload_failed'] = estadisticas['errores_por_tipo'].get('upload_failed', 0) + 1
                    logger.error(f"❌ Error subiendo archivo {index}: {error_msg}")
                    
            except Exception as e:
                error_msg = f"Error procesando archivo {index}: {str(e)}"
                logger.error(error_msg, exc_info=True)
                
                archivos_fallidos.append({
                    'archivo_original': getattr(file_obj, 'filename', f'archivo_{index}'),
                    'error': error_msg,
                    'tipo_error': 'excepcion',
                    'excepcion': str(e)
                })
                
                estadisticas['errores_por_tipo']['excepcion'] = estadisticas['errores_por_tipo'].get('excepcion', 0) + 1
        
        # 🆕 FINALIZAR ESTADÍSTICAS
        estadisticas['total_fallidos'] = len(archivos_fallidos)
        estadisticas['tiempo_fin'] = datetime.now()
        estadisticas['duracion_segundos'] = (estadisticas['tiempo_fin'] - estadisticas['tiempo_inicio']).total_seconds()
        estadisticas['tamaño_total_mb'] = estadisticas['tamaño_total'] / (1024 * 1024)
        
        # Evaluar resultado general
        success = estadisticas['total_exitosos'] > 0
        
        if estadisticas['total_fallidos'] == 0:
            message = f"Todos los archivos ({estadisticas['total_exitosos']}) fueron subidos exitosamente"
        elif estadisticas['total_exitosos'] == 0:
            message = f"No se pudo subir ningún archivo. {estadisticas['total_fallidos']} errores"
            success = False
        else:
            message = f"{estadisticas['total_exitosos']} archivos subidos, {estadisticas['total_fallidos']} fallaron"
        
        # 📊 LOG DE ESTADÍSTICAS FINALES
        logger.info(f"📊 Resumen de upload para FormID {form_id}:")
        logger.info(f"   ✅ Exitosos: {estadisticas['total_exitosos']}")
        logger.info(f"   ❌ Fallidos: {estadisticas['total_fallidos']}")
        logger.info(f"   📏 Tamaño total: {estadisticas['tamaño_total_mb']:.2f} MB")
        logger.info(f"   ⏱️ Duración: {estadisticas['duracion_segundos']:.2f} segundos")
        
        if estadisticas['archivos_por_tipo']:
            logger.info(f"   📁 Tipos de archivo:")
            for ext, count in estadisticas['archivos_por_tipo'].items():
                logger.info(f"      {ext}: {count} archivo(s)")
        
        if estadisticas['errores_por_tipo']:
            logger.info(f"   ⚠️ Tipos de error:")
            for error_type, count in estadisticas['errores_por_tipo'].items():
                logger.info(f"      {error_type}: {count} error(es)")
        
        return {
            'success': success,
            'message': message,
            'archivos_subidos': archivos_subidos,
            'archivos_fallidos': archivos_fallidos,
            'total_procesados': estadisticas['total_intentados'],
            'ruta_completa': ruta_completa,
            'docs_id': docs_id,
            'download_urls': [archivo['download_url'] for archivo in archivos_subidos],
            'estadisticas': estadisticas  # 🆕 Incluir estadísticas completas
        }
        
    except Exception as e:
        error_msg = f"Error general en upload_form_files_v2: {str(e)}"
        logger.error(error_msg, exc_info=True)
        return {
            'success': False,
            'message': 'Error interno al procesar archivos del formulario',
            'code': 'FORM_FILES_ERROR',
            'technical_error': error_msg,
            'archivos_subidos': [],
            'archivos_fallidos': [],
            'docs_id': None,
            'estadisticas': {
                'total_intentados': len(files) if files else 0,
                'total_exitosos': 0,
                'total_fallidos': len(files) if files else 0,
                'error_general': error_msg
            }
        }

def extraer_datos_formulario_general(formulario_general):
    """
    Extrae datos específicos del formulario general para el correo
    
    Args:
        formulario_general (dict): Datos del formulario general
    
    Returns:
        dict: Datos extraídos y formateados
    """
    try:
        preguntas = formulario_general.get('preguntas', [])
        
        # Mapear preguntas por su texto (más robusto que por índice)
        datos_extraidos = {
            'requiere_visita': False,
            'estado': '',
            'ciudad': '',
            'direccion': '',
            'descripcion_proyecto': ''
        }
        
        for pregunta in preguntas:
            pregunta_texto = pregunta.get('pregunta', '').lower()
            respuesta = pregunta.get('respuesta', '')
            
            if 'visita' in pregunta_texto and 'especialista' in pregunta_texto:
                datos_extraidos['requiere_visita'] = respuesta.lower() in ['sí', 'si', 'yes']
            elif 'estado' in pregunta_texto:
                datos_extraidos['estado'] = respuesta
            elif 'ciudad' in pregunta_texto:
                datos_extraidos['ciudad'] = respuesta
            elif 'dirección' in pregunta_texto or 'direccion' in pregunta_texto:
                datos_extraidos['direccion'] = respuesta
            elif 'información' in pregunta_texto and 'proyecto' in pregunta_texto:
                datos_extraidos['descripcion_proyecto'] = respuesta
        
        return datos_extraidos
        
    except Exception as e:
        logger.warning(f"Error extrayendo datos del formulario general: {str(e)}")
        return {
            'requiere_visita': False,
            'estado': '',
            'ciudad': '',
            'direccion': '',
            'descripcion_proyecto': ''
        }
    
def obtener_archivos_adjuntos(oportunity_id, task_id):
    """
    Construye el arreglo archivos_adjuntos a partir de la info en BD
    para la oportunidad y task indicados.
    """
    rows = consultar_docs_por_oportunidad_y_tarea(oportunity_id, task_id)
    archivos_adjuntos = []
    for row in rows:
        # Según el SELECT:
        # 0 -> CRM_OpportunityID
        # 1 -> FormID
        # 2 -> Title
        # 3 -> Ruta
        # 4 -> Type
        title = row[2]
        ruta = row[3]
        # Determinar tipo según si es URL o ruta local
        if ruta and ruta.lower().startswith(("http://", "https://")):
            tipo = 'url'
        else:
            tipo = 'local'
        # Determinar nombre del archivo
        if title and title.strip():
            nombre = title.strip()
        elif ruta:
            nombre = os.path.basename(ruta)
        else:
            nombre = ''
 
        archivos_adjuntos.append({
            'tipo': tipo,
            'ruta': ruta,
            'nombre': nombre
        })
    return archivos_adjuntos

def enviar_notificacion_cotizacion_especial_v2(data, opportunity_id, opportunity_number, nueva_version, 
                                            forms_created, tasks_generated, user_id, total_archivos_subidos):
    """
    Versión mejorada del envío de correo de notificación.
    Corregida para:
    1. Evitar KeyError si no hay tareas.
    2. Usar current_app para acceder a la configuración (evita AttributeError).
    """
    try:
        from App.Utilities_module.MailManagement import enviar_correo_universal
        from datetime import datetime
        from flask import current_app  # 🆕 IMPORTANTE: Importar current_app
        
        # Obtener nombre del usuario
        user_name = obtener_nombre_usuario(user_id)
        
        # Mapear nombres amigables de formularios
        formularios_nombres = []
        for form_id in forms_created:
            if 'FormSolicVis' in form_id:
                formularios_nombres.append('Solicitud de Visita Técnica')
            elif 'FormPTTransNac' in form_id:
                formularios_nombres.append('Plantas, Tableros y Transformadores Nacionales')
            elif 'FormPTTransUL' in form_id:
                formularios_nombres.append('Plantas, Tableros y Transformadores UL')
            elif 'FormAire' in form_id:
                formularios_nombres.append('Aires Acondicionados')
            elif 'FormSistCritUPS' in form_id:
                formularios_nombres.append('Sistemas Críticos (UPS)')
            elif 'FormBess' in form_id:
                formularios_nombres.append('BESS')
            elif 'FormSolar' in form_id:
                formularios_nombres.append('Sistemas Solares')
            elif 'FormEneCog' in form_id:
                formularios_nombres.append('Energía y Cogeneración')
            elif 'FormImess' in form_id:
                formularios_nombres.append('iMess')
            elif 'FormProyecEsp' in form_id:
                formularios_nombres.append('Proyectos Especiales')
        
        # EXTRAER DATOS DEL FORMULARIO GENERAL DE FORMA ROBUSTA
        datos_proyecto = extraer_datos_formulario_general(data.get('formularioGeneral', {}))

        # Preparar datos para el template
        template_data = {
            # Información de la oportunidad
            'numero_oportunidad': opportunity_number,
            'cliente_nombre': data['oportunidad'].get('CRM_ContactName', 'No especificado'),
            'vendedor_asignado': data['oportunidad'].get('CRM_AssignedSalesperson', 'No asignado'),
            
            # Información del solicitante
            'solicitante_nombre': user_name,
            'fecha_solicitud': datetime.now().strftime('%d/%m/%Y'),
            'hora_solicitud': datetime.now().strftime('%H:%M'),
            
            # Formularios y tareas
            'formularios_solicitados': formularios_nombres,
            'total_formularios': len(forms_created),
            'total_tareas': len(tasks_generated),
            'form_ids': forms_created,
            'version': nueva_version,
            
            # Información del proyecto (extraída de forma robusta)
            'proyecto_estado': datos_proyecto['estado'],
            'proyecto_ciudad': datos_proyecto['ciudad'],
            'proyecto_direccion': datos_proyecto['direccion'],
            'proyecto_descripcion': datos_proyecto['descripcion_proyecto'],
            'requiere_visita': datos_proyecto['requiere_visita'],
            
            # URLs del sistema
            'url_sistema': 'https://sycelephant.com',
            'url_oportunidad': f'https://sycelephant.com/ventas/oportunidad/{opportunity_id}',
        }
        
        # Construir asunto del correo
        asunto = f'🔔 Nueva Solicitud de Cotización Especial - Oportunidad {opportunity_number}'
        if nueva_version > 1:
            asunto += f' (Versión {nueva_version})'
        
        # AGREGAR INDICADOR DE URGENCIA SI REQUIERE VISITA
        if datos_proyecto['requiere_visita']:
            asunto = f'🚨 URGENTE - {asunto} - REQUIERE VISITA TÉCNICA'

        # Inicializar resultado por defecto para evitar KeyError
        resultado = {
            'success': True, 
            'mensaje': 'No se generaron tareas para envío de correo específico.',
            'destinatarios': {'total_destinatarios': 0}
        }

        for taskID in tasks_generated:
            template_data['TaskId'] = int(taskID)
            destinatario = ""
            opportunity_id
            formID_task = get_FormID_by_TaskID(int(taskID))
            archivosAdjuntos = obtener_archivos_adjuntos(opportunity_id,formID_task)
            if IS_PRODUCTION == False:
                destinatario = "victor.barrera@igsa.com.mx"
            else:
                if 'FormSolicVis' in formID_task:
                    destinatario = "marco.escobar@igsa.com.mx"
                elif 'FormPTTransNac' in formID_task:
                    destinatario = "carlos.huitron@igsa.com.mx"
                elif 'FormPTTransUL' in formID_task:
                    destinatario = "jose.lopez@igsa.com.mx"
                elif 'FormAire' in formID_task:
                    destinatario = "rogelio.robles@igsa.com.mx"
                elif 'FormSistCritUPS' in formID_task:
                    destinatario = "rogelio.robles@igsa.com.mx"
                elif 'FormBess' in formID_task:
                    destinatario = "carlos.anguiano@igsa.com.mx"
                elif 'FormSolar' in formID_task:
                    destinatario = "carlos.anguiano@igsa.com.mx"
                elif 'FormEneCog' in formID_task:
                    destinatario = "diego.rivas@igsa.com.mx"
                elif 'FormImess' in formID_task:
                    destinatario = "carlos.anguiano@igsa.com.mx"
                elif 'FormProyecEsp' in formID_task:
                    destinatario = "arturo.martinez@igsa.com.mx"

            # MODO PROTOTIPO DE CORREO (Corregido usando current_app)
            if current_app.config.get('EMAIL_PROTOTYPE_MODE'):
                destinatario = current_app.config.get('EMAIL_PROTOTYPE_TOADRESS')

            resultado = enviar_correo_universal(
                template_path='Emails/Ventas/Cotiz/CotizEspSolicitudMail.html',
                asunto=asunto,
                destinatarios_adicionales={
                    'TO': [destinatario],  # fallback si no hay correo
                    'CC': ['victor.barrera.@igsa.com.mx','victor.cervantes@igsa.com.mx','alexis.moreno@igsa.com.mx']
                },
                mail_list_id=1000,  # Lista de correos de ingeniería
                template_data=template_data,
                archivos_adjuntos=archivosAdjuntos
            )

        # LOG DETALLADO DEL RESULTADO
        if resultado.get('success', False):
            total_destinatarios = resultado.get('destinatarios', {}).get('total_destinatarios', 0)
            logger.info(f"✅ Proceso de correo finalizado para {opportunity_id}. Enviados: {total_destinatarios}")
        else:
            logger.error(f"❌ Error enviando correo para {opportunity_id}: {resultado.get('mensaje', 'Error desconocido')}")
        
        return resultado
        
    except Exception as e:
        logger.error(f"Error en enviar_notificacion_cotizacion_especial_v2: {str(e)}", exc_info=True)
        return {
            'success': False,
            'mensaje': f'Error interno al enviar notificación: {str(e)}',
            'errores': [str(e)]
        }