/**
 * SLM Framework - Elasticsearch Service
 * Servicio de interacción con Elasticsearch usando Axios
 *
 * Equivalente a: appweb/slm.framework/system/slm_es_control.slm
 */

const axios = require('axios');
const logger = require('./logger.slm');

// Cargar variables de entorno
require('dotenv').config();

//======================================================================================================================
// CONFIGURACIÓN DE ELASTICSEARCH
//======================================================================================================================

/**
 * Obtiene la URL del servidor Elasticsearch según el entorno
 * @returns {string} URL del servidor ES
 */
function getEsServer() {
    const isProduction = process.env.NODE_ENV === 'production';
    return isProduction ?
        (process.env.ES_SERVER || 'http://elasticsearch:9200') :
        (process.env.ES_SERVER || process.env.DEV_ES_SERVER || 'http://elasticsearch:9200');
}

/**
 * Obtiene las credenciales de ES según el entorno
 * @returns {Object|null} Credenciales {username, password} o null si no hay auth
 */
function getEsCredentials() {
    const isProduction = process.env.NODE_ENV === 'production';

    if (isProduction) {
        const username = process.env.ES_USERNAME;
        const password = process.env.ES_PASSWORD;
        return (username && password) ? { username, password } : null;
    } else {
        const username = process.env.DEV_ES_USERNAME;
        const password = process.env.DEV_ES_PASSWORD;
        return (username && password) ? { username, password } : null;
    }
}

/**
 * Obtiene el índice de ES según el entorno
 * @returns {string} Nombre del índice
 */
function getEsIndex() {
    const isProduction = process.env.NODE_ENV === 'production';
    return isProduction ?
        (process.env.ES_INDEX || 'slm-app-prod') :
        (process.env.DEV_ES_INDEX || 'slm-app-dev');
}

/**
 * Obtiene el índice de logs
 * @returns {string} Nombre del índice de logs
 */
function getEsIndexLog() {
    return `${getEsIndex()}-log`;
}

/**
 * Obtiene el índice de sesiones
 * @returns {string} Nombre del índice de sesiones
 */
function getEsIndexSessions() {
    return 'sessions';
}

/**
 * Obtiene timestamp actual en milisegundos
 * @returns {number} Timestamp en milisegundos
 */
function getTimestamp() {
    return Date.now();
}

//======================================================================================================================
// AXIOS INSTANCE CONFIGURADO PARA ELASTICSEARCH
//======================================================================================================================

/**
 * Crea una instancia de Axios configurada para Elasticsearch
 * @returns {import('axios').AxiosInstance} Instancia de Axios configurada
 */
function createEsClient() {
    const config = {
        baseURL: getEsServer(),
        headers: {
            'Content-Type': 'application/json',
            'User-Agent': 'SLM_ES_CONTROL_NODEJS/1.0'
        },
        timeout: 30000, // 30 segundos
        validateStatus: (status) => status < 500 // No lanzar error en 4xx
    };

    // Agregar autenticación si existe
    const credentials = getEsCredentials();
    if (credentials) {
        config.auth = credentials;
    }

    const instance = axios.create(config);

    // Interceptor para logging de requests (solo en desarrollo)
    if (process.env.NODE_ENV === 'development') {
        instance.interceptors.request.use(
            (config) => {
                logger.info('ES Request', {
                    method: config.method?.toUpperCase(),
                    url: config.url,
                    hasData: !!config.data
                });
                return config;
            },
            (error) => {
                logger.error('ES Request Error', { error: error.message });
                return Promise.reject(error);
            }
        );
    }

    // Interceptor para manejo de errores
    instance.interceptors.response.use(
        (response) => response,
        (error) => {
            if (error.response) {
                // El servidor respondió con un código de estado fuera del rango 2xx
                logger.error('ES Response Error', {
                    status: error.response.status,
                    statusText: error.response.statusText,
                    url: error.config?.url,
                    data: error.response.data
                });
            } else if (error.request) {
                // La petición se hizo pero no hubo respuesta
                logger.error('ES No Response', {
                    url: error.config?.url,
                    message: error.message
                });
            } else {
                // Algo pasó al configurar la petición
                logger.error('ES Request Setup Error', { message: error.message });
            }
            return Promise.reject(error);
        }
    );

    return instance;
}

