/*
  Сервис интеграции ПП Парус 8 с WEB API
  Модуль ядра: вспомогательные функции
*/

//----------------------
// Подключение библиотек
//----------------------

const fs = require("fs"); //Работа с файлами
const os = require("os"); //Средства операционной системы
const xml2js = require("xml2js"); //Конвертация XML в JSON
const Schema = require("validate"); //Схемы валидации
const nodemailer = require("nodemailer"); //Отправка E-Mail сообщений
const {
    SERR_UNEXPECTED,
    SMODULES_PATH_MODULES,
    SERR_OBJECT_BAD_INTERFACE,
    SERR_MODULES_NO_MODULE_SPECIFIED,
    SERR_MODULES_BAD_INTERFACE,
    SERR_MAIL_FAILED
} = require("./constants"); //Глобавльные константы системы
const { ServerError } = require("./server_errors"); //Ошибка сервера
const prmsUtilsSchema = require("../models/prms_utils"); //Схемы валидации параметров функций
const { SPROTOCOL_HTTP, SPROTOCOL_KAFKA } = require("../models/obj_service"); //Схемы валидации сервиса

//------------
// Тело модуля
//------------

//Глубокое клонирование
const deepClone = value => {
    //Буфер для фиксации "клонированных" элементов исходного объекта
    const seen = new WeakMap();
    //Функция рекурсивного клонирования (разбирает тип полученного значения и в зависимости от него выполняет клонирование, сохраняя прототипы)
    const clone = val => {
        //Примитивы и функции возвращаем как есть
        if (val === null || typeof val !== "object") return val;
        if (typeof val === "function") return val;
        //Защита от циклических ссылок
        if (seen.has(val)) return seen.get(val);
        //Специализированные типы
        if (val instanceof Date) return new Date(val);
        if (Buffer.isBuffer(val)) return Buffer.from(val);
        if (ArrayBuffer.isView(val)) return new val.constructor(val);
        if (val instanceof RegExp) return new RegExp(val);
        //Ошибки: сохраняем тип, сообщение, стек и пользовательские поля
        if (val instanceof Error) {
            const clonedError = new val.constructor(val.message);
            seen.set(val, clonedError);
            clonedError.name = val.name;
            clonedError.stack = val.stack;
            Reflect.ownKeys(val).forEach(key => {
                if (key === "name" || key === "message" || key === "stack") return;
                const desc = Object.getOwnPropertyDescriptor(val, key);
                if (!desc) return;
                if ("value" in desc) desc.value = clone(desc.value);
                try {
                    Object.defineProperty(clonedError, key, desc);
                } catch (e) {}
            });
            return clonedError;
        }
        //Коллекции
        if (val instanceof Map) {
            const m = new Map();
            seen.set(val, m);
            val.forEach((v, k) => m.set(clone(k), clone(v)));
            return m;
        }
        if (val instanceof Set) {
            const s = new Set();
            seen.set(val, s);
            val.forEach(v => s.add(clone(v)));
            return s;
        }
        //Коллекции, которые невозможно корректно клонировать - возвращаем по ссылке
        if (val instanceof WeakMap || val instanceof WeakSet || val instanceof Promise) {
            return val;
        }
        //Массивы
        if (Array.isArray(val)) {
            const arr = new Array(val.length);
            seen.set(val, arr);
            for (let i = 0; i < val.length; i++) {
                if (Object.prototype.hasOwnProperty.call(val, i)) {
                    arr[i] = clone(val[i]);
                }
            }
            return arr;
        }
        //Общие объекты и пользовательские классы: сохраняем прототип и дескрипторы свойств
        const proto = Object.getPrototypeOf(val);
        const obj = Object.create(proto);
        seen.set(val, obj);
        Reflect.ownKeys(val).forEach(key => {
            const desc = Object.getOwnPropertyDescriptor(val, key);
            if (!desc) return;
            if ("value" in desc) {
                desc.value = clone(desc.value);
            }
            try {
                Object.defineProperty(obj, key, desc);
            } catch (e) {}
        });
        return obj;
    };
    //Выполняем копирование рекурсивно с верхнего уровня
    try {
        //Возвращаем что получилось
        return clone(value);
    } catch (e) {
        //В случае непредвиденной ошибки формируем исключение
        const err = new Error("Ошибка глубокого копирования объекта");
        err.originalError = e;
        throw err;
    }
};

