/*
 * @package    ECLCDEK shipping plugin СДЭК to RadicalMart
 * @subpackage    ECLCDEK
 * @version    1.0.1
 * @author Econsult Lab.
 * @copyright  Copyright (c) 2025 Econsult Lab. All rights reserved.
 * @license    GNU/GPL license: https://www.gnu.org/copyleft/gpl.html
 * @link       https://econsultlab.ru
 */

/**
 * Базовый класс
 *
 * @since 1.0.0
 */
class Core {
    /**
     * Флаг режима отладки
     * @type {boolean}
     * @since 1.0.0
     */
    #debug_mode;

    /**
     * URL для отправки запросов
     * @type {string}
     * @since 1.0.0
     */
    #url;

    /**
     * Параметры инициализации
     * @type {object}
     * @since 1.0.0
     */
    #params;

    /**
     * Флаг блокировки запросов на время выполнения текущего
     * @type {boolean}
     * @since 1.0.0
     */
    #ajaxRequestLock;

    /**
     * Имя класса (для вывода отладочной информации)
     * @type {string}
     * @since 1.0.0
     */
    className;

    /**
     * Элемент лоадера, запускаемого при выполнении запроса
     * @type {HTMLElement}
     * @since 1.0.0
     */
    ajaxLoader;

    /**
     * Callback удачного выполнения запроса
     * @type {function}
     * @since 1.0.0
     */
    successCallBack;

    /**
     * Callback ошибки выполнения запроса
     * @type {function}
     * @since 1.0.0
     */
    errorCallBack;

