/*
    Парус 8 - Панели мониторинга
    Ядро: Клиент для взаимодействия с сервером приложений "Парус 8 Онлайн"
*/

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

import { XMLBuilder } from "fast-xml-parser"; //Конвертация XML в JSON и JSON в XML
import dayjs from "dayjs"; //Работа с датами
import config from "../../app.config"; //Настройки приложения
import { xml2JSON } from "./utils"; //Вспомогательные функции

//---------
//Константы
//---------

//Коды функций сервера
const SRV_FN_CODE_EXEC_STORED = "EXEC_STORED"; //Код функции сервера "Запуск хранимой процедуры"

//Типы данных сервера
const SERV_DATA_TYPE_STR = "STR"; //Тип данных "строка"
const SERV_DATA_TYPE_NUMB = "NUMB"; //Тип данных "число"
const SERV_DATA_TYPE_DATE = "DATE"; //Тип данных "дата"
const SERV_DATA_TYPE_CLOB = "CLOB"; //Тип данных "текст"

//Состояния ответов сервера
const RESP_STATUS_OK = "OK"; //Успех
const RESP_STATUS_ERR = "ERR"; //Ошибка

//Типовые ошибки клиента
const ERR_APPSERVER = "Ошибка сервера приложений"; //Общая ошибка клиента
const ERR_UNEXPECTED = "Неожиданный ответ сервера"; //Неожиданный ответ сервера
const ERR_NETWORK = "Ошибка соединения с сервером"; //Ошибка сети
const ERR_UNAUTH = "Сеанс завершен. Пройдите аутентификацию повторно."; //Ошибка аутентификации
const ERR_ABORTED = "Запрос прерван принудительно";

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

//Определение типа данных значения аргумента
const getServerDataType = value => {
    let res = SERV_DATA_TYPE_STR;
    if (typeof value == "number") res = SERV_DATA_TYPE_NUMB;
    if (value instanceof Date) res = SERV_DATA_TYPE_DATE;
    return res;
};

//Формирование стандартного ответа - ошибка
const makeRespErr = ({ message }) => ({ SSTATUS: RESP_STATUS_ERR, SMESSAGE: message });

//Разбор XML
const parseXML = async (xmlDoc, isArray, transformTagName, tagValueProcessor, attributeValueProcessor) =>
    await xml2JSON({ xmlDoc, isArray, transformTagName, tagValueProcessor, attributeValueProcessor, useDefaultPatterns: true });

//Формирование XML
const buildXML = jsonObj => {
    return new Promise((resolve, reject) => {
        try {
            const builder = new XMLBuilder({ ignoreAttributes: false, oneListGroup: true });
            resolve(builder.build(jsonObj));
        } catch (e) {
            reject(e);
        }
    });
};

//Проверка ответа на наличие ошибки
const isRespErr = resp => resp && resp?.SSTATUS && resp?.SSTATUS === RESP_STATUS_ERR;

//Извлечение ошибки из ответа
const getRespErrMessage = resp => (isRespErr(resp) && resp.SMESSAGE ? resp.SMESSAGE : "");

//Извлечение полезного содержимого из ответа
const getRespPayload = resp => (resp && resp.XPAYLOAD ? resp.XPAYLOAD : null);

