/**
 * 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 { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import { CustomElement } from "../../dom/customelement.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { isString } from "../../types/is.mjs";
import { FetchBoxStyleSheet } from "./stylesheet/fetch-box.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { Formatter } from "../../text/formatter.mjs";

export { FetchBox };

/**
 * @private
 * @type {symbol}
 */
export const fetchBoxElementSymbol = Symbol("fetchBoxElement");

/**
 * A FetchBox
 *
 * @fragments /fragments/components/content/fetch-box/
 *
 * @example /examples/components/content/fetch-box-simple
 *
 * @since 3.115.0
 * @copyright Volker Schukai
 * @summary A fetch-driven content box that loads remote fragments or responses into a visible UI surface.
 */
class FetchBox extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @returns {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for(
			"@schukai/monster/components/content/fetch-box@@instance",
		);
	}

	/**
	 *
	 * @return {Components.Content.FetchBox
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		return this;
	}

	/**
	 * Handles the component's connection to the DOM.
	 * Determines the styling of the component based on its parent's tag name.
	 * Fetches necessary data for the component.
	 *
	 * @return {void} This method does not return a value.
	 */
	connectedCallback() {
		super.connectedCallback();

		const parent = this.parentElement;

		if (parent) {
			const blockLevelElements = [
				"DIV",
				"SECTION",
				"ARTICLE",
				"HEADER",
				"FOOTER",
				"MAIN",
				"NAV",
				"ASIDE",
			];
			const isBlockLevel = blockLevelElements.includes(parent.tagName);

			if (isBlockLevel) {
				this.style.display = "block";
				this.style.width = "100%";
				this.style.height = "100%";
			} else {
				this.style.display = "inline-flex";
				this.style.height = "100%";
			}
		}

		this.fetch();
	}

	/**
	 * 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} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} classes CSS classes
	 * @property {string} classes.default CSS class for the main template
	 * @property {string} classes.loading CSS class for the loading template
	 * @property {string} classes.error CSS class for the error template
	 * @property {string} classes.empty CSS class for the empty template
	 * @property {Object} fetch fetch options for the request
	 * @property {string} fetch.redirect error, follow, manual
	 * @property {string} fetch.method GET, POST, PUT, DELETE
	 * @property {string} fetch.mode same-origin, cors, no-cors, navigate
	 * @property {string} fetch.credentials omit, same-origin, include
	 * @property {Object} fetch.headers
	 * @property {string} fetch.headers.accept text/html, application/json
	 * @property {string} fetch.headers.content-type application/json
	 * @property {string} parameter value for the data url
	 * @property {Object} formatter
	 * @property {Object} formatter.marker
	 * @property {string} formatter.marker.open marker for the url
	 * @property {string} formatter.marker.close marker for the url
	 * @property {string} url url to fetch
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			classes: {
				default: "monster-fetch-box-default",
				loading: "loading",
				error: "error",
				empty: "empty",
			},
			data: {},
			url: null,
			fetch: {
				redirect: "error",
				method: "GET",
				mode: "same-origin",
				credentials: "same-origin",
				headers: {
					accept: "text/html",
				},
			},

			content: {
				// <slot name="loading"></slot>
				loading: `<div class="monster-skeleton-animated monster-skeleton-col-100"></div>`,
				error: `<slot name="error"></slot>`,
				empty: `<slot name="empty"></slot>`,
			},
			visible: "loading",
			parameter: null,

			formatter: {
				marker: {
					open: null,
					close: null,
				},
			},
		});
	}

	/**
	 * @return {string}
	 */
	static getTag() {
		return "monster-fetch-box";
	}

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [FetchBoxStyleSheet];
	}

	/**
	 * load content from url
	 */
	fetch() {
		try {
			return loadContent
				.call(this)
				.then((obj) => {
					if (obj.type !== "application/json") {
						this.setOption("visible", "error");
						throw new Error("not a json response");
					}

					try {
						const content = obj.content;
						if (!isString(content) || content === "") {
							this.setOption("visible", "empty");
							return;
						}

						const jsonContent = JSON.parse(content);
						if (jsonContent === null) {
							this.setOption("visible", "empty");
							return;
						}

						this.setOption("data", jsonContent);
						this.setOption("visible", "default");
					} catch (e) {
						this.setOption("visible", "error");
						throw e;
					}
				})
				.catch((e) => {
					this.setOption("visible", "error");
					addErrorAttribute(this, e);
				});
		} catch (e) {
			addErrorAttribute(this, e);
			this.setOption("visible", "error");
			return Promise.reject(e);
		}
	}
}

/**
 * @private
 * @throws {Error} missing url
 * @throws {Error} we won't be able to read the data
 * @throws {Error} request failed
 * @return {Promise}
 */
function loadContent() {
	let url = this.getOption("url");

	if (url instanceof URL) {
		url = url.toString();
	}

	if (!isString(url) || url === "") {
		throw new Error("missing url");
	}

	let p = this.getOption("parameter", null);
	if (p === null) {
		p = "";
	}

	const data = {
		parameter: p,
	};

	const formatter = new Formatter(data);

	if (this.getOption("formatter.marker.open")) {
		const open = this.getOption("formatter.marker.open");
		if (!isString(open)) {
			throw new TypeError("open is not a string");
		}

		const close = this.getOption("formatter.marker.close");
		if (close !== undefined && !isString(close)) {
			throw new TypeError("close is not a string");
		}

		formatter.setMarker(open, close);
	}

	const formattedUrl = formatter.format(url);

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

	return fetch(formattedUrl, options).then((response) => {
		if (!response.ok) {
			if (["error", "opaque", "opaqueredirect"].includes(response.type)) {
				throw new Error(`we won't be able to read the data (${response.type})`);
			}

			const statusClass = String(response.status).charAt(0);
			if (statusClass === "4") {
				throw new Error(`client error ${response.statusText}`);
			}
			throw new Error(
				`undefined status (${response.status} / ${response.statusText}) or type (${response.type})`,
			);
		}

		return response.text().then((content) => ({
			content,
			type: response.headers.get("Content-Type"),
		}));
	});
}

/**
 * @private
 * @return {void}
 */
function initControlReferences() {
	this[fetchBoxElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="control"]`,
	);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <div
                    data-monster-attributes="class path:classes.default,
				 data-monster-visible path:visible | equals:default | ?:true:false
                 ">
                <slot></slot>
            </div>
            <div
                    data-monster-attributes="class path:classes.loading,
				 data-monster-visible path:visible | equals:loading | ?:true:false"
                    data-monster-replace="path:content.loading"></div>
            <div
                    data-monster-attributes="class path:classes.error,
				 data-monster-visible path:visible | equals:error | ?:true:false"
                    data-monster-replace="path:content.error"></div>
            <div
                    data-monster-attributes="class path:classes.empty,
				 data-monster-visible path:visible | equals:empty | ?:true:false"
                    data-monster-replace="path:content.empty"></div>
        </div>`;
}

registerCustomElement(FetchBox);