    /**
     * Конструктор класса Core
     *
     * @param {object} params
     *
     * @since 1.0.0
     */
    constructor(params) {
        this.#url = "/index.php?option=com_ajax&group=radicalmart_shipping&plugin=eclcdek&format=json";
        let _defaultParams = {
            debug: true,
            ajaxLoader: null,
            disabled_class: "uk-disabled",
        }
        this.#params = params ?? _defaultParams;
        this.ajaxLoader = params.ajaxLoader;
        this.hideLoader();

        this.className = "Core";
        this.#ajaxRequestLock = false;
        this.successCallBack = null;
        this.errorCallBack = null;
        this.#debug_mode = params.debug;
        this.debug('constructor', 'this.#params', this.#params, 'debug');
    }

    /**
     * Отображает лоадер. Может быть переопределен.
     *
     * @since 1.0.0
     */
    showLoader() {
        if (this.ajaxLoader && typeof this.ajaxLoader.style !== "undefined") {
            this.ajaxLoader.style.display = "";
        }
    }

    /**
     * Скрывает лоадер. Может быть переопределен.
     *
     * @since 1.0.0
     */
    hideLoader() {
        if (this.ajaxLoader && typeof this.ajaxLoader.style !== "undefined") {
            this.ajaxLoader.style.display = "none";
        }
    }

    /**
     * Получает значение выбранной опции
     *
     * @param {HTMLElement} select      Элемент select
     *
     * @returns {*}
     *
     * @since 1.0.0
     */
    getSelectedValue(select) {
        if (select && typeof select.options !== "undefined" && typeof select.options[select.selectedIndex] !== "undefined") {
            return select.options[select.selectedIndex].value;
        }
        return "";
    }

    /**
     * Получает текст выбранной опции
     *
     * @param {HTMLElement} select      Элемент select
     *
     * @returns {string}
     *
     * @since 1.0.0
     */
    getSelectedText(select) {
        if (select && typeof select.options !== "undefined" && typeof select.options[select.selectedIndex] !== "undefined") {
            return select.options[select.selectedIndex].text === Joomla.Text._("JSELECT") ? '' : select.options[select.selectedIndex].text;
        }
        return "";
    }

    /**
     * Получить значение опции по тексту для select
     * @param {HTMLElement} fld Элемент select
     * @param {string} text Значение текста
     *
     * @returns {*|string}
     *
     * @since 1.0.0
     */
    getSelectValueByText(fld, text) {
        this.debug("getSelectValueByText", "fld", fld);
        this.debug("getSelectValueByText", "text", text);
        // Очищаем текст
        text = this.clearText(text);
        this.debug("getSelectValueByText", "cleared text", text);

        let _ech = ECLChoices.Create('#' + fld.id)//new ECLChoices('#'+fld.id);

        if (this.checkUsedChoices(_ech)) {
            let _option = _ech.findOptionByText(text, true);
            let _val = "";
            if (null === _option) {
                _option = _ech.findOptionByText(text, false);
                if (null === _option) {
                    this.debug("getSelectValueByText", "не найдено значение text", text);
                    return '';
                } else {
                    _val = _option.getAttribute("data-value");
                    this.debug("getSelectValueByText", "value", _val);
                }
            } else {
                _val = _option.getAttribute("data-value");
                this.debug("getSelectValueByText", "По не точному соответствию value", _val);
            }
            return _val;
        }


        if (fld && typeof fld.options !== "undefined") {
            let _options = fld.options;
            for (var i = 0; i < _options.length; i++) {
                if (_options[i].textContent === text) {
                    this.debug("getSelectValueByText", "value", _options[i].value);
                    return _options[i].value;
                } else if (text.indexOf(_options[i].textContent) >= 0) {
                    this.debug("getSelectValueByText", "По не точному соответствию value", _options[i].value);
                    return _options[i].value;
                }
            }
            this.debug("getSelectValueByText", "не найдено значение text", text);
        }
        return "";
    }

    /**
     * Получить текст опции по значению для select
     * @param {HTMLElement} fld Элемент select
     * @param {*|string} value Значение value
     *
     * @returns {*|string}
     *
     * @since 1.0.0
     */
    getSelectTextByValue(fld, value) {
        this.debug("getSelectTextByValue", "fld", fld);
        this.debug("getSelectTextByValue", "value", value);
        if (fld && typeof fld.options !== "undefined") {
            let _options = fld.options;
            for (var i = 0; i < _options.length; i++) {
                if (_options[i].value === value) {
                    this.debug("getSelectTextByValue", "text", _options[i].textContent);
                    return _options[i].textContent;
                }
            }

            this.debug("getSelectTextByValue", "не найдено значение value", value);
        }
        return "";
    }

    /**
     * Устанавливает выбранной опцию по параметру значения или текста
     *
     * @param {HTMLElement} fld Элемент select
     * @param {string} key Ключ значения (text или value)
     * @param {*|string} value  Значение ключа
     *
     * @since 1.0.0
     */
    setOptionSelected(fld, key, value) {
        this.debug("setOptionSelected", "fld", fld);
        this.debug("setOptionSelected", "key", key);
        this.debug("setOptionSelected", "value", value);
        let _options = fld.options;
        if (key !== 'value' && key !== 'text') {
            key = 'value'
        } else if (key === 'text') {
            key = 'textContent';
        }

        let _ech = ECLChoices.Create('#' + fld.id);//new ECLChoices('#'+fld.id);
        if (this.checkUsedChoices(_ech)) {
            if (!_ech.selectByValue(value)) {
                if (key === 'textContent') {
                    if (!_ech.selectByText(this.clearText(value), true)) {
                        this.debug("setOptionSelected", "не найдено '" + key + "' со значением value:", value);
                        return;
                    } else {
                        this.debug("setOptionSelected", "Выбрано", value);
                    }
                } else {
                    this.debug("setOptionSelected", "Ошибочное значение ключа", key);
                    return;
                }
            } else {
                this.debug("setOptionSelected", "Выбрано", value);
            }
            return;
        }

        for (var i = 0; i < _options.length; i++) {
            if (_options[i][key] == value) {
                _options[i].selected = true;
                this.debug("setOptionSelected", "Выбрано", _options[i][key]);
                return;
            } else if (key === 'textContent' && this.clearText(value).indexOf(_options[i][key]) >= 0) {
                _options[i].selected = true;
                this.debug("setOptionSelected", "Выбрано по неточному совпадению", _options[i][key]);
                return;
            }
        }

        this.debug("setOptionSelected", "не найдено '" + key + "' со значением value:", value);
    }

    /**
     * Заполняет опции элемента select значениями
     *
     * @param {HTMLElement} fld Элемент select
     * @param {object}  valList Список значений для заполнения опций
     * @param {string} value_key Наименование ключа значения опции
     * @param {string} text_key Наименование ключа текста опции
     * @param {*|string} selectedValue Значение для выбранной опции
     *
     * @since 1.0.0
     */
    updateSelectOptions(fld, valList, value_key, text_key, selectedValue = null) {
        this.debug('updateSelectList', 'started', '...');
        this.debug('updateSelectList', 'fld', fld);
        this.debug('updateSelectList', 'valList', valList);
        this.debug('updateSelectList', 'value_key', value_key);
        this.debug('updateSelectList', 'text_key', text_key);
        this.debug('updateSelectList', 'selectedValue', selectedValue);
        let _this = this;
        if (valList) {
            // удаляем все опции, кроме "Выбрать"
            this.clearSelectOptions(fld, false);
            let _ech = ECLChoices.Create('#' + fld.id);//new ECLChoices('#'+fld.id);
            // Заполняем опции массивом значений
            let _ech_options = [];
            Object.entries(valList).forEach(function ([key, val]) {
                let opt = document.createElement('option');
                let text = document.createTextNode(val[value_key]);
                opt.setAttribute('value', val[text_key]);
                opt.appendChild(text);
                fld.appendChild(opt);

                _ech_options.push({
                    value: val[text_key],
                    label: val[value_key],
                    selected: false,
                    disabled: false
                });
            });
            /** Изменен механизм обновления списка значений
             *  @since 1.0.3
             */
            if (_this.checkUsedChoices(_ech)) {
                this.debug('updateSelectList', '_ech_options', _ech_options);
                _ech.updateChoices(_ech_options, 'value', 'label', false);
            }

            // выбираем опцию, если передано ее значение
            if (selectedValue !== null) {
                this.setOptionSelected(fld, 'value', selectedValue);
            }

        } else {
            // удаляем все опции, кроме "Выбрать"
            this.clearSelectOptions(fld, false);
        }
        this.debug('updateSelectList', 'done', '...');
    }

    /**
     * Очищает все опции элемента select
     *
     * @param {HTMLElement} fld Элемент select
     * @param {boolean} with_default Флаг очистки всех опций (true) или оставить опцию по-умолчанию (false)
     *
     * @since 1.0.0
     */
    clearSelectOptions(fld, with_default) {
        this.debug('clearSelectOptions', 'start', '...');
        if (fld instanceof HTMLElement) {
            if (typeof with_default === "undefined")
                with_default = false;

            let _options = fld.querySelectorAll("option");
            if (_options) {
                // удаляем все опции, кроме "Выбрать"
                _options.forEach(function (el) {
                    if (with_default || el.value !== "") {
                        el.remove();
                    }
                });
            }
        }

        let _ech = ECLChoices.Create('#' + fld.id)//new ECLChoices('#'+fld.id);
        if (this.checkUsedChoices(_ech)) {
            _ech.removeOptions("");
        }

        this.debug('clearSelectOptions', 'done', '...');
    }


    /**
     * Управляет доступностью кнопки
     *
     * @param {HTMLElement} btn Элемент кнопки
     * @param {boolean} disabled    Недоступна true, доступна - false
     *
     * @since 1.0.0
     */
    setDisabledBtn(btn, disabled = true) {
        if (typeof btn !== "undefined" && btn instanceof HTMLElement) {
            let _classes = btn.classList;
            if (_classes) {
                if (disabled) {
                    if (!_classes.contains(this.#params.disabled_class)) {
                        _classes.add(this.#params.disabled_class);
                    }
                } else {
                    if (_classes.contains(this.#params.disabled_class)) {
                        _classes.remove(this.#params.disabled_class);
                    }
                }
            }
        }
    }

    /**
     * Обновляет стоимость заказа
     *
     * @since 1.0.0
     */
    updateCheckoutPrice() {
        if (typeof window.RadicalMartCheckout !== "undefined") {
            let _t = window.RadicalMartCheckout();
            _t.updateDisplayData();
        }
    }

    /**
     * Проверяет значение клиента и если оно не корректно, подставляет по умолчанию
     *
     * @param {string} client   Клиент, откуда производится вызов (сайт, админка)
     *
     * @returns {string} (site или admin)
     *
     * @since 1.0.0
     */
    validateClient(client) {
        if (client !== 'site' && client !== 'admin') {
            return 'site';
        }
        return 'admin';
    }


    /**
     * Отправляет запрос
     *
     * @param {string} action Тип запроса
     * @param {object} data Данные запроса
     *
     * @returns {Promise<unknown>}
     *
     * @since 1.0.0
     */
    sendAjax(action, data) {
        let _this = this;
        _this.debug('sendAjax', 'action', action, 'debug');
        _this.debug('sendAjax', 'data', data, 'debug');
        return new Promise((success, error) => {
            if (action === null) {
                return error({
                    message: 'Action is empty'
                });
            }
            this.#sendRequest(action, data).then(s => success(s)).catch(e => error(e));
        });
    }

    /**
     * Выполнение запроса по AJAX
     *
     * @param {string} action Тип отправляемого запроса
     * @param {object} data Данные, передаваемые в запросе
     *
     * @returns {Promise<unknown>}
     *
     * @since 1.0.0
     */

    #sendRequest(action, data) {
        let _this = this;
        this.debug('#sendRequest', 'action', action, 'debug');
        this.debug('#sendRequest', 'data', data, 'debug');
        let jsonData = JSON.stringify({request_data: data});
        return new Promise((success, error) => {
            Joomla.request({
                url: _this.#url + "&action=" + action,
                method: 'POST',
                data: jsonData,
                headers: {
                    'Cache-Control': 'no-cache',
                    'Content-Type': 'application/json'
                },
                onSuccess: response => {
                    try {
                        this.debug('#sendRequest onSuccess', 'response', response, 'debug');
                        response = JSON.parse(response);
                        if (response.success) {
                            return success(response.data);
                        } else {
                            return error({
                                message: response.message
                            });
                        }
                    } catch (je) {
                        return error(je);
                    }
                },
                onError: e => {
                    let errorObject = this.#parseJoomlaRequestError(e);
                    if (errorObject) {
                        return error(errorObject);
                    }
                },
                onComplete: function (xhr) {

                }
            });
        });
    }

    /**
     * Обрабатывает ошибку выполнения запроса
     *
     * @param {object} error    Ошибка выполнения запроса
     *
     * @returns {*|{message: string}|boolean}
     *
     * @since 1.0.0
     */
    #parseJoomlaRequestError(error) {
        let _this = this;
        if (error instanceof XMLHttpRequest) {
            if (error.status === 0) {
                _this.debug('#parseJoomlaRequestError', 'aborted error', error, 'error');
                return false;
            }
            let message;
            if (error.response) {
                let responseElement = document.createElement('div');
                responseElement.innerHTML = error.response;
                if (responseElement) {
                    responseElement = responseElement.querySelector('title');
                    if (responseElement) {
                        message = responseElement.textContent;
                    }
                }
            } else {
                message = error.status + ' ' + error.statusText;
            }
            return {
                'message': message
            };
        }
        if (typeof error === 'object') {
            if (error.message === 'Request aborted' || error.message === null || error.message === '' || error.message === 0 || error.message === '0') {
                _this.debug('#parseJoomlaRequestError', 'aborted error', error, 'error');
                return false;
            }
        } else {
            return error;
        }
    }

    /**
     * Вывод отладочной информации в консоль
     *
     * @param {string} source   Метод источник записи
     * @param {string} name Имя переменной
     * @param value Переменная
     * @param {string} type   Тип вывода debug, error, log, info
     *
     * @since 1.0.0
     */
    debug(source, name, value, type = "debug") {
        if (this.#debug_mode) {
            switch (type) {
                case 'log':
                    console.log('[' + this.className + '] ' + source + ' ' + name, value);
                    break;
                case 'error':
                    console.error('[' + this.className + '] ' + source + ' ' + name, value);
                    break;
                case 'info':
                    console.info('[' + this.className + '] ' + source + ' ' + name, value);
                    break;
                default:
                    console.debug('[' + this.className + '] ' + source + ' ' + name, value);
                    break;
            }
        }
    }

    /**
     * Очищает текст от названий типов населенных пунктов
     *
     * @param {string} text  Исходный текст
     * @returns {string}
     * @public
     * @since 1.0.3
     */
    clearText(text) {
        let _r = ["поселок", "посёлок", "село", "хутор", "станица", "пгт"];
        let result = text;
        _r.forEach(substr => {
            result = result.replaceAll(substr, '');
        });
        return result.trim();
    }

    /**
     * Проверяет используется или нет для поля select j00mla-field-fancy-select
     * @param select_field
     * @returns {boolean}
     * @public
     * @since 1.0.3
     */
    checkUsedChoices(select_field) {
        return select_field !== null && select_field instanceof ECLChoices
    }
}