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

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

const pg = require("pg"); //Работа с СУБД Postgre

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

const DT_VARCHAR = pg.types.builtins.VARCHAR; //Тип данных "Строка" БД
const DT_NUMBER = pg.types.builtins.NUMERIC; //Тип данных "Число" БД
const DT_DATE = pg.types.builtins.DATE; //Тип данных "Дата" БД
const DT_CLOB = pg.types.builtins.TEXT; //Тип данных "Текстовые данные" БД
const DT_BLOB = pg.types.builtins.BYTEA; //Тип данных "Двоичные данные" БД
const DT_CURSOR = pg.types.builtins.REFCURSOR; //Тип данных "Курсор" БД

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

//Установка парсера значений для типа данных NUMERIC БД PG
pg.types.setTypeParser(pg.types.builtins.NUMERIC, val => {
    return val ? Number(val) : val;
});

//Формирование имени хранимого объекта БД (с параметрами)
const makeStoredName = (sName, inPrms) => {
    let prms = "";
    let prm_numb = 0;
    for (i in inPrms) {
        prm_numb++;
        let prm_type = "";
        if (inPrms[i] != null && inPrms[i] != undefined)
            switch (typeof inPrms[i]) {
                case "string":
                    if (inPrms[i].length ? inPrms[i].length > 4000 : false) prm_type = "::text";
                    else prm_type = "::character varying";
                    break;
                case "number":
                    prm_type = "::numeric";
                    break;
                case "bigint":
                    prm_type = "::numeric";
                    break;
                case "boolean":
                    prm_type = "::boolean";
                    break;
                case "object":
                    if (inPrms[i] instanceof Date) prm_type = "::date";
                    else prm_type = "::bytea";
                    break;
                default:
                    break;
            }
        prms += `${prms ? ", " : ""}$${prm_numb}${prm_type}`;
    }
    return `${sName.replace(".", "$")}(${prms})`;
};

//Формирование параметров хранимого объекта БД
const makeStoredPrms = inPrms => {
    let prms = [];
    for (i in inPrms) prms.push(inPrms[i]);
    return prms;
};

//Исполнение хранимого объекта БД
const executeStored = async prms => {
    let client;
    let outResult = {};
    try {
        client = await prms.connection.connect();
        let outPrmName = "";
        let outPrmIsCursor = false;
        for (i in prms.outPrms) {
            outPrmName = i;
            outPrmIsCursor = prms.outPrms[i] == DT_CURSOR;
            break;
        }
        await client.query(`begin;`);
        let res = await client.query({
            text: `select ${makeStoredName(prms.sName, prms.inPrms)}${outPrmName ? ` as "${outPrmName}"` : ""};`,
            values: makeStoredPrms(prms.inPrms)
        });
        if (res)
            if (res.rows)
                if (res.rows[0]) {
                    const outBind = res.rows[0][outPrmName];
                    if (outPrmIsCursor) {
                        let rowsRes = [];
                        let doExit = false;
                        while (!doExit) {
                            doExit = true;
                            res = await client.query(`fetch next from "${outBind}";`);
                            if (res)
                                if (res.rows)
                                    if (res.rowCount > 0 && res.rows.length > 0) {
                                        doExit = false;
                                        res.rows.map(r => rowsRes.push(r));
                                    }
                        }
                        outResult[outPrmName] = rowsRes;
                    } else outResult[outPrmName] = outBind;
                }
        await client.query(`commit;`);
    } catch (e) {
        if (client) {
            try {
                await client.query(`rollback;`);
            } catch (err) {
                throw new Error(err.message);
            }
        }
        throw new Error(e.message);
    } finally {
        if (client) {
            try {
                await client.release();
            } catch (err) {
                throw new Error(err.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 !== "" ? prms.sServerName : null,
            SSESSION_APP_NAME: prms.sSessionAppName
        },
        outPrms: {
            SERR_TEXT: DT_VARCHAR
        }
    });
    //Если есть ошибки запуска
    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
        }
    });
    //Если рег. номер сервера интеграции потерялся
    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
        }
    });
};

//Подключение к БД
const connect = async prms => {
    try {
        //Создаем пул подключения
        let pool = new pg.Pool({
            connectionString: `postgres://${prms.sUser}:${prms.sPassword}@${prms.sConnectString}`,
            connectionTimeoutMillis: 600000,
            min: prms.nPoolMin ? prms.nPoolMin : 4,
            max: prms.nPoolMax ? prms.nPoolMax : 4
        });
        pool.on("acquire", async client => {
            //Устанавливаем схему
            await client.query(`select ALTER_SESSION_SET_SCHEMA($1);`, [prms.sSchema]);
            //Устанавливаем модуль сессии
            await client.query(`select PKG_EXS$UTL_MODULE_SET($1);`, [prms.exsSrv.sServerName]);
        });
        return pool;
    } catch (e) {
        throw new Error(e.message);
    }
};

//Отключение от БД
const disconnect = async prms => {
    try {
        await prms.connection.end();
    } 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
        }
    });
    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$2",
        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$2",
        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$2",
        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$2",
        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$2",
        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;
