/*
  Сервис интеграции ПП Парус 8 с WEB API
  Дополнительный модуль: работа с БД ПП Парус 8 (Oracle)
*/

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

const oracledb = require("oracledb"); //Работа с СУБД Oracle
const { makeErrorText } = require("../core/utils"); //Вспомогательные функции

//---------------------------
// Инициализация Thick-режима
//---------------------------

//Инициализируем Thick-режим до любых подключений к БД
try {
    if (typeof oracledb.initOracleClient === "function" && !(process.env.NODE_ORACLE_DB_THIN_MODE === "1")) {
        const libDir = process.env.ORACLE_CLIENT_LIB_DIR;
        if (libDir) {
            oracledb.initOracleClient({ libDir });
        } else {
            oracledb.initOracleClient();
        }
    }
} catch (e) {
    throw new Error(`Не удалось инициализировать Oracle Client (Thick-режим): ${makeErrorText(e)}`);
}

//--------------------------
// Глобальные идентификаторы
//--------------------------

const NPOOL_DRAIN_TIME = 30; //Таймаут ожидания завершения подключений при отключении пула от БД (сек)
const DT_VARCHAR = oracledb.DB_TYPE_VARCHAR; //Тип данных "Строка" БД
const DT_NUMBER = oracledb.DB_TYPE_NUMBER; //Тип данных "Число" БД
const DT_DATE = oracledb.DB_TYPE_DATE; //Тип данных "Дата" БД
const DT_CLOB = oracledb.DB_TYPE_CLOB; //Тип данных "Текстовые данные" БД
const DT_BLOB = oracledb.DB_TYPE_BLOB; //Тип данных "Двоичные данные" БД
const DT_CURSOR = oracledb.CURSOR; //Тип данных "Курсор" БД
const DT_VARCHAR_LENGTH = 32767; //Длина типа "Строка"

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

//Формирование имени хранимого объекта БД (с параметрами)
const makeStoredName = (sName, inPrms, outPrms, isFunction = false) => {
    let prms = "";
    let resultVar = "";
    for (i in inPrms) prms += `${prms ? ", " : ""}${i} => :${i}`;
    for (i in outPrms) {
        if (isFunction) resultVar = `:${i} := `;
        else prms += `${prms ? ", " : ""}${i} => :${i}`;
        break;
    }
    return `${resultVar ? resultVar : ""}${sName.replace(/\$[0-9]{1,9}$/, "").replace("$", ".")}(${prms})`;
};

//Формирование параметров хранимого объекта БД
const makeStoredPrms = (inPrms, outPrms) => {
    let prms = inPrms ? inPrms : {};
    for (i in outPrms) {
        prms[i] = {
            type: outPrms[i],
            dir: oracledb.BIND_OUT,
            ...(outPrms[i] === DT_VARCHAR ? { maxSize: DT_VARCHAR_LENGTH } : {})
        };
        break;
    }
    return prms;
};

//Исполнение хранимого объекта БД
const executeStored = async prms => {
    let pooledConnection;
    let outResult = {};
    try {
        pooledConnection = await prms.connection.getConnection();
        let outPrmName = "";
        let outPrmIsCursor = false;
        for (i in prms.outPrms) {
            outPrmName = i;
            outPrmIsCursor = prms.outPrms[i] == DT_CURSOR;
            break;
        }
        let res = await pooledConnection.execute(
            `begin ${makeStoredName(prms.sName, prms.inPrms, prms.outPrms, prms.isFunction)}; end;`,
            makeStoredPrms(prms.inPrms, prms.outPrms),
            {
                ...(outPrmIsCursor ? { outFormat: oracledb.OBJECT } : {}),
                ...{ autoCommit: true }
            }
        );
        if (res)
            if (res.outBinds) {
                const outBind = res.outBinds[outPrmName];
                if (outPrmIsCursor) {
                    const readCursorData = cursor => {
                        return new Promise((resolve, reject) => {
                            let queryStream = cursor.toQueryStream();
                            let rows = [];
                            queryStream.on("data", row => {
                                rows.push(row);
                            });
                            queryStream.on("error", err => {
                                queryStream.destroy();
                                reject(new Error(err.message));
                            });
                            queryStream.on("close", () => {
                                queryStream.destroy();
                                resolve(rows);
                            });
                        });
                    };
                    let rows = await readCursorData(outBind);
                    let rowsRes = [];
                    await Promise.all(
                        rows.map(async row => {
                            let rowRes = {};
                            for (i in row) {
                                let isLob = row[i] ? (row[i].type ? row[i].type == oracledb.BLOB || row[i].type == oracledb.CLOB : false) : false;
                                rowRes[i] = isLob ? await row[i].getData() : row[i];
                            }
                            rowsRes.push(rowRes);
                        })
                    );
                    outResult[outPrmName] = rowsRes;
                } else outResult[outPrmName] = outBind;
            }
    } catch (e) {
        throw new Error(e.message);
    } finally {
        if (pooledConnection) {
            try {
                await pooledConnection.close();
            } catch (e) {
                throw new Error(e.message);
            }
        }
    }
    if (outResult) return outResult;
};