//Валидация объекта
const validateObject = (obj, schema, sObjName) => {
    //Объявим результат
    let sRes = "";
    //Если пришла верная схема
    if (schema instanceof Schema) {
        //И есть что проверять
        if (obj) {
            //Сделаем это
            const objTmp = deepClone(obj);
            const errors = schema.validate(objTmp, { strip: false });
            //Если есть ошибки
            if (errors && Array.isArray(errors)) {
                if (errors.length > 0) {
                    //Сформируем из них сообщение об ошибке валидации
                    let a = errors.map(e => {
                        return e.message;
                    });
                    sRes = `Объект${sObjName ? ` "${sObjName}" ` : " "}имеет некорректный формат: ${Array.from(new Set(a)).join("; ")}`;
                }
            } else {
                //Валидатор вернул не то, что мы ожидали
                sRes = "Неожиданный ответ валидатора";
            }
        } else {
            //Нам не передали объект на проверку
            sRes = `Объект${sObjName ? ` "${sObjName}" ` : " "}не указан`;
        }
    } else {
        //Пришла не схема валидации а непонятно что
        sRes = "Ошибочный формат схемы валидации";
    }
    //Вернем результат
    return sRes;
};

//Формирование полного пути к подключаемому модулю
const makeModuleFullPath = sModuleName => {
    //Если имя модуля передано
    if (sModuleName) {
        //Объединим его с шаблоном пути до библиотеки модулей
        return `${SMODULES_PATH_MODULES}/${sModuleName}`;
    } else {
        //Нет имени модуля - нет полного пути
        return "";
    }
};

//Формирование текста ошибки
const makeErrorText = e => {
    //Сообщение об ошибке по умолчанию
    let sErr = `${SERR_UNEXPECTED}: ${e.message}`;
    //Если это простая строка
    if (e instanceof String || typeof e === "string") sErr = `${SERR_UNEXPECTED}: ${e}`;
    //Если это наше внутреннее сообщение, с кодом, то сделаем ошибку более информативной
    if (e instanceof ServerError) sErr = `${e.sCode}: ${e.sMessage}`;
    //Вернем ответ
    return sErr;
};

//Считывание наименования модуля-обработчика сервера приложений (ожидаемый формат - <МОДУЛЬ>.js/<ФУНКЦИЯ>)
const getAppSrvModuleName = sAppSrv => {
    //Если есть что разбирать
    if (sAppSrv) {
        //И если это строка
        if (sAppSrv instanceof String || typeof sAppSrv === "string") {
            //Проверим наличие разделителя между именем модуля и функции
            if (sAppSrv.indexOf("/") === -1) {
                //Нет разделителя - нечего вернуть
                return null;
            } else {
                //Вернём всё, что левее разделителя
                return sAppSrv.substring(0, sAppSrv.indexOf("/"));
            }
        } else {
            //Пришла не строка
            return null;
        }
    } else {
        //Ничего не пришло
        return null;
    }
};

//Считывание наименования функции модуля-обработчика сервера приложений (ожидаемый формат - <МОДУЛЬ>.js/<ФУНКЦИЯ>)
const getAppSrvFunctionName = sAppSrv => {
    //Если есть что разбирать
    if (sAppSrv) {
        //И если это строка
        if (sAppSrv instanceof String || typeof sAppSrv === "string") {
            //Проверим наличие разделителя между именем модуля и функции
            if (sAppSrv.indexOf("/") === -1) {
                //Нет разделителя - нечего вернуть
                return null;
            } else {
                //Вернём всё, что правее разделителя
                return sAppSrv.substring(sAppSrv.indexOf("/") + 1, sAppSrv.length);
            }
        } else {
            //Пришла не строка
            return null;
        }
    } else {
        //Ничего не пришло
        return null;
    }
};

//Получение функции обработчика
const getAppSrvFunction = sAppSrv => {
    //Объявим формат (для сообщений об ошибках)
    const sFormat = "(ожидаемый формат: <МОДУЛЬ>/<ФУНКЦИЯ>)";
    //Проверим, что есть что разбирать
    if (!sAppSrv) throw new ServerError(SERR_MODULES_NO_MODULE_SPECIFIED, `Не указаны модуль и функция обработчика ${sFormat}`);
    //Разбираем
    try {
        //Разбираем на модуль и функцию
        let moduleName = getAppSrvModuleName(sAppSrv);
        let funcName = getAppSrvFunctionName(sAppSrv);
        //Проверим, что есть и то и другое
        if (!moduleName) throw Error(`Обработчик ${sAppSrv} не указывает на модуль ${sFormat}`);
        if (!funcName) throw Error(`Обработчик ${sAppSrv} не указывает на функцию ${sFormat}`);
        //Подключаем модуль
        let mdl = null;
        try {
            mdl = require(makeModuleFullPath(moduleName));
        } catch (e) {
            throw Error(
                `Не удалось подключить модуль ${moduleName}, проверье что он существует и не имеет синтаксических ошибок. Ошибка подключения: ${e.message}`
            );
        }
        //Проверяем, что в нём есть эта функция
        if (!mdl[funcName]) throw Error(`Функция ${funcName} не определена в модуле ${moduleName}`);
        //Проверяем, что функция асинхронна и если это так - возвращаем её
        if ({}.toString.call(mdl[funcName]) === "[object AsyncFunction]") return mdl[funcName];
        else throw Error(`Функция ${funcName} модуля ${moduleName} должна быть асинхронной`);
    } catch (e) {
        throw new ServerError(SERR_MODULES_BAD_INTERFACE, e.message);
    }
};