//Исполнение действия на сервере
const executeAction = async ({
    serverURL,
    action,
    payload = {},
    isArray,
    transformTagName,
    tagValueProcessor,
    attributeValueProcessor,
    signal = null
} = {}) => {
    console.log(`EXECUTING ${action ? action : ""} ON ${serverURL} WITH PAYLOAD:`);
    console.log(payload ? payload : "NO PAYLOAD");
    let response = null;
    let responseJSON = null;
    try {
        //Сформируем типовой запрос
        const rqBody = {
            XREQUEST: { SACTION: action, XPAYLOAD: payload }
        };
        //Выполняем запрос
        response = await fetch(serverURL, {
            method: "POST",
            body: await buildXML(rqBody),
            headers: {
                "content-type": "application/xml"
            },
            ...(signal ? { signal } : {})
        });
    } catch (e) {
        //Прервано принудительно
        if (signal?.aborted === true) throw new Error(ERR_ABORTED);
        //Сетевая ошибка
        else throw new Error(`${ERR_NETWORK}: ${e.message || "неопределённая ошибка"}`);
    }
    //Проверим на наличие ошибок HTTP - если есть вернём их
    if (!response.ok) throw new Error(`${ERR_APPSERVER}: ${response.statusText}`);
    //Проверим на наличие редиректа к аутентификации (возникает, если сеанс окончен)
    if (response.redirected && response.url.toLowerCase().includes("auth?returnurl")) throw new Error(ERR_UNAUTH);
    //Ошибок нет - пробуем разобрать
    try {
        let responseText = await response.text();
        //console.log("SERVER RESPONSE TEXT:");
        //console.log(responseText);
        responseJSON = await parseXML(responseText, isArray, transformTagName, tagValueProcessor, attributeValueProcessor);
    } catch (e) {
        //Что-то пошло не так при парсинге
        throw new Error(ERR_UNEXPECTED);
    }
    //Разобрали, проверяем структуру ответа на обязательные атрибуты
    if (
        !responseJSON?.XRESPOND ||
        !responseJSON?.XRESPOND?.SSTATUS ||
        ![RESP_STATUS_ERR, RESP_STATUS_OK].includes(responseJSON?.XRESPOND?.SSTATUS) ||
        (responseJSON?.XRESPOND?.SSTATUS === RESP_STATUS_OK && responseJSON?.XRESPOND?.XPAYLOAD == undefined) ||
        (responseJSON?.XRESPOND?.SSTATUS === RESP_STATUS_ERR && responseJSON?.XRESPOND?.SMESSAGE == undefined)
    )
        throw new Error(ERR_UNEXPECTED);
    //Всё хорошо - возвращаем (без корня, он не нужен)
    console.log("SERVER RESPONSE JSON:");
    console.log(JSON.parse(JSON.stringify(responseJSON.XRESPOND)));
    return responseJSON.XRESPOND;
};

//Запуск хранимой процедуры
const executeStored = async ({
    stored,
    args,
    respArg,
    isArray,
    tagValueProcessor,
    attributeValueProcessor,
    throwError = true,
    spreadOutArguments = false,
    signal = null
} = {}) => {
    let res = null;
    try {
        let serverArgs = [];
        if (args)
            for (const arg in args) {
                let typedArg = false;
                if (args[arg] && Object.hasOwn(args[arg], "VALUE") && Object.hasOwn(args[arg], "SDATA_TYPE") && args[arg]?.SDATA_TYPE)
                    typedArg = true;
                const dataType = typedArg ? args[arg].SDATA_TYPE : getServerDataType(args[arg]);
                let value = typedArg ? args[arg].VALUE : args[arg];
                if (dataType === SERV_DATA_TYPE_DATE) value = dayjs(value).format("YYYY-MM-DDTHH:mm:ss");
                serverArgs.push({ XARGUMENT: { SNAME: arg, VALUE: value, SDATA_TYPE: dataType } });
            }
        res = await executeAction({
            serverURL: `${config.SYSTEM.SERVER}${!config.SYSTEM.SERVER.endsWith("/") ? "/" : ""}Process`,
            action: SRV_FN_CODE_EXEC_STORED,
            payload: { SSTORED: stored, XARGUMENTS: serverArgs, SRESP_ARG: respArg },
            isArray,
            tagValueProcessor,
            attributeValueProcessor,
            signal
        });
        if (spreadOutArguments === true && Array.isArray(res?.XPAYLOAD?.XOUT_ARGUMENTS)) {
            let spreadArgs = {};
            for (let arg of res.XPAYLOAD.XOUT_ARGUMENTS) spreadArgs[arg.SNAME] = arg.VALUE;
            delete res.XPAYLOAD.XOUT_ARGUMENTS;
            res.XPAYLOAD = { ...res.XPAYLOAD, ...spreadArgs };
        }
    } catch (e) {
        if (throwError) throw e;
        else return makeRespErr({ message: e.message });
    }
    if (res.SSTATUS === RESP_STATUS_ERR && throwError === true) throw new Error(res.SMESSAGE);
    return res;
};

//Чтение конфигурации плагина
const getConfig = async ({ throwError = true } = {}) => {
    let res = null;
    try {
        res = await executeAction({
            serverURL: `${config.SYSTEM.SERVER}${!config.SYSTEM.SERVER.endsWith("/") ? "/" : ""}GetConfig`
        });
    } catch (e) {
        if (throwError) throw e;
        else return makeRespErr({ message: e.message });
    }
    if (res.SSTATUS === RESP_STATUS_ERR && throwError === true) throw new Error(res.SMESSAGE);
    return res;
};

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

export default {
    ERR_APPSERVER,
    ERR_UNEXPECTED,
    ERR_NETWORK,
    ERR_UNAUTH,
    ERR_ABORTED,
    SERV_DATA_TYPE_STR,
    SERV_DATA_TYPE_NUMB,
    SERV_DATA_TYPE_DATE,
    SERV_DATA_TYPE_CLOB,
    isRespErr,
    getRespErrMessage,
    getRespPayload,
    executeStored,
    getConfig
};
