/**
 * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact schukai GmbH.
 */

import {instanceSymbol} from "../../constants.mjs";
import {addAttributeToken} from "../../dom/attributes.mjs";
import {
    ATTRIBUTE_ERRORMESSAGE,
    ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import {
    assembleMethodSymbol,
    registerCustomElement,
} from "../../dom/customelement.mjs";

import {isArray, isFunction, isString, isIterable, isObject, isPrimitive} from "../../types/is.mjs";
import {fireCustomEvent} from "../../dom/events.mjs";
import {ButtonBar} from "./button-bar.mjs";
import {validateString} from "../../types/validate.mjs";
import {Pathfinder} from "../../data/pathfinder.mjs";
import {buildMap} from "../../data/buildmap.mjs";
import {ApiButtonStyleSheet} from "./stylesheet/api-button.mjs";
import {Formatter} from "../../text/formatter.mjs";
import {getGlobal} from "../../types/global.mjs";

import "./button.mjs";
import "./message-state-button.mjs";
import "./state-button.mjs";
import {MessageStateButton} from "./message-state-button.mjs";
import {StateButton} from "./state-button.mjs";

export {ApiBar};


/**
 * A ApiBar
 *
 * @fragments /fragments/components/form/api-bar/
 *
 * @example /examples/components/form/api-bar-simple
 *
 * @since 3.90.0
 * @copyright schukai GmbH
 * @summary A beautiful ApiBar that can make your life easier and also looks good.
 */
class ApiBar extends ButtonBar {
    /**
     * This method is called by the `instanceof` operator.
     * @returns {symbol}
     */
    static get [instanceSymbol]() {
        return Symbol.for("@schukai/monster/components/form/api-bar@@instance");
    }

    /**
     * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
     * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
     *
     * The individual configuration values can be found in the table.
     *
     * @property {object} mapping - The mapping object.
     * @property {string} mapping.selector - The selector to find the buttons in the response.
     * @property {string} mapping.labelSelector - The selector to find the label for the button.
     * @property {string} mapping.labelTemplate - The template to create the label for the button.
     * @property {string} mapping.apiTemplate - The key to find the api value in the response.
     * @property {string} mapping.urlTemplate - The key to find the url value in the response, if empty the api value is used.
     * @property {function} mapping.filter - The filter function to filter the buttons.
     * @property {string} url - The url to fetch the data.
     * @property {string} buttonTag - The tag name of the button
     * @property {object} api - The api options.
     * @property {object} api.fetch - The fetch options.
     * @property {string} api.body - The body template.
     * @property {object} callbacks - The callbacks object.
     * @property {function} callbacks.beforeApiCall - The beforeApiCall callback called before the api request is made.
     * @property {function} callbacks.failedApiCall - The failedApiCall callback called when the api request failed.
     * @property {function} callbacks.successfulApiCall - The successfulApiCall callback called when the api request was successful.
     * @property {object} fetch - The fetch options.
     * @property {string} fetch.redirect - The redirect option.
     * @property {string} fetch.method - The method option.
     * @property {string} fetch.mode - The mode option.
     * @property {string} fetch.credentials - The credentials option.
     * @property {object} fetch.headers - The headers option.
     * @property {string} fetch.headers.accept - The acceptance option.
     * @property {object} actions - The actions object.
     * @property {function} actions.execute - The execute action.
     * @property {object} data - The data object, this can be used to store some data and send it with the request.
     * @extends {ActionButton.defaults}
     */
    get defaults() {
        const opts = Object.assign({}, super.defaults, {
            mapping: {
                selector: "*",
                labelSelector: "",
                labelTemplate: "",
                apiTemplate: "",
                urlTemplate: "",
                filter: "",
            },
            api: {
                fetch: {
                    method: "POST",
                    redirect: "error",
                    mode: "same-origin",
                    credentials: "same-origin",
                    headers: {
                        accept: "application/json",
                    },
                },
                body: null,
            },
            url: "",
            buttonTag: "monster-message-state-button",
            callbacks: {
                beforeApiCal: null,
                failedApiCall: null,
                successfulApiCall: null,
            },
            fetch: {
                redirect: "error",
                method: "GET",
                mode: "same-origin",
                credentials: "same-origin",
                headers: {
                    accept: "application/json",
                },
            },
            actions: {
                execute: executeAPIButton
            },
            data: null
        });

        return opts;
    }

    /**
     *
     * @return {Promise}
     */
    fetch(url) {
        if (url instanceof URL) {
            url = url.toString();
        }

        if (url !== undefined) {
            url = validateString(url);
        }

        return fetchData.call(this, url).then((map) => {
            if (
                isObject(map) ||
                isArray(map) | (map instanceof Set) ||
                map instanceof Map
            ) {
                this.importButtons(map);
            }
        });
    }

    /**
     * Import buttons from a map.
     *
     * @param {array|object|Map|Set} data
     * @return {ApiButton}
     * @throws {Error} map is not iterable
     * @throws {Error} missing label configuration
     */
    importButtons(data) {

        const self = this;

        const currentButtons = self.querySelectorAll(`[${ATTRIBUTE_ROLE}="api-button"]`);
        for (const btnElement of currentButtons) {
            btnElement.remove();
        }

        const mappingOptions = this.getOption("mapping", {});
        const selector = mappingOptions?.["selector"];
        const labelSelector = mappingOptions?.["labelSelector"];
        const labelTemplate = mappingOptions?.["labelTemplate"];
        const apiTemplate = mappingOptions?.["apiTemplate"];
        let urlTemplate = mappingOptions?.["urlTemplate"];

        const filter = mappingOptions?.["filter"];

        let flag = false;
        let apiEqualUrl = false;
        if (labelTemplate === "") {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty label template");
            flag = true;
        }

        if (apiTemplate === "") {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "empty api template");
            flag = true;
        }

        if (urlTemplate === "") {
            urlTemplate = apiTemplate;
            apiEqualUrl = true;
        }

        if (flag === true) {
            throw new Error("missing label or api configuration, check the error attribute");
        }

        if (isPrimitive(labelSelector) && labelSelector !== "") {
            const finder = new Pathfinder(data);
            const label = finder.getVia(labelSelector);
            this.setOption("labels.button", label);
            this.value = label;
        }

        let labelMap;
        const urlMap = buildMap(data, selector, urlTemplate, apiTemplate, filter);
        if (apiEqualUrl === true) {
            labelMap = urlMap;
        } else {
            labelMap = buildMap(data, selector, labelTemplate, apiTemplate, filter);
        }

        const buttons = [];
        if (!isIterable(urlMap)) {
            throw new Error("map is not iterable");
        }

        const buttonTag = this.getOption("buttonTag");
        const executerCallback = this.getOption("actions.execute");

        for (const [iterKey] of urlMap) {
            const vmUrl = urlMap.get(iterKey);
            const vmLabel = labelMap.get(iterKey);

            const button = getGlobal().document.createElement(buttonTag);
            button.setAttribute(ATTRIBUTE_ROLE, `api-button`);
            button.setOption("labels.button", vmLabel);
            button.setOption("actions.click", (event) => {

                if (isFunction(executerCallback)) {

                    executerCallback.call(this, event, {
                        key: iterKey,
                        url: vmUrl,
                        label: vmLabel,
                        button: button,
                    })

                }
            });

            this.appendChild(button);
        }

        try {
            this.updateI18n();
        } catch (e) {
            addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
        }

        this.setOption("buttons", buttons);

        fireCustomEvent(this, "monster-button-set", {
            buttons: buttons,
        });

        return this;
    }

    /**
     *
     * @return {void}
     */
    [assembleMethodSymbol]() {
        super[assembleMethodSymbol]();
    }

    /**
     * @return {string}
     */
    static getTag() {
        return "monster-api-bar";
    }

    /**
     * @return {Array<CSSStyleSheet>}
     */
    static getCSSStyleSheet() {
        const styles = super.getCSSStyleSheet();
        styles.push(ApiButtonStyleSheet);
        return styles;
    }
}