// Instancia global de Elasticsearch client
const esClient = createEsClient();

//======================================================================================================================
// FUNCIONES DE INTERACCIÓN CON ELASTICSEARCH
//======================================================================================================================

/**
 * Ejecuta una petición HTTP a Elasticsearch
 * @param {string} url - URL del endpoint (ej: '/_doc/123')
 * @param {Object|null} data - Datos a enviar (opcional)
 * @param {string} method - Método HTTP (GET, POST, PUT, DELETE)
 * @returns {Promise<Object>} Respuesta de ES
 * @throws {Error} Si hay error en la petición
 */
async function es_execute(url, data = null, method = 'GET') {
    try {
        const config = {
            method: method.toUpperCase(),
            url: url.startsWith('/') ? url : `/${url}`
        };

        if (data && ['POST', 'PUT', 'PATCH'].includes(config.method)) {
            config.data = data;
        }

        const response = await esClient(config);
        return response.data;
    } catch (error) {
        logger.error('es_execute failed', {
            url,
            method,
            error: error.message,
            response: error.response?.data
        });
        throw error;
    }
}

/**
 * Crea un nuevo objeto en Elasticsearch
 * @param {Object} data - Datos del objeto a crear
 * @param {string} id - ID del documento (opcional, ES genera uno si se omite)
 * @param {string} es_index - Índice (opcional, usa el índice por defecto)
 * @returns {Promise<Object>} Respuesta de ES con _id, _version, result
 * @throws {Error} Si hay error al crear
 *
 * @example
 * const user = await setNewObject({
 *   slm: { type: 'user' },
 *   name: 'John',
 *   email: 'john@example.com'
 * }, 'user-123');
 */
async function setNewObject(data, id = '', es_index = '') {
    const index = es_index || getEsIndex();

    // Agregar timestamp si no existe
    if (!data.slm) {
        data.slm = {};
    }
    if (!data.slm.timestamp) {
        data.slm.timestamp = getTimestamp();
    }

    try {
        const url = id ?
            `/${index}/_doc/${id}?refresh=true` :
            `/${index}/_doc?refresh=true`;

        const response = await es_execute(url, data, 'POST');

        logger.info('Object created', {
            index,
            id: response._id,
            result: response.result
        });

        return response;
    } catch (error) {
        logger.error('setNewObject failed', {
            index,
            id,
            error: error.message
        });
        throw error;
    }
}

/**
 * Actualiza un objeto por ID
 * @param {string} id - ID del documento
 * @param {Object} data - Datos a actualizar
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<Object>} Respuesta de ES
 * @throws {Error} Si hay error al actualizar
 *
 * @example
 * // Actualización simple
 * await updateObjectByID('user-123', { name: 'Jane' });
 *
 * // Actualización con script
 * await updateObjectByID('user-123', {
 *   script: {
 *     source: 'ctx._source.counter += params.count',
 *     params: { count: 1 }
 *   }
 * });
 */
async function updateObjectByID(id, data, es_index = '') {
    const index = es_index || getEsIndex();

    if (!id) {
        throw new Error('ID is required for update');
    }

    try {
        let updateBody;

        // Si tiene script, usar directamente
        if (data.script) {
            updateBody = data;
        } else {
            // Actualización normal con doc
            updateBody = {
                doc: data,
                detect_noop: false
            };
        }

        const response = await es_execute(
            `/${index}/_update/${id}?refresh=true`,
            updateBody,
            'POST'
        );

        logger.info('Object updated', {
            index,
            id,
            result: response.result
        });

        return response;
    } catch (error) {
        logger.error('updateObjectByID failed', {
            index,
            id,
            error: error.message
        });
        throw error;
    }
}

/**
 * Obtiene un objeto por ID
 * @param {string} id - ID del documento
 * @param {string} ext - Parámetros adicionales de query (opcional)
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<Object|null>} Objeto (_source) o null si no existe
 * @throws {Error} Si hay error en la petición
 *
 * @example
 * const user = await getObjectById('user-123');
 * if (user) {
 *   console.log(user.name);
 * }
 */