//Отправка E-Mail уведомления
const sendMail = prms => {
    return new Promise((resolve, reject) => {
        //Проверяем структуру переданного объекта для отправки E-Mail уведомления
        let sCheckResult = validateObject(prms, prmsUtilsSchema.sendMail, "Параметры функции отправки E-Mail уведомления");
        //Если структура объекта в норме
        if (!sCheckResult) {
            //Формируем параметры для подключения к SMTP
            let transpOptions = {
                host: prms.mail.sHost,
                port: prms.mail.nPort,
                secure: prms.mail.bSecure,
                tls: {
                    rejectUnauthorized: prms.mail.bRejectUnauthorized
                }
            };
            //Если есть информация о пользователе - добавляем
            if (prms.mail.sUser) {
                transpOptions.auth = {
                    user: prms.mail.sUser,
                    pass: prms.mail.sPass
                };
            }
            //Настраиваем подключение к SMTP-серверу
            let transporter = nodemailer.createTransport(transpOptions);
            //Параметры отправляемого сообщения
            let mailOptions = {
                from: prms.mail.sFrom,
                to: prms.sTo,
                subject: prms.sSubject,
                text: prms.sMessage,
                html: prms.sHTML,
                attachments: prms.attachments
            };
            //Отправляем сообщение
            transporter.sendMail(mailOptions, (error, info) => {
                if (error) {
                    reject(new ServerError(SERR_MAIL_FAILED, `${error.code}: ${error}`));
                } else {
                    if (info.rejected && Array.isArray(info.rejected) && info.rejected.length > 0) {
                        reject(new ServerError(SERR_MAIL_FAILED, `Сообщение не доствлено адресатам: ${info.rejected.join(", ")}`));
                    } else {
                        resolve(info);
                    }
                }
            });
        } else {
            reject(new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult));
        }
    });
};

//Сборка URL по адресу сервиса и функции сервиса
const buildURL = prms => {
    //Проверяем структуру переданного объекта для сборки URL
    let sCheckResult = validateObject(prms, prmsUtilsSchema.buildURL, "Параметры функции формирования URL");
    //Если структура объекта в норме
    if (!sCheckResult) {
        //Формируем URL с учетом лишних "/"
        return `${prms.sSrvRoot.replace(/\/+$/, "")}/${prms.sFnURL.replace(/^\/+/, "")}${prms.sQuery ? `?${prms.sQuery}` : ""}`;
    } else {
        throw new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult);
    }
};

//Получение списка IP-адресов хоста сервера
const getIPs = () => {
    let ips = [];
    //получим список сетевых интерфейсов
    const ifaces = os.networkInterfaces();
    //обходим сетевые интерфейсы
    Object.keys(ifaces).forEach(ifname => {
        ifaces[ifname].forEach(iface => {
            //пропускаем локальный адрес и не IPv4 адреса
            if ("IPv4" !== iface.family || iface.internal !== false) return;
            //добавим адрес к резульату
            ips.push(iface.address);
        });
    });
    //вернем ответ
    return ips;
};

//Разбор XML (обёртка для async/await)
const parseXML = prms => {
    return new Promise((resolve, reject) => {
        //Проверяем структуру переданного объекта для парсинша
        let sCheckResult = validateObject(prms, prmsUtilsSchema.parseXML, "Параметры функции разбора XML");
        //Если структура объекта в норме
        if (!sCheckResult) {
            xml2js.parseString(prms.sXML, prms.options, (err, result) => {
                if (err) reject(err);
                else resolve(result);
            });
        } else {
            reject(new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult));
        }
    });
};