//Проверка условий при запуске сервиса интеграции
const checkAppStart = async prms => {
    let res = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.UTL_APPSRV_START_CHECK",
        inPrms: {
            NCONTROL_VERSION: prms.bControlSystemVersion ? 1 : 0,
            SEXS_RELEASE_DATE: prms.sRelease,
            NWORKERS: prms.nWorkers,
            SEXSSRV: prms.sServerName,
            SSESSION_APP_NAME: prms.sSessionAppName
        },
        outPrms: {
            SERR_TEXT: DT_VARCHAR
        },
        isFunction: false
    });
    //Если есть ошибки запуска
    if (res.SERR_TEXT) {
        throw new Error(res.SERR_TEXT);
    }
};

//Инициализация сервера интеграции
const initServer = async prms => {
    let res = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.EXSSRV_INIT",
        inPrms: {
            SEXSSRV: prms.sServerName,
            SIP: prms.sServerIP,
            NWORKERS: prms.nMaxWorkers
        },
        outPrms: {
            NEXSSRV: DT_NUMBER
        },
        isFunction: false
    });
    //Если рег. номер сервера интеграции не определен
    if (!res.NEXSSRV) {
        throw new Error(`Ошибка считывания сервера интеграции с мнемокодом "${prms.sServerName}".`);
    }
    //Возвращаем рег. номер сервера интеграции
    return res.NEXSSRV;
};

//Очистка информации о сервере при закрытии сервиса
const clearServer = async prms => {
    await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.EXSSRV_CLEAR",
        inPrms: {
            NEXSSRV: prms.nExsSrv
        },
        isFunction: false
    });
};

//Подключение к БД
const connect = async prms => {
    let pool = null;
    try {
        //Создаем пул подключения
        pool = await oracledb.createPool({
            user: prms.sUser,
            password: prms.sPassword,
            connectString: prms.sConnectString,
            queueTimeout: 600000,
            poolMin: prms.nPoolMin ? prms.nPoolMin : 4,
            poolMax: prms.nPoolMax ? prms.nPoolMax : 4,
            poolIncrement: prms.nPoolIncrement ? prms.nPoolIncrement : 0,
            sessionCallback: (connection, requestedTag, callback) => {
                //Устанавливаем схему
                connection
                    .execute(`ALTER SESSION SET CURRENT_SCHEMA=${prms.sSchema}`)
                    .then(r => {
                        //Устанавливаем модуль сессии
                        connection.execute(`begin PKG_EXS.UTL_MODULE_SET(SEXSSRV => '${prms.exsSrv.sServerName}'); end;`);
                    })
                    .then(r => {
                        callback(null, connection);
                    })
                    .catch(e => {
                        callback(e, null);
                    });
            }
        });
        //Проверяем доступность пула
        let tstConn = await pool.getConnection();
        await tstConn.close();
        //Всё ок - возвращяем его
        return pool;
    } catch (e) {
        //Закрываем пул, если успели открыть
        if (pool) pool.close(NPOOL_DRAIN_TIME);
        //Возвращаем ошибку
        throw new Error(e.message);
    }
};

//Отключение от БД
const disconnect = async prms => {
    try {
        //Отключаем от базы данных
        await prms.connection.close(NPOOL_DRAIN_TIME);
        return;
    } catch (e) {
        throw new Error(e.message);
    }
};

//Получение списка сервисов
const getServices = async prms => {
    let servicesData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.SERVICES_GET",
        inPrms: {
            NEXSSRV: prms.nExsSrv
        },
        outPrms: {
            RCSERVICES: DT_CURSOR
        }
    });
    return servicesData.RCSERVICES;
};

//Получение списка функций сервиса
const getServiceFunctions = async prms => {
    let serviceFunctionsData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.SERVICEFNS_GET",
        inPrms: {
            NEXSSERVICE: prms.nServiceId
        },
        outPrms: {
            RCSERVICEFNS: DT_CURSOR
        }
    });
    return serviceFunctionsData.RCSERVICEFNS;
};