/**
 *
 * @param {Event} event
 * @param {object} opts
 */
function executeAPIButton(event, opts) {
    const self = this;

    if (!isObject(opts)) {
        opts = {};
    }

    const button = opts?.["button"];
    const fetchOptions = self.getOption("api.fetch", {});

    const callback = self.getOption("callbacks.beforeApiCall");
    if (isFunction(callback)) {
        callback.call(self, fetchOptions);
    }

    const successfulApiCall = self.getOption("callbacks.successfulApiCall");
    const failedApiCall = self.getOption("callbacks.failedApiCall");

    let url = opts?.["url"];
    let label = opts?.["label"];
    let key = opts?.["key"];

    let body = self.getOption("api.body");

    if (isString(body)) {
        try {
            body = JSON.parse(body);
        } catch (e) {
            body = {};
        }
    }

    if (isObject(body)) {
        const bodyString = JSON.stringify(body);

        const obj = {
            url: url,
            label: label,
            key: key,
            data: self.getOption("data"),
        };

        fetchOptions.body = new Formatter(obj, {}).format(bodyString);
    }

    if (button instanceof HTMLElement) {
        button?.setState("activity");
    }

    fireCustomEvent(self, "monster-api-bar-click", {
        button,
    });

    const global = getGlobal();
    global
        .fetch(url, fetchOptions)
        .then((response) => {

            if (!response.ok) {
                if (button instanceof MessageStateButton || button instanceof StateButton) {
                    button.setState("failed", 4000);
                }
                return Promise.reject(response);
            }

            const contentType = response?.headers?.get("content-type");
            if (contentType && contentType.indexOf("application/json") !== -1) {
                return response
                    .text()
                    .then((text) => {
                        try {

                            const data = JSON.parse(text); // Try to parse the response as JSON

                            if (button instanceof MessageStateButton || button instanceof StateButton) {
                                button.setState("successful", 4000);
                            }

                            fireCustomEvent(self, "monster-api-bar-successful", {
                                button,
                                data,
                                response,
                                contentType: response.headers.get("Content-Type"),
                            });

                            if (isFunction(successfulApiCall)) {
                                successfulApiCall.call(self, data, response);
                            }

                        } catch (error) {
                            if (button instanceof HTMLElement) {
                                button.setState("failed", 4000);
                                button.setMessage(error.message).showMessage(2000);
                            }

                            fireCustomEvent(self, "monster-api-bar-failed", {
                                button,
                                error,
                                response,
                                contentType: response.headers.get("Content-Type"),
                            });
                        }
                    })
                    .catch((error) => {

                        button.setState("failed", 4000);

                        if (isFunction(failedApiCall)) {
                            failedApiCall.call(self, error, response);
                        } else if (button instanceof MessageStateButton || button instanceof StateButton) {
                            button.setMessage("request failed").showMessage(2000);
                        }

                        fireCustomEvent(self, "monster-api-bar-failed", {
                            button,
                            error,
                            response,
                            contentType: response.headers.get("Content-Type"),
                        });
                    });
            } else {
                return response
                    .blob()
                    .then((data) => {

                        if (button instanceof MessageStateButton || button instanceof StateButton) {
                            button.setState("successful", 4000);
                        }

                        fireCustomEvent(self, "monster-api-bar-successful", {
                            button,
                            data,
                            response,
                            contentType: response.headers.get("Content-Type"),
                        });

                        if (isFunction(successfulApiCall)) {
                            successfulApiCall.call(self, data, response);
                        }

                    })
                    .catch((error) => {

                        if (button instanceof MessageStateButton || button instanceof StateButton) {
                            button.setState("failed", 4000);
                        }

                        if (isFunction(failedApiCall)) {
                            failedApiCall.call(self, error, response);
                        } else if (button instanceof MessageStateButton || button instanceof StateButton) {

                            if (error instanceof Response) {
                                error = new Error(error.statusText);
                            }

                            button.setMessage("request failed").showMessage(2000);
                        }

                        fireCustomEvent(self, "monster-api-bar-failed", {
                            button,
                            error,
                            response,
                            contentType: response.headers.get("Content-Type"),
                        });
                    });
            }
        })
        .catch((error) => {

            if (button instanceof MessageStateButton || button instanceof StateButton) {
                button.setState("failed", 4000);
            }

            if (isFunction(failedApiCall)) {
                failedApiCall.call(self, button, error, response);
            } else if (button instanceof MessageStateButton || button instanceof StateButton) {

                if (error instanceof Response) {
                    error = new Error(error.statusText);
                }

                button?.setMessage(error.message).showMessage(3000);
            }

            fireCustomEvent(self, "monster-api-bar-failed", {
                button,
                error,
            });
        });
}

/**
 * @private
 * @param {string} url
 * @return {Promise}
 * @throws {TypeError} the result cannot be parsed
 * @throws {TypeError} unsupported response
 */
function fetchData(url) {
    if (!url) url = this.getOption("url");
    if (!url) return Promise.resolve();

    const fetchOptions = this.getOption("fetch", {});

    const global = getGlobal();
    return global
        .fetch(url, fetchOptions)
        .then((response) => {
            const contentType = response.headers.get("content-type");
            if (contentType && contentType.indexOf("application/json") !== -1) {
                return response.text();
            }

            throw new TypeError(`unsupported response ${contentType}`);
        })
        .then((text) => {
            try {
                return Promise.resolve(JSON.parse(text));
            } catch (e) {
                throw new TypeError("the result cannot be parsed");
            }
        });
}

registerCustomElement(ApiBar);