/**
 * Copyright © Volker Schukai 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 Volker Schukai.
 */

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 API Bar
 *
 * @since 3.90.0
 * @copyright Volker Schukai
 * @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);
			}
			return 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 {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);
			} 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);
