/**
 * 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 { languages } from "./map/languages.mjs";

/**
 * Normalizes a number represented as a string by converting it into a valid floating-point number format,
 * based on the provided or detected locale. It accounts for different decimal and a thousand separator conventions.
 *
 * @param {string} input - The string representation of the number to normalize. May include locale-specific formatting.
 * @param {string} [locale=navigator.language] - The locale used to determine the decimal separator convention. Defaults to the user's browser locale.
 * @return {number} The normalized number as a floating-point value. Returns NaN if the input is not a parsable number.
 */
export function normalizeNumber(input, locale = navigator.language) {
	if (input === null || input === undefined) return NaN;
	if (typeof input === "number") return input; // If input is already a number, return it directly
	if (typeof input !== "string") return NaN;

	const cleaned = input.trim().replace(/\s/g, "");
	const decimalSeparator = getDecimalSeparator(locale);
	let normalized = cleaned;

	if (decimalSeparator === "," && cleaned.includes(".")) {
		normalized = cleaned.replace(/\./g, "").replace(",", ".");
	} else if (decimalSeparator === "." && cleaned.includes(",")) {
		normalized = cleaned.replace(/,/g, "").replace(".", ".");
	} else if (decimalSeparator === "," && cleaned.includes(",")) {
		normalized = cleaned.replace(",", ".");
	}

	const result = parseFloat(normalized);
	return Number.isNaN(result) ? NaN : result;
}

/**
 * Retrieves the decimal separator for a given locale.
 *
 * @param {string} [locale=navigator.language] - The locale identifier to determine the decimal separator. Defaults to the user's current locale if not provided.
 * @return {string} The decimal separator used in the specified locale.
 */
function getDecimalSeparator(locale = navigator.language) {
	const numberWithDecimal = 1.1;
	const formatted = new Intl.NumberFormat(locale).format(numberWithDecimal);
	return formatted.replace(/\d/g, "")[0]; // z.B. "," oder "."
}

/**
 * Determines the user's preferred language based on browser settings and available language options.
 *
 * It evaluates the current HTML document language, the browser's defined languages, and
 * the language options from `<link>` elements with `hreflang` attributes in the document.
 *
 * @return {Object} An object containing information about the detected language, preferred language, and available languages.
 */
export function detectUserLanguagePreference() {
	const currentLang = document.documentElement.lang;

	let preferredLanguages = [];

	if (typeof navigator.language === "string" && navigator.language.length > 0) {
		preferredLanguages = [navigator.language];
	}

	if (Array.isArray(navigator.languages) && navigator.languages.length > 0) {
		preferredLanguages = navigator.languages;
	}

	// add to preferredLanguages all the base languages of the preferred languages
	preferredLanguages = preferredLanguages.concat(
		preferredLanguages.map((lang) => lang.split("-")[0]),
	);

	if (!currentLang && preferredLanguages.length === 0) {
		return {
			message: "No language information available.",
		};
	}

	const linkTags = document.querySelectorAll("link[hreflang]");
	if (linkTags.length === 0) {
		return {
			current: currentLang || null,
			message: "No <link> tags with hreflang available.",
		};
	}

	const availableLanguages = [...linkTags].map((link) => {
		const fullLang = link.getAttribute("hreflang");
		const baseLang = fullLang.split("-")[0];
		let label = link.getAttribute("data-monster-label");
		if (!label) {
			label = link.getAttribute("title");
			if (!label) {
				label = languages?.[fullLang];
				if (!label) {
					label = languages?.[baseLang];
				}
			}
		}

		return {
			fullLang,
			baseLang,
			label,
			href: link.getAttribute("href"),
		};
	});

	// filter availableLanguages to only include languages that are in the preferredLanguages array
	const offerableLanguages = availableLanguages.filter(
		(lang) =>
			preferredLanguages.includes(lang.fullLang) ||
			preferredLanguages.includes(lang.baseLang),
	);

	const allOfferableLanguages = availableLanguages.filter(
		(lang) => lang.baseLang !== currentLang && lang.fullLang !== currentLang,
	);

	if (offerableLanguages.length === 0) {
		return {
			current: currentLang || null,
			message: "No available languages match the user's preferences.",
			available: availableLanguages.map((lang) => ({
				...lang,
				weight: 1,
			})),
			allOfferable: allOfferableLanguages,
		};
	}

	// Helper function to determine the "weight" of a language match
	function getWeight(langEntry) {
		// Full match has priority 3
		if (preferredLanguages.includes(langEntry.fullLang)) return 3;
		// Base language match has priority 2
		if (preferredLanguages.includes(langEntry.baseLang)) return 2;
		// No match is priority 1
		return 1;
	}

	// Sort the available languages by descending weight
	offerableLanguages.sort((a, b) => getWeight(b) - getWeight(a));

	// The best match is the first in the sorted list
	const bestMatch = offerableLanguages[0];
	const bestMatchWeight = getWeight(bestMatch);

	const currentLabel = languages?.[currentLang] || currentLang;

	// If we found a language that matches user preferences (weight > 0)
	if (bestMatchWeight > 0) {
		return {
			current: currentLang || null,
			currentLabel: currentLabel,
			preferred: {
				full: bestMatch.fullLang,
				base: bestMatch.baseLang,
				label: bestMatch.label,
				href: bestMatch.href,
			},
			available: availableLanguages.map((lang) => ({
				...lang,
				weight: getWeight(lang),
			})),
			offerable: offerableLanguages.map((lang) => ({
				...lang,
				weight: getWeight(lang),
			})),
			allOfferable: allOfferableLanguages,
		};
	}

	// If no language matched the user's preferences
	return {
		current: currentLang || null,
		message: "None of the preferred languages are available.",
		available: availableLanguages.map((lang) => ({
			...lang,
			weight: getWeight(lang),
		})),
		allOfferable: allOfferableLanguages,
	};
}