async function getObjectById(id, ext = '', es_index = '') {
    const index = es_index || getEsIndex();

    if (!id) {
        return null;
    }

    try {
        const url = ext ?
            `/${index}/_doc/${id}?${ext}` :
            `/${index}/_doc/${id}`;

        const response = await es_execute(url, null, 'GET');

        if (!response.found) {
            return null;
        }

        return response._source;
    } catch (error) {
        if (error.response?.status === 404) {
            return null;
        }

        logger.error('getObjectById failed', {
            index,
            id,
            error: error.message
        });
        throw error;
    }
}

/**
 * Obtiene un objeto por valor de campo
 * @param {string} type - Tipo de documento (slm.type)
 * @param {string} field - Campo a buscar
 * @param {string} value - Valor a buscar
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<Object>} Objeto encontrado con id incluido, o {} si no existe
 * @throws {Error} Si hay error en la búsqueda
 *
 * @example
 * const user = await getObjectWithValue('user', 'email.keyword', 'john@example.com');
 * if (user.id) {
 *   console.log('Usuario encontrado:', user.name);
 * }
 */
async function getObjectWithValue(type, field, value, es_index = '') {
    const index = es_index || getEsIndex();

    if (!value) {
        return {};
    }

    try {
        // Construir query DSL
        const query = {
            query: {
                bool: {
                    must: [
                        { term: { 'slm.type': type } }
                    ]
                }
            },
            size: 1
        };

        // Determinar tipo de match según el campo
        if (field.includes('keyword')) {
            query.query.bool.must.push({ term: { [field]: value } });
        } else {
            query.query.bool.must.push({ match: { [field]: value } });
        }

        const response = await es_execute(`/${index}/_search`, query, 'POST');

        if (response.error) {
            return {};
        }

        if (response.hits.total.value > 0) {
            const hit = response.hits.hits[0];
            return {
                id: hit._id,
                ...hit._source
            };
        }

        return {};
    } catch (error) {
        logger.error('getObjectWithValue failed', {
            index,
            type,
            field,
            value,
            error: error.message
        });
        throw error;
    }
}

/**
 * Elimina un objeto por ID
 * @param {string} id - ID del documento
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<Object>} Respuesta de ES
 * @throws {Error} Si hay error al eliminar
 *
 * @example
 * await deleteObjectByID('user-123');
 */
async function deleteObjectByID(id, es_index = '') {
    const index = es_index || getEsIndex();

    if (!id) {
        throw new Error('ID is required for delete');
    }

    try {
        const response = await es_execute(
            `/${index}/_doc/${id}?refresh=true`,
            null,
            'DELETE'
        );

        logger.info('Object deleted', {
            index,
            id,
            result: response.result
        });

        return response;
    } catch (error) {
        logger.error('deleteObjectByID failed', {
            index,
            id,
            error: error.message
        });
        throw error;
    }
}

/**
 * Realiza una búsqueda con query DSL de Elasticsearch
 * @param {Object} query - Query DSL de Elasticsearch
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<Object>} Respuesta completa de ES con hits
 * @throws {Error} Si hay error en la búsqueda
 *
 * @example
 * const results = await search({
 *   query: {
 *     bool: {
 *       must: [
 *         { term: { 'slm.type': 'user' } },
 *         { match: { name: 'john' } }
 *       ]
 *     }
 *   },
 *   size: 10,
 *   from: 0
 * });
 *
 * console.log('Total:', results.hits.total.value);
 * results.hits.hits.forEach(hit => {
 *   console.log(hit._id, hit._source);
 * });
 */
async function search(query, es_index = '') {
    const index = es_index || getEsIndex();

    try {
        const response = await es_execute(`/${index}/_search`, query, 'POST');
        return response;
    } catch (error) {
        logger.error('search failed', {
            index,
            query: JSON.stringify(query),
            error: error.message
        });
        throw error;
    }
}