//Разбор параметров сообщения/ответа (XML > JSON)
const parseOptionsXML = async prms => {
    //Проверяем структуру переданных параметров
    let sCheckResult = validateObject(prms, prmsUtilsSchema.parseOptionsXML, "Параметры функции разбора XML параметров сообщения/ответа");
    //Если структура объекта в норме
    if (!sCheckResult) {
        try {
            parseRes = await parseXML({
                sXML: prms.sOptions,
                options: {
                    explicitArray: false,
                    mergeAttrs: true,
                    valueProcessors: [xml2js.processors.parseNumbers, xml2js.processors.parseBooleans]
                }
            });
            return parseRes.root;
        } catch (e) {
            throw new Error("Ошибка рабора XML с параметрами сообщения/ответа: " + e);
        }
    } else {
        throw new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult);
    }
};

//Сборка параметров сообщения/ответа (JSON > XML)
const buildOptionsXML = prms => {
    //Проверяем структуру переданных параметров
    let sCheckResult = validateObject(prms, prmsUtilsSchema.buildOptionsXML, "Параметры функции сборки XML параметров сообщения/ответа");
    //Если структура объекта в норме
    if (!sCheckResult) {
        try {
            let builder = new xml2js.Builder();
            return builder.buildObject({ root: prms.options });
        } catch (e) {
            throw new Error("Ошибка сборки XML с параметрами сообщения/ответа: " + e);
        }
    } else {
        throw new ServerError(SERR_OBJECT_BAD_INTERFACE, sCheckResult);
    }
};

//Получение текущего времени в строковом формате
const getNowString = () => {
    //Создадим объект даты
    const dNow = new Date();
    //Возьмём его строковое представление
    const sNow = dNow.toLocaleString();
    //Вернем результат
    return sNow;
};

//Глубокое слияние объектов
const deepMerge = (...sources) => {
    //Проверка на простой объект (не имеет специального поведения или прототипа)
    const isPlainObject = value => Object.prototype.toString.call(value) === "[object Object]";
    //Клоникарование значения
    const cloneValue = value => deepClone(value);
    //Буфер для результата
    const target = {};
    //Обходим входные параметры по очереди
    for (const source of sources) {
        //Пропускаем непростые объекты - сливать можем только простые
        if (!isPlainObject(source)) continue;
        //Обходим пары "ключ-значение"
        for (const [key, value] of Object.entries(source)) {
            //Если значение ключа - объект
            if (isPlainObject(value)) {
                //Сливаем рекурсивно
                if (!isPlainObject(target[key])) target[key] = {};
                target[key] = deepMerge(target[key], value);
            } else {
                //Примитивы - просто клонируем
                target[key] = cloneValue(value);
            }
        }
    }
    //Возвращаем собранный буфер, содержащий все слитые воедино объекты-параметры
    return target;
};

//Глубокое копирование объекта
const deepCopyObject = obj => JSON.parse(JSON.stringify(obj));

//Проверка на undefined
const isUndefined = value => value === undefined;

//Извлечение сообщения обработчика БД
const extractQueuePrcExecMsg = prcRes => {
    //Нет результата обработчика - сообщения нет
    if (!prcRes) return null;
    //Вернём сообщение обработчика (null, если не передано)
    return isUndefined(prcRes.sMsg) ? null : prcRes.sMsg;
};

//Считывание параметров подключения для сервиса обмена (при service === "" считывание подключения "По умолчанию", settingsArray - массив объектов [{sService: "", ...},...])
const getConnectionSettings = (service, settingsArray) => {
    //Считываем параметры и возвращаем
    return settingsArray.find(connection => {
        return connection.sService === service;
    });
};

//Считывание параметров подключения к Kafka для сервиса обмена (kafka - массив объектов [{sService: "", ...},...])
const getKafkaConnectionSettings = (service, kafka) => {
    //Считываем подключение с указанным сервисом обмена
    let kafkaConnection = getConnectionSettings(service, kafka);
    //Если нет подключения с указанным сервисом обмена
    if (!kafkaConnection) {
        //Считываем "По умолчанию"
        kafkaConnection = getConnectionSettings("", kafka);
    }
    //Вернем результат
    return kafkaConnection;
};

//Считывание параметров подключения к MQTT для сервиса обмена (mqtt - массив объектов [{sService: "", ...},...])
const getMQTTConnectionSettings = (service, mqtt) => {
    //Считываем подключение с указанным сервисом обмена
    let mqttConnection = getConnectionSettings(service, mqtt);
    //Если нет подключения с указанным сервисом обмена
    if (!mqttConnection) {
        //Считываем "По умолчанию"
        mqttConnection = getConnectionSettings("", mqtt);
    }
    //Вернем результат
    return mqttConnection;
};