//Получение контекста сервиса
const getServiceContext = async prms => {
    let serviceContextData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.SERVICE_CTX_GET",
        inPrms: {
            NFLAG_SMART: 0,
            NEXSSERVICE: prms.nServiceId
        },
        outPrms: {
            RCSERVICE_CTX: DT_CURSOR
        }
    });
    return serviceContextData.RCSERVICE_CTX[0];
};

//Установка контекста сервиса
const setServiceContext = async prms => {
    await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.SERVICE_CTX_SET",
        inPrms: {
            NEXSSERVICE: prms.nServiceId,
            SCTX: prms.sCtx,
            DCTX_EXP: prms.dCtxExp
        }
    });
};

//Очистка контекста сервиса
const clearServiceContext = async prms => {
    await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.SERVICE_CTX_CLEAR",
        inPrms: { NEXSSERVICE: prms.nServiceId }
    });
};

//Проверка атуентифицированности сервиса
const isServiceAuth = async prms => {
    let serviceAuth = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.SERVICE_IS_AUTH",
        inPrms: { NEXSSERVICE: prms.nServiceId },
        outPrms: {
            RET: DT_NUMBER
        },
        isFunction: true
    });
    return serviceAuth.RET;
};

//Постановка в очередь задания на аутентификацию сервиса
const putServiceAuthInQueue = async prms => {
    await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.SERVICE_AUTH_PUT_INQUEUE_AT",
        inPrms: { NEXSSERVICE: prms.nServiceId, NFORCE: prms.nForce }
    });
};

//Получение информации о просроченных сообщениях обмена сервиса
const getServiceExpiredQueueInfo = async prms => {
    let serviceExpiredQueueInfoData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.SERVICE_QUEUE_EXPIRED_INFO_GET",
        inPrms: {
            NEXSSERVICE: prms.nServiceId
        },
        outPrms: {
            RCSERVICE_QUEUE_EXPIRED_INFO: DT_CURSOR
        }
    });
    return serviceExpiredQueueInfoData.RCSERVICE_QUEUE_EXPIRED_INFO[0];
};

//Запись в протокол работы
const log = async prms => {
    let logData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.LOG_PUT",
        inPrms: {
            NLOG_STATE: prms.nLogState,
            SMSG: prms.sMsg,
            NEXSSERVICE: prms.nServiceId,
            NEXSSERVICEFN: prms.nServiceFnId,
            NEXSQUEUE: prms.nQueueId,
            NEXSSRV: prms.nExsSrv
        },
        outPrms: { RCLOG: DT_CURSOR }
    });
    return logData.RCLOG[0];
};

//Считывание записи очереди обмена
const getQueue = async prms => {
    let queueData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_GET",
        inPrms: {
            NFLAG_SMART: 0,
            NEXSQUEUE: prms.nQueueId
        },
        outPrms: { RCQUEUE: DT_CURSOR }
    });
    return queueData.RCQUEUE[0];
};

//Помещение сообщения в очередь
const putQueue = async prms => {
    let queueData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_PUT",
        inPrms: {
            NEXSSERVICEFN: prms.nServiceFnId,
            BMSG: prms.blMsg,
            NEXSSRV: prms.nExsSrv,
            NEXSQUEUE: prms.nQueueId,
            NLNK_COMPANY: prms.nLnkCompanyId,
            NLNK_DOCUMENT: prms.nLnkDocumentId,
            SLNK_UNITCODE: prms.sLnkUnitcode,
            SOPTIONS: prms.sOptions
        },
        outPrms: { RCQUEUE: DT_CURSOR }
    });
    return queueData.RCQUEUE[0];
};

//Установка признака "В работе" в сообщении очереди
const setInProgress = async prms => {
    await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_IN_PROGRESS_SET",
        inPrms: {
            NFLAG_SMART: 1,
            NEXSQUEUE: prms.nQueueId,
            NIN_PROGRESS: prms.nInProgress
        }
    });
};

//Считывание очередной порции исходящих сообщений из очереди
const getQueueOutgoing = async prms => {
    let queueOutgoingData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_SRV_TYPE_SEND_GET",
        inPrms: {
            NPORTION_SIZE: prms.nPortionSize,
            NEXSSRV: prms.nExsSrv
        },
        outPrms: { RCQUEUES: DT_CURSOR }
    });
    return queueOutgoingData.RCQUEUES;
};