/**
 * Realiza una búsqueda simple por tipo y filtros
 * @param {string} type - Tipo de documento (slm.type)
 * @param {Object} filters - Filtros adicionales {campo: valor}
 * @param {number} size - Número de resultados (default: 10)
 * @param {number} from - Offset para paginación (default: 0)
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<Array>} Array de objetos encontrados con id
 *
 * @example
 * const users = await searchByType('user', { status: 'active' }, 10, 0);
 * users.forEach(user => console.log(user.name));
 */
async function searchByType(type, filters = {}, size = 10, from = 0, es_index = '') {
    const index = es_index || getEsIndex();

    try {
        const must = [
            { term: { 'slm.type': type } }
        ];

        // Agregar filtros
        Object.entries(filters).forEach(([field, value]) => {
            if (field.includes('keyword')) {
                must.push({ term: { [field]: value } });
            } else {
                must.push({ match: { [field]: value } });
            }
        });

        const query = {
            query: { bool: { must } },
            size,
            from,
            sort: [
                { 'slm.timestamp': { order: 'desc' } }
            ]
        };

        const response = await search(query, index);

        return response.hits.hits.map(hit => ({
            id: hit._id,
            ...hit._source
        }));
    } catch (error) {
        logger.error('searchByType failed', {
            index,
            type,
            filters,
            error: error.message
        });
        throw error;
    }
}

/**
 * Cuenta documentos que coinciden con una query
 * @param {Object} query - Query DSL de Elasticsearch
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<number>} Número de documentos que coinciden
 *
 * @example
 * const count = await countDocuments({
 *   query: {
 *     term: { 'slm.type': 'user' }
 *   }
 * });
 * console.log('Total users:', count);
 */
async function countDocuments(query, es_index = '') {
    const index = es_index || getEsIndex();

    try {
        const response = await es_execute(`/${index}/_count`, query, 'POST');
        return response.count;
    } catch (error) {
        logger.error('countDocuments failed', {
            index,
            error: error.message
        });
        throw error;
    }
}

/**
 * Bulk insert/update de múltiples documentos
 * @param {Array} operations - Array de operaciones bulk
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<Object>} Respuesta de ES bulk
 *
 * @example
 * await bulkOperations([
 *   { index: { _id: 'user-1' } },
 *   { slm: { type: 'user' }, name: 'John' },
 *   { index: { _id: 'user-2' } },
 *   { slm: { type: 'user' }, name: 'Jane' }
 * ]);
 */
async function bulkOperations(operations, es_index = '') {
    const index = es_index || getEsIndex();

    try {
        // Construir body para bulk
        const body = operations.map(op => JSON.stringify(op)).join('\n') + '\n';

        const response = await es_execute(
            `/${index}/_bulk?refresh=true`,
            body,
            'POST'
        );

        logger.info('Bulk operation completed', {
            index,
            items: response.items?.length || 0,
            errors: response.errors
        });

        return response;
    } catch (error) {
        logger.error('bulkOperations failed', {
            index,
            error: error.message
        });
        throw error;
    }
}

/**
 * Verifica si un documento existe
 * @param {string} id - ID del documento
 * @param {string} es_index - Índice (opcional)
 * @returns {Promise<boolean>} true si existe, false si no
 */
async function existsDocument(id, es_index = '') {
    const index = es_index || getEsIndex();

    if (!id) {
        return false;
    }

    try {
        await es_execute(`/${index}/_doc/${id}`, null, 'HEAD');
        return true;
    } catch (error) {
        if (error.response?.status === 404) {
            return false;
        }
        throw error;
    }
}

//======================================================================================================================
// EXPORTS
//======================================================================================================================

module.exports = {
    // Configuración
    getEsServer,
    getEsCredentials,
    getEsIndex,
    getEsIndexLog,
    getEsIndexSessions,
    getTimestamp,

    // Cliente Axios
    esClient,

    // Funciones de interacción
    es_execute,
    setNewObject,
    updateObjectByID,
    getObjectById,
    getObjectWithValue,
    deleteObjectByID,
    search,
    searchByType,
    countDocuments,
    bulkOperations,
    existsDocument
};