//Получение брокера Kafka по адресу сервиса обмена (прим. kafka://server.ru -> server.ru, https://server.ru => undefined)
const getKafkaBroker = sURL => {
    //Если протокол URL - Kafka
    if (getURLProtocol(sURL) === SPROTOCOL_KAFKA) {
        //Возвращаем брокера
        return sURL.slice(8);
    }
    //Возвращаем undefined
    return;
};

//Получение авторизации для Kafka
const getKafkaAuth = (sUser, sPass, kafka) => {
    //Если аутентификация по SSL-сертификату
    if (kafka.bAuthSSL) {
        //Возвращаем авторизацию в формате SSL
        return {
            ssl: {
                rejectUnauthorized: kafka.ssl.bRejectUnauthorized,
                ca: kafka.ssl.sPathCa ? [fs.readFileSync(kafka.ssl.sPathCa, "utf-8")] : [],
                key: kafka.ssl.sPathKey ? fs.readFileSync(kafka.ssl.sPathKey, "utf-8") : "",
                cert: kafka.ssl.sPathCert ? fs.readFileSync(kafka.ssl.sPathCert, "utf-8") : ""
            }
        };
    }
    //Возвращаем авторизацию по пользователю, если необходимо
    return sUser ? { ssl: true, sasl: { mechanism: "plain", username: sUser, password: sPass } } : null;
};

//Получение протокола адреса (прим. mqtt://server.ru -> mqtt, https://server.ru => https, ...)
const getURLProtocol = sURL => {
    //Если начинается с "/" - HTTP, иначе получаем из URL
    return sURL.substring(0, 1) === "/" ? SPROTOCOL_HTTP : new URL(sURL).protocol.slice(0, -1);
};

//Обёртывание промиса в таймаут исполнения
const wrapPromiseTimeout = (timeout, executor) => {
    //Проверяем входные параметры - должен быть указан ненулевой таймаут и функция-исполнитель промиса
    if (!timeout || typeof executor !== "function") {
        //Разрешаем сразу, не ожидая
        return executor ? executor() : Promise.resolve();
    }
    //Параметры прошли проверку, создаем экхемпляр контроллера прерывания асинхронного процесса
    const controller = new AbortController();
    //Подготовим объект для выбрасывания ошибки
    const sMessage = `Истёк интервал ожидания (${timeout} мс) завершения асинхронного процесса.`;
    const timeoutError = new Error(sMessage);
    timeoutError.error = sMessage;
    //Буфер для идентификатора таймера
    let timeoutPid;
    //Создаём промис с взведенным таумером на у казанный таймаут, промис будет "отклонён" по истечении таймера, но перед этим контроллер прерывания выдаст "сигнал" на отмену осинхронного процесса
    const timeoutPromise = new Promise((_, reject) => {
        timeoutPid = setTimeout(() => {
            controller.abort(timeoutError);
            reject(timeoutError);
        }, timeout);
    });
    //Создаём промис с функцией-исполнителем асинхронного процесса
    const taskPromise = Promise.resolve(executor(controller.signal));
    //Запускаем оба промиса в режиме "гонка" - кто "быстрее" исполнится
    return Promise.race([taskPromise, timeoutPromise]).finally(() => {
        //В любом случае не забываем зачистить взведённый таймер
        if (timeoutPid) clearTimeout(timeoutPid);
    });
};

//-----------------
// Интерфейс модуля
//-----------------

exports.validateObject = validateObject;
exports.makeModuleFullPath = makeModuleFullPath;
exports.makeErrorText = makeErrorText;
exports.getAppSrvModuleName = getAppSrvModuleName;
exports.getAppSrvFunctionName = getAppSrvFunctionName;
exports.getAppSrvFunction = getAppSrvFunction;
exports.sendMail = sendMail;
exports.buildURL = buildURL;
exports.getIPs = getIPs;
exports.parseXML = parseXML;
exports.parseOptionsXML = parseOptionsXML;
exports.buildOptionsXML = buildOptionsXML;
exports.getNowString = getNowString;
exports.deepMerge = deepMerge;
exports.deepClone = deepClone;
exports.deepCopyObject = deepCopyObject;
exports.isUndefined = isUndefined;
exports.extractQueuePrcExecMsg = extractQueuePrcExecMsg;
exports.getKafkaConnectionSettings = getKafkaConnectionSettings;
exports.getMQTTConnectionSettings = getMQTTConnectionSettings;
exports.getKafkaBroker = getKafkaBroker;
exports.getKafkaAuth = getKafkaAuth;
exports.getURLProtocol = getURLProtocol;
exports.wrapPromiseTimeout = wrapPromiseTimeout;