//Установка значения состояния в сообщении очереди
const setQueueState = async prms => {
    let queueStateData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_EXEC_STATE_SET",
        inPrms: {
            NEXSQUEUE: prms.nQueueId,
            NEXEC_STATE: prms.nExecState,
            SEXEC_MSG: prms.sExecMsg,
            NINC_EXEC_CNT: prms.nIncExecCnt,
            NRESET_DATA: prms.nResetData
        },
        outPrms: { RCQUEUE: DT_CURSOR }
    });
    return queueStateData.RCQUEUE[0];
};

//Считывание данных сообщения очереди
const getQueueMsg = async prms => {
    let queueMsgData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_MSG_GET",
        inPrms: {
            NEXSQUEUE: prms.nQueueId
        },
        outPrms: { RCQUEUE_MSG: DT_CURSOR }
    });
    return queueMsgData.RCQUEUE_MSG[0];
};

//Установка данных сообщения очереди
const setQueueMsg = async prms => {
    let queueMsgData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_MSG_SET",
        inPrms: {
            NEXSQUEUE: prms.nQueueId,
            BMSG: prms.blMsg
        },
        outPrms: { RCQUEUE: DT_CURSOR }
    });
    return queueMsgData.RCQUEUE[0];
};

//Установка параметров сообщения очереди
const setQueueOptions = async prms => {
    let queueOptionsData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_OPTIONS_SET",
        inPrms: {
            NEXSQUEUE: prms.nQueueId,
            SOPTIONS: prms.sOptions
        },
        outPrms: { RCQUEUE: DT_CURSOR }
    });
    return queueOptionsData.RCQUEUE[0];
};

//Считывание результата обработки сообщения очереди
const getQueueResp = async prms => {
    let queueRespData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_RESP_GET",
        inPrms: {
            NEXSQUEUE: prms.nQueueId
        },
        outPrms: { RCQUEUE_RESP: DT_CURSOR }
    });
    return queueRespData.RCQUEUE_RESP[0];
};

//Установка результата обработки сообщения очереди
const setQueueResp = async prms => {
    let queueRespData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_RESP_SET",
        inPrms: {
            NEXSQUEUE: prms.nQueueId,
            BRESP: prms.blResp,
            NIS_ORIGINAL: prms.nIsOriginal
        },
        outPrms: { RCQUEUE: DT_CURSOR }
    });
    return queueRespData.RCQUEUE[0];
};

//Установка параметров результата обработки сообщения очереди
const setQueueOptionsResp = async prms => {
    let queueOptionsRespData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_OPTIONS_RESP_SET",
        inPrms: {
            NEXSQUEUE: prms.nQueueId,
            SOPTIONS_RESP: prms.sOptionsResp
        },
        outPrms: { RCQUEUE: DT_CURSOR }
    });
    return queueOptionsRespData.RCQUEUE[0];
};

//Исполнение обработчика со стороны БД для сообщения очереди
const execQueuePrc = async prms => {
    let queuePrcData = await executeStored({
        connection: prms.connection,
        sName: "PKG_EXS.QUEUE_PRC",
        inPrms: {
            NEXSQUEUE: prms.nQueueId
        },
        outPrms: { RCRESULT: DT_CURSOR }
    });
    return queuePrcData.RCRESULT[0];
};

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

exports.DT_VARCHAR = DT_VARCHAR;
exports.DT_NUMBER = DT_NUMBER;
exports.DT_DATE = DT_DATE;
exports.DT_CLOB = DT_CLOB;
exports.DT_BLOB = DT_BLOB;
exports.DT_CURSOR = DT_CURSOR;
exports.executeStored = executeStored;
exports.checkAppStart = checkAppStart;
exports.initServer = initServer;
exports.clearServer = clearServer;
exports.connect = connect;
exports.disconnect = disconnect;
exports.getServices = getServices;
exports.getServiceFunctions = getServiceFunctions;
exports.getServiceContext = getServiceContext;
exports.setServiceContext = setServiceContext;
exports.clearServiceContext = clearServiceContext;
exports.isServiceAuth = isServiceAuth;
exports.putServiceAuthInQueue = putServiceAuthInQueue;
exports.getServiceExpiredQueueInfo = getServiceExpiredQueueInfo;
exports.log = log;
exports.getQueue = getQueue;
exports.putQueue = putQueue;
exports.setInProgress = setInProgress;
exports.getQueueOutgoing = getQueueOutgoing;
exports.setQueueState = setQueueState;
exports.getQueueMsg = getQueueMsg;
exports.setQueueMsg = setQueueMsg;
exports.setQueueOptions = setQueueOptions;
exports.getQueueResp = getQueueResp;
exports.setQueueResp = setQueueResp;
exports.setQueueOptionsResp = setQueueOptionsResp;
exports.execQueuePrc = execQueuePrc;
