/**
 * 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.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { isString } from "../../../types/is.mjs";
import { fireCustomEvent } from "../../../dom/events.mjs";
import { validateInstance, validateString } from "../../../types/validate.mjs";

/**
 * Traverse the element's ancestors to find an existing Shadow DOM.
 *
 * @param {HTMLElement} element - The starting element.
 * @returns {ShadowRoot|null} - The found Shadow DOM or null if none exists.
 */
function findShadowRoot(element) {
	while (element) {
		if (element?.shadowRoot) {
			return element?.shadowRoot;
		}
		element = element?.parentNode;
	}
	return null;
}

/**
 * Loads content from a URL and assigns it to an element.
 * Optionally, the loaded content can be filtered using a CSS selector.
 * Additionally, any <script> elements within the content are extracted and executed.
 *
 * @param {HTMLElement} element - The target element to insert the content.
 * @param {string|URL} url - The URL from which to load the content.
 * @param {Object} options - Options for the fetch call.
 * @param {string} [filter] - Optional CSS selector to filter the loaded content.
 * @returns {Promise<Object>} A promise that resolves to an object containing { content: string, type: string | null }.
 * @throws {Error} When the content cannot be read or the response contains an error.
 * @throws {TypeError} When the provided parameters do not match the expected types.
 */
function loadAndAssignContent(element, url, options, filter) {
	return loadContent(url, options).then((response) => {
		let content = response.content;

		// Optional filtering: if a valid, non-empty CSS selector is provided,
		// only the matching elements will be retained.
		if (isString(filter) && filter !== "") {
			const filteredContainer = document.createElement("div");
			const tempContainer = document.createElement("div");
			tempContainer.innerHTML = content;
			const matchingNodes = tempContainer.querySelectorAll(filter);
			matchingNodes.forEach((node) => {
				filteredContainer.appendChild(node);
			});
			content = filteredContainer.innerHTML;
		}

		// Temporary container for processing the content and extracting scripts
		const tempDiv = document.createElement("div");
		tempDiv.innerHTML = content;

		// Extract and execute all <script> elements by appending them to the document head
		const scriptElements = tempDiv.querySelectorAll("script");
		scriptElements.forEach((oldScript) => {
			const newScript = document.createElement("script");
			if (oldScript.src) newScript.src = oldScript.src;
			if (oldScript.type) newScript.type = oldScript.type;
			if (oldScript.async) newScript.async = oldScript.async;
			if (oldScript.defer) newScript.defer = oldScript.defer;
			if (oldScript.crossOrigin) newScript.crossOrigin = oldScript.crossOrigin;
			if (oldScript.integrity) newScript.integrity = oldScript.integrity;
			if (oldScript.referrerPolicy)
				newScript.referrerPolicy = oldScript.referrerPolicy;
			newScript.textContent = oldScript.textContent;
			document.head.appendChild(newScript);
			if (oldScript.parentNode) {
				oldScript.parentNode.removeChild(oldScript);
			}
		});

		// Assign the processed content to the target element
		validateInstance(element, HTMLElement).innerHTML = tempDiv.innerHTML;

		// If the element is within a Shadow DOM, use the host as the event target
		const shadowRoot = findShadowRoot(element);
		const eventTarget = shadowRoot !== null ? shadowRoot.host : element;
		if (eventTarget instanceof HTMLElement) {
			fireCustomEvent(eventTarget, "monster-fetched", { url });
		}

		return response;
	});
}

/**
 * Loads content from a URL using fetch and returns an object with the loaded content
 * and the Content-Type header.
 *
 * @param {string|URL} url - The URL from which to load the content.
 * @param {Object} options - Options for the fetch call.
 * @returns {Promise<Object>} A promise that resolves to an object { content: string, type: string | null }.
 * @throws {Error} When the content cannot be read or the response contains an error.
 * @throws {TypeError} When the URL cannot be validated as a string.
 */
function loadContent(url, options) {
	if (url instanceof URL) {
		url = url.toString();
	}
	return fetch(validateString(url), 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"),
		}));
	});
}

export { loadAndAssignContent, loadContent };
