/**
 * 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 { instanceSymbol } from "../../constants.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { getDocument } from "../../dom/util.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { Formatter } from "../../text/formatter.mjs";
import { isFunction, isObject, isString } from "../../types/is.mjs";
import { CommonStyleSheet } from "../stylesheet/common.mjs";
import { FormStyleSheet } from "../stylesheet/form.mjs";
import { VariantSelectStyleSheet } from "./stylesheet/variant-select.mjs";
import { BuyBoxStyleSheet } from "./stylesheet/buy-box.mjs";
import "./variant-select.mjs";
import "./quantity.mjs";
import "./message-state-button.mjs";
import "../datatable/datasource/dom.mjs";
import "../datatable/datasource/rest.mjs";

export { BuyBox };

/**
 * @private
 * @type {symbol}
 */
const controlElementSymbol = Symbol("controlElement");

/**
 * @private
 * @type {symbol}
 */
const variantElementSymbol = Symbol("variantElement");

/**
 * @private
 * @type {symbol}
 */
const quantityElementSymbol = Symbol("quantityElement");

/**
 * @private
 * @type {symbol}
 */
const quantityInputElementSymbol = Symbol("quantityInputElement");

/**
 * @private
 * @type {symbol}
 */
const quantityUnitElementSymbol = Symbol("quantityUnitElement");

/**
 * @private
 * @type {symbol}
 */
const submitElementSymbol = Symbol("submitElement");

/**
 * @private
 * @type {symbol}
 */
const submitLabelElementSymbol = Symbol("submitLabelElement");

/**
 * @private
 * @type {symbol}
 */
const messageElementSymbol = Symbol("messageElement");

/**
 * @private
 * @type {symbol}
 */
const priceElementSymbol = Symbol("priceElement");

/**
 * @private
 * @type {symbol}
 */
const sumElementSymbol = Symbol("sumElement");

/**
 * @private
 * @type {symbol}
 */
const listPriceElementSymbol = Symbol("listPriceElement");

/**
 * @private
 * @type {symbol}
 */
const basePriceElementSymbol = Symbol("basePriceElement");

/**
 * @private
 * @type {symbol}
 */
const taxElementSymbol = Symbol("taxElement");

/**
 * @private
 * @type {symbol}
 */
const totalElementSymbol = Symbol("totalElement");

/**
 * @private
 * @type {symbol}
 */
const deliveryElementSymbol = Symbol("deliveryElement");

/**
 * @private
 * @type {symbol}
 */
const quantityStaticElementSymbol = Symbol("quantityStaticElement");

/**
 * @private
 * @type {symbol}
 */
const datasourceElementSymbol = Symbol("datasourceElement");

/**
 * @private
 * @type {symbol}
 */
const productDataSymbol = Symbol("productData");

/**
 * @private
 * @type {symbol}
 */
const cartDataSymbol = Symbol("cartData");

/**
 * @private
 * @type {symbol}
 */
const stockRequestSymbol = Symbol("stockRequest");

/**
 * @private
 * @type {symbol}
 */
const cartControlSymbol = Symbol("cartControl");

/**
 * @private
 * @type {symbol}
 */
const pendingCartPayloadSymbol = Symbol("pendingCartPayload");

/**
 * @private
 * @type {symbol}
 */
const pricingRequestSymbol = Symbol("pricingRequest");

/**
 * @private
 * @type {symbol}
 */
const pricingCacheSymbol = Symbol("pricingCache");

/**
 * BuyBox
 *
 * @fragments /fragments/components/form/buy-box/
 *
 * @example /examples/components/form/buy-box-basic Basic buy box
 *
 * @summary A commerce buy box for product variants, quantity changes, pricing updates and add-to-cart actions.
 * @since 4.65.0
 * @fires monster-buy-box-change
 * @fires monster-buy-box-valid
 * @fires monster-buy-box-invalid
 * @fires monster-buy-box-submit
 * @fires monster-buy-box-success
 * @fires monster-buy-box-error
 * @fires monster-buy-box-loaded
 */
class BuyBox extends CustomControl {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/form/buy-box@@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}
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} labels Labels
	 * @property {Object} features Feature toggles
	 * @property {boolean} features.messages Show status messages
	 * @property {boolean} features.rowSelects Allow row selects in variant select
	 * @property {boolean} features.combinedSelect Allow combined variant select
	 * @property {boolean} features.allowDecimal Allow decimal quantities
	 * @property {Object} product Static product config
	 * @property {Object} pricing Static pricing config
	 * @property {Object} variants Variant configuration (passed to monster-variant-select)
	 * @property {Object} quantity Quantity configuration
	 * @property {Object} cart Cart configuration (endpoint + mapping or cart control selector)
	 * @property {Object} stock Stock configuration (endpoint + mapping)
	 * @property {Object} datasource Datasource config (selector or rest)
	 * @property {Object} mapping Mapping from datasource data to product values
	 * @property {Object} actions Callback actions
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: getTranslations(),
			features: {
				messages: true,
				rowSelects: false,
				combinedSelect: false,
				allowDecimal: false,
			},
			product: {
				sku: null,
				name: null,
				stock: null,
				delivery: null,
				unitLabel: null,
				currency: "EUR",
				taxIncluded: true,
				taxRate: null,
			},
			pricing: {
				price: null,
				listPrice: null,
				basePrice: null,
				basePriceUnit: null,
				tiers: [],
				api: {
					url: null,
					features: {
						alwaysFetch: false,
					},
					fetch: {
						method: "GET",
						headers: {
							accept: "application/json",
						},
					},
					mapping: {
						selector: "*",
						priceTemplate: null,
						listPriceTemplate: null,
						basePriceTemplate: null,
						basePriceUnitTemplate: null,
						tiersPath: null,
						taxRateTemplate: null,
						taxIncludedTemplate: null,
					},
				},
			},
			variants: {
				dimensions: [],
				data: [],
				features: {},
				pricing: {
					priceKey: "price",
					listPriceKey: "listPrice",
					basePriceKey: "basePrice",
					basePriceUnitKey: "basePriceUnit",
					currencyKey: "currency",
					taxRateKey: "taxRate",
					taxIncludedKey: "taxIncluded",
					tiersKey: "tiers",
				},
			},
			quantity: {
				value: 1,
				min: 1,
				max: Number.POSITIVE_INFINITY,
				step: 1,
				precision: null,
				allowed: null,
				rounding: "half-up",
			},
			datasource: {
				selector: null,
				rest: null,
			},
			mapping: {
				selector: "*",
				skuTemplate: null,
				nameTemplate: null,
				stockTemplate: null,
				deliveryTemplate: null,
				unitLabelTemplate: null,
				priceTemplate: null,
				listPriceTemplate: null,
				basePriceTemplate: null,
				basePriceUnitTemplate: null,
				currencyTemplate: null,
				taxIncludedTemplate: null,
				taxRateTemplate: null,
				variantsPath: null,
				cartPath: null,
			},
			stock: {
				url: null,
				fetch: {
					method: "GET",
					headers: {
						accept: "application/json",
					},
				},
				mapping: {
					selector: "*",
					stockTemplate: null,
				},
				features: {
					autoCheck: true,
				},
			},
			cart: {
				url: null,
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				controlSelector: null,
				bodyTemplate: null,
				mapping: {
					selector: "*",
					skuTemplate: null,
					qtyTemplate: null,
				},
			},
			actions: {
				onchange: null,
				onvalid: null,
				oninvalid: null,
				onsubmit: null,
				onsuccess: null,
				onerror: null,
				onload: null,
			},
		});
	}

	/**
	 * @return {BuyBox}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		this[pricingCacheSymbol] = new Map();
		initControlReferences.call(this);
		initDatasource.call(this);
		initEventHandler.call(this);
		updateSubmitLabel.call(this);
		applyData.call(this);
		return this;
	}

	/**
	 * Re-apply options and mapped data.
	 * @return {BuyBox}
	 */
	refresh() {
		initDatasource.call(this);
		applyData.call(this);
		return this;
	}

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [
			CommonStyleSheet,
			FormStyleSheet,
			VariantSelectStyleSheet,
			BuyBoxStyleSheet,
		];
	}

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

/**
 * @private
 */
function initControlReferences() {
	this[controlElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=control]",
	);
	this[variantElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=variants]",
	);
	this[quantityElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=quantity]",
	);
	this[quantityInputElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=quantity-input]",
	);
	this[quantityUnitElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=quantity-unit]",
	);
	this[submitElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=submit]",
	);
	this[submitLabelElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=submit-label]",
	);
	this[messageElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=message]",
	);
	this[quantityStaticElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=quantity-static]",
	);
	this[priceElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=price]",
	);
	this[sumElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=sum]",
	);
	this[listPriceElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=list-price]",
	);
	this[basePriceElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=base-price]",
	);
	this[taxElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=tax]",
	);
	this[totalElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=total]",
	);
	this[deliveryElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=delivery]",
	);
}

/**
 * @private
 * @return {HTMLElement|null}
 */
function getCartControl() {
	const selector = this.getOption("cart.controlSelector");
	if (!isString(selector) || selector === "") {
		return null;
	}
	if (this[cartControlSymbol] && this[cartControlSymbol].isConnected) {
		return this[cartControlSymbol];
	}
	const element = getDocument().querySelector(selector);
	if (element) {
		this[cartControlSymbol] = element;
	}
	return element || null;
}

/**
 * @private
 */
function initDatasource() {
	if (this[datasourceElementSymbol]) {
		return;
	}
	const selector = this.getOption("datasource.selector");
	if (isString(selector) && selector !== "") {
		this[datasourceElementSymbol] = getDocument().querySelector(selector);
	} else if (isObject(this.getOption("datasource.rest"))) {
		const rest = getDocument().createElement("monster-datasource-rest");
		rest.setOption("read", this.getOption("datasource.rest.read", {}));
		rest.setOption("features.autoInit", true);
		rest.setOption("autoInit.oneTime", true);
		this.appendChild(rest);
		this[datasourceElementSymbol] = rest;
	}

	if (this[datasourceElementSymbol]) {
		this[datasourceElementSymbol].addEventListener(
			"monster-datasource-fetched",
			(event) => {
				const data = event?.detail?.data || this[datasourceElementSymbol]?.data;
				if (data) {
					this[productDataSymbol] = data;
					applyData.call(this);
				}
			},
		);
	}
}

/**
 * @private
 */
function initEventHandler() {
	const variants = this.shadowRoot.querySelector("monster-variant-select");
	const quantity = this.shadowRoot.querySelector("monster-quantity");
	const submit = this[submitElementSymbol];

	if (variants) {
		variants.addEventListener("monster-variant-select-change", () => {
			updateState.call(this);
			checkStockIfNeeded.call(this);
			checkPricingIfNeeded.call(this);
		});
	}

	if (quantity) {
		quantity.addEventListener("monster-quantity-change", () => {
			updateState.call(this);
			checkStockIfNeeded.call(this);
			checkPricingIfNeeded.call(this);
		});
	}
	if (this[quantityInputElementSymbol]) {
		this[quantityInputElementSymbol].addEventListener("input", () => {
			updateState.call(this);
			checkStockIfNeeded.call(this);
			checkPricingIfNeeded.call(this);
		});
	}

	if (submit) {
		submit.setOption("actions.click", () => {
			handleSubmit.call(this);
		});
	}
}

/**
 * @private
 */
function applyData() {
	const data = this[productDataSymbol] || this.getOption("product");
	if (!data) {
		return;
	}

	const mapped = mapProductData.call(this, data);
	this[productDataSymbol] = mapped;
	this[cartDataSymbol] = mapped.cart;

	applyVariants.call(this, mapped);
	applyQuantity.call(this);
	updateState.call(this, true);
	checkStockIfNeeded.call(this);
	checkPricingIfNeeded.call(this);
	updateSubmitLabel.call(this);
	fireCustomEvent(this, "monster-buy-box-loaded", { product: mapped });
	const loadAction = this.getOption("actions.onload");
	if (isFunction(loadAction)) {
		loadAction.call(this, mapped);
	}
}

/**
 * @private
 * @param {Object} data
 * @return {Object}
 */
function mapProductData(data) {
	const mapping = this.getOption("mapping", {});
	let source = data;
	if (
		isString(mapping?.selector) &&
		mapping.selector !== "*" &&
		mapping.selector
	) {
		try {
			source = new Pathfinder(data).getVia(mapping.selector);
		} catch (_e) {
			source = data;
		}
	}

	const value = (key) => mapValueFromSource(source, mapping[key]);

	const product = Object.assign({}, this.getOption("product"), {
		sku: value("skuTemplate") ?? this.getOption("product.sku"),
		name: value("nameTemplate") ?? this.getOption("product.name"),
		stock: parseNumber(value("stockTemplate"), this.getOption("product.stock")),
		delivery: value("deliveryTemplate") ?? this.getOption("product.delivery"),
		unitLabel:
			value("unitLabelTemplate") ?? this.getOption("product.unitLabel"),
		currency: value("currencyTemplate") ?? this.getOption("product.currency"),
		taxIncluded: parseBoolean(
			value("taxIncludedTemplate"),
			this.getOption("product.taxIncluded"),
		),
		taxRate: parseNumber(
			value("taxRateTemplate"),
			this.getOption("product.taxRate"),
		),
	});

	const pricing = Object.assign({}, this.getOption("pricing"), {
		price: parseNumber(value("priceTemplate"), this.getOption("pricing.price")),
		listPrice: parseNumber(
			value("listPriceTemplate"),
			this.getOption("pricing.listPrice"),
		),
		basePrice: parseNumber(
			value("basePriceTemplate"),
			this.getOption("pricing.basePrice"),
		),
		basePriceUnit:
			value("basePriceUnitTemplate") ?? this.getOption("pricing.basePriceUnit"),
	});

	const variantsPath = mapping?.variantsPath;
	const variantData = variantsPath
		? new Pathfinder(source).getVia(variantsPath)
		: this.getOption("variants.data");

	const cartPath = mapping?.cartPath;
	const cartData = cartPath ? new Pathfinder(source).getVia(cartPath) : null;

	return { product, pricing, variants: variantData, cart: cartData, source };
}

/**
 * @private
 * @param {Object} source
 * @param {string|null} template
 * @return {string|null}
 */
function mapValueFromSource(source, template) {
	if (!isString(template) || template === "") {
		return null;
	}
	try {
		if (template.includes("${")) {
			return new Formatter(source).format(template);
		}
		return new Pathfinder(source).getVia(template);
	} catch (_e) {
		return null;
	}
}

/**
 * @private
 * @param {Object} mapped
 */
function applyVariants(mapped) {
	const variants = this.shadowRoot.querySelector("monster-variant-select");
	if (!variants) {
		return;
	}

	variants.setAttribute(
		"aria-label",
		this.getOption("labels.variantsAria") || "Variant selection",
	);
	const variantOptions = Object.assign({}, this.getOption("variants"));
	if (Array.isArray(mapped?.variants)) {
		variantOptions.data = mapped.variants;
	}

	variantOptions.features = Object.assign({}, variantOptions.features, {
		rowSelects: this.getOption("features.rowSelects"),
		combinedSelect: this.getOption("features.combinedSelect"),
	});

	const hasDimensions =
		Array.isArray(variantOptions.dimensions) &&
		variantOptions.dimensions.length > 0;
	const hasData =
		Array.isArray(variantOptions.data) && variantOptions.data.length > 0;
	const hasUrl = isString(variantOptions.url) && variantOptions.url !== "";
	if (this[variantElementSymbol]) {
		this[variantElementSymbol].hidden = !(hasDimensions && (hasData || hasUrl));
	}

	variants.setOption("dimensions", variantOptions.dimensions || []);
	variants.setOption("data", variantOptions.data || []);
	variants.setOption("url", variantOptions.url ?? null);
	variants.setOption("fetch", variantOptions.fetch || {});
	variants.setOption("mapping", variantOptions.mapping || {});
	variants.setOption("layout", variantOptions.layout || {});
	variants.setOption("features.rowSelects", variantOptions.features.rowSelects);
	variants.setOption(
		"features.combinedSelect",
		variantOptions.features.combinedSelect,
	);
	if (typeof variants.refresh === "function") {
		variants.refresh();
	}
}

/**
 * @private
 */
function applyQuantity() {
	const quantity = this.shadowRoot.querySelector("monster-quantity");
	const quantityInput = this[quantityInputElementSymbol];
	const quantityStatic = this[quantityStaticElementSymbol];
	const quantityLabel = this.shadowRoot.querySelector(
		"[data-monster-role=quantity-label]",
	);
	if (!quantity) {
		return;
	}
	if (!quantityInput) {
		return;
	}
	quantity.setAttribute(
		"aria-label",
		this.getOption("labels.quantityAria") || "Quantity",
	);
	quantityInput.setAttribute(
		"aria-label",
		this.getOption("labels.quantityAria") || "Quantity",
	);
	const options = this.getOption("quantity", {});
	const precision =
		this.getOption("features.allowDecimal") === true
			? Number(options.precision ?? 2)
			: 0;
	const useDecimalInput = this.getOption("features.allowDecimal") === true;
	const hasFixedQuantity = isFixedQuantity(options);
	quantity.setOption("min", options.min);
	quantity.setOption("max", options.max);
	quantity.setOption("step", options.step);
	quantity.setOption("precision", precision);
	quantity.setOption("inputmode", useDecimalInput ? "decimal" : "numeric");
	quantity.value = options.value ?? options.min ?? 1;

	quantity.hidden = useDecimalInput || hasFixedQuantity;
	quantityInput.hidden = !useDecimalInput || hasFixedQuantity;
	quantityInput.step = options.step ?? 1;
	quantityInput.min = Number.isFinite(options.min) ? options.min : "";
	quantityInput.max = Number.isFinite(options.max) ? options.max : "";
	quantityInput.value = formatQuantityForLocale(
		Number.isFinite(options.value) ? options.value : (options.min ?? 1),
		precision,
	);
	if (quantityStatic) {
		quantityStatic.hidden = !hasFixedQuantity;
		if (hasFixedQuantity) {
			const label = this.getOption("labels.quantity") || "Quantity";
			setElementText(
				quantityStatic,
				`${label}: ${formatQuantityForLocale(options.min ?? 1, precision)}`,
			);
		}
	}
	if (quantityLabel) {
		quantityLabel.hidden = hasFixedQuantity;
	}

	if (this[quantityUnitElementSymbol]) {
		const unit = this[productDataSymbol]?.product?.unitLabel;
		this[quantityUnitElementSymbol].textContent =
			unit && unit !== "" ? `(${unit})` : "";
	}
}

/**
 * @private
 * @param {boolean} initial
 */
function updateState(initial = false) {
	const variants = this.shadowRoot.querySelector("monster-variant-select");
	const quantity = this.shadowRoot.querySelector("monster-quantity");
	const selection = variants?.getOption?.("selection") || {};
	const hasVariants = hasVariantOptions.call(this, variants);
	const sku = hasVariants ? variants?.value : this.getOption("product.sku");
	const qty = getQuantityValue.call(this, quantity);

	const productBase =
		this[productDataSymbol]?.product || this.getOption("product");
	const pricingBase =
		this[productDataSymbol]?.pricing || this.getOption("pricing");
	const variantData = findSelectedVariantData.call(this, variants, selection);
	const variantPricingActive = hasVariantPricing.call(this, variants);
	const resolved = resolvePricing.call(
		this,
		pricingBase,
		productBase,
		variantData,
		qty,
		variantPricingActive,
	);
	const product = resolved.product;
	const pricing = resolved.pricing;
	const isPriceOk = resolved.priceAvailable !== false;
	const stock = product?.stock;
	const cartItem = findCartItem.call(this, sku);
	const hasSku = isString(sku) && sku !== "";

	const isValidQty = isQuantityAllowed.call(this, qty);
	const isStockOk = hasSku
		? Number.isFinite(stock)
			? qty <= stock
			: true
		: true;
	const isVariantOk = hasSku;

	updatePriceDisplay.call(this, pricing, qty, product);
	updateMessage.call(this, {
		isValidQty,
		isStockOk,
		isVariantOk,
		isPriceOk,
		hasVariants,
	});

	const detail = {
		sku,
		selection,
		qty,
		product,
		pricing,
		cartItem,
		priceAvailable: isPriceOk,
	};
	fireCustomEvent(this, "monster-buy-box-change", detail);
	const changeAction = this.getOption("actions.onchange");
	if (isFunction(changeAction)) {
		changeAction.call(this, detail);
	}

	const valid = isValidQty && isStockOk && isVariantOk && isPriceOk;
	if (!initial) {
		if (valid) {
			fireCustomEvent(this, "monster-buy-box-valid", detail);
			const action = this.getOption("actions.onvalid");
			if (isFunction(action)) {
				action.call(this, detail);
			}
		} else {
			fireCustomEvent(this, "monster-buy-box-invalid", detail);
			const action = this.getOption("actions.oninvalid");
			if (isFunction(action)) {
				action.call(this, detail);
			}
		}
	}

	setSubmitEnabled.call(this, valid);
}

/**
 * @private
 * @param {Object} pricing
 * @param {number} qty
 * @param {Object} product
 */
function updatePriceDisplay(pricing, qty, product) {
	const currency = product?.currency || "EUR";
	const price = pricing?.price;
	const listPrice = pricing?.listPrice;
	const basePrice = pricing?.basePrice;
	const baseUnit = pricing?.basePriceUnit;
	const taxIncluded = product?.taxIncluded;
	const taxRate = product?.taxRate;
	const subtotal =
		price !== null && price !== undefined && Number.isFinite(qty)
			? price * qty
			: null;

	if (this[priceElementSymbol]) {
		setElementText(
			this[priceElementSymbol],
			price !== null && price !== undefined ? formatMoney(price, currency) : "",
		);
	}
	if (this[sumElementSymbol]) {
		setElementText(
			this[sumElementSymbol],
			subtotal !== null ? formatMoney(subtotal, currency) : "",
		);
	}
	if (this[listPriceElementSymbol]) {
		setElementText(
			this[listPriceElementSymbol],
			listPrice !== null && listPrice !== undefined
				? formatMoney(listPrice, currency)
				: "",
		);
	}
	if (this[basePriceElementSymbol]) {
		if (basePrice !== null && basePrice !== undefined) {
			const base = formatMoney(basePrice, currency);
			setElementText(
				this[basePriceElementSymbol],
				baseUnit ? `${base} / ${baseUnit}` : base,
			);
		} else {
			setElementText(this[basePriceElementSymbol], "");
		}
	}
	if (this[taxElementSymbol]) {
		const label = taxIncluded
			? this.getOption("labels.taxIncluded")
			: this.getOption("labels.taxExcluded");
		if (Number.isFinite(taxRate)) {
			setElementText(this[taxElementSymbol], `${label} (${taxRate}%)`);
		} else {
			setElementText(this[taxElementSymbol], label);
		}
	}
	if (this[totalElementSymbol]) {
		const total = subtotal;
		setElementText(
			this[totalElementSymbol],
			total !== null ? formatMoney(total, currency) : "",
		);
	}
	if (this[deliveryElementSymbol]) {
		setElementText(
			this[deliveryElementSymbol],
			product?.delivery ?? this.getOption("labels.deliveryUnknown"),
		);
	}
}

/**
 * @private
 * @param {Object} state
 */
function updateMessage(state) {
	if (this.getOption("features.messages") === false) {
		if (this[messageElementSymbol]) {
			this[messageElementSymbol].textContent = "";
		}
		return;
	}

	if (!this[messageElementSymbol]) {
		return;
	}

	let msg = "";
	msg = getInvalidMessage.call(this, state);

	this[messageElementSymbol].textContent = msg;
}

/**
 * @private
 * @param {Object} state
 * @return {string}
 */
function getInvalidMessage(state) {
	if (!state) {
		return "";
	}
	if (!state.isVariantOk && state.hasVariants) {
		return this.getOption("labels.selectVariant");
	}
	if (state.isPriceOk === false) {
		return this.getOption("labels.priceUnavailable");
	}
	if (!state.isValidQty) {
		return this.getOption("labels.selectQuantity");
	}
	if (!state.isStockOk) {
		return this.getOption("labels.outOfStock");
	}
	return "";
}

/**
 * @private
 * @param {boolean} enabled
 */
function setSubmitEnabled(enabled) {
	if (!this[submitElementSymbol]) {
		return;
	}
	this[submitElementSymbol].setOption("features.disableButton", false);
}

/**
 * @private
 */
function handleSubmit() {
	const variants = this.shadowRoot.querySelector("monster-variant-select");
	const quantity = this.shadowRoot.querySelector("monster-quantity");
	const hasVariants = hasVariantOptions.call(this, variants);
	const sku = hasVariants ? variants?.value : this.getOption("product.sku");
	const qty = getQuantityValue.call(this, quantity);
	const selection = variants?.getOption?.("selection") || {};
	const productBase =
		this[productDataSymbol]?.product || this.getOption("product");
	const pricingBase =
		this[productDataSymbol]?.pricing || this.getOption("pricing");
	const variantData = findSelectedVariantData.call(this, variants, selection);
	const variantPricingActive = hasVariantPricing.call(this, variants);
	const resolved = resolvePricing.call(
		this,
		pricingBase,
		productBase,
		variantData,
		qty,
		variantPricingActive,
	);
	const product = resolved.product;
	const pricing = resolved.pricing;
	const isPriceOk = resolved.priceAvailable !== false;

	const hasSku = isString(sku) && sku !== "";
	const stock = product?.stock;
	const isValidQty = isQuantityAllowed.call(this, qty);
	const isStockOk = hasSku
		? Number.isFinite(stock)
			? qty <= stock
			: true
		: true;
	const isVariantOk = hasSku;
	const valid = isValidQty && isStockOk && isVariantOk && isPriceOk;
	if (!valid) {
		updateState.call(this);
		const button = this[submitElementSymbol];
		const message = getInvalidMessage.call(this, {
			isValidQty,
			isStockOk,
			isVariantOk,
			isPriceOk,
			hasVariants,
		});
		button?.setState("failed", 3000);
		button?.setMessage(message);
		button?.showMessage?.(3000);
		return;
	}

	const detail = { sku, qty, selection, product, pricing };
	fireCustomEvent(this, "monster-buy-box-submit", detail);
	const action = this.getOption("actions.onsubmit");
	if (isFunction(action)) {
		action.call(this, detail);
	}

	const button = this[submitElementSymbol];
	const cartControl = getCartControl.call(this);
	if (cartControl && isFunction(cartControl.addOrChange)) {
		this[pendingCartPayloadSymbol] = detail;
		button?.setState("activity");
		const onVerified = (event) => {
			if (!matchesCartPayload.call(this, event?.detail)) {
				return;
			}
			this[pendingCartPayloadSymbol] = null;
			cartControl.removeEventListener(
				"monster-cart-control-verified",
				onVerified,
			);
			cartControl.removeEventListener("monster-cart-control-error", onError);
			const responseData = event?.detail?.data;
			const items = Array.isArray(responseData)
				? responseData
				: Array.isArray(responseData?.items)
					? responseData.items
					: null;
			if (items) {
				this[cartDataSymbol] = items;
				updateState.call(this);
			}
			button?.setState("successful", 2000);
			fireCustomEvent(this, "monster-buy-box-success", {
				sku,
				qty,
				payload: event?.detail?.data,
			});
			const okAction = this.getOption("actions.onsuccess");
			if (isFunction(okAction)) {
				okAction.call(this, { sku, qty, payload: event?.detail?.data });
			}
		};
		const onError = (event) => {
			if (!matchesCartPayload.call(this, event?.detail)) {
				return;
			}
			this[pendingCartPayloadSymbol] = null;
			cartControl.removeEventListener(
				"monster-cart-control-verified",
				onVerified,
			);
			cartControl.removeEventListener("monster-cart-control-error", onError);
			button?.setState("failed", 3000);
			button?.setMessage(this.getOption("labels.addToCartError"));
			button?.showMessage?.(3000);
			fireCustomEvent(this, "monster-buy-box-error", {
				sku,
				qty,
				error: event?.detail?.error,
			});
			const errAction = this.getOption("actions.onerror");
			if (isFunction(errAction)) {
				errAction.call(this, event?.detail?.error);
			}
		};
		cartControl.addEventListener("monster-cart-control-verified", onVerified);
		cartControl.addEventListener("monster-cart-control-error", onError);
		cartControl.addOrChange(detail);
		return;
	}

	const url = this.getOption("cart.url");
	if (!isString(url) || url === "") {
		const notConfigured = this.getOption("labels.addToCartNotConfigured");
		button?.setState("failed", 3000);
		button?.setMessage(notConfigured);
		button?.showMessage?.(3000);
		fireCustomEvent(this, "monster-buy-box-error", {
			sku,
			qty,
			error: new Error("cart url not configured"),
		});
		const errAction = this.getOption("actions.onerror");
		if (isFunction(errAction)) {
			errAction.call(this, new Error("cart url not configured"));
		}
		return;
	}

	button?.setState("activity");

	fetch(url, buildCartRequest.call(this, detail))
		.then(async (response) => {
			if (!response.ok) {
				throw new Error("add to cart failed");
			}
			const payload = await response.json();
			button?.setState("successful", 2000);
			fireCustomEvent(this, "monster-buy-box-success", { sku, qty, payload });
			const okAction = this.getOption("actions.onsuccess");
			if (isFunction(okAction)) {
				okAction.call(this, { sku, qty, payload });
			}
		})
		.catch((e) => {
			button?.setState("failed", 3000);
			button?.setMessage(this.getOption("labels.addToCartError"));
			button?.showMessage?.(3000);
			fireCustomEvent(this, "monster-buy-box-error", { sku, qty, error: e });
			const errAction = this.getOption("actions.onerror");
			if (isFunction(errAction)) {
				errAction.call(this, e);
			}
		});
}

/**
 * @private
 * @param {Object} detail
 * @return {boolean}
 */
function matchesCartPayload(detail) {
	const pending = this[pendingCartPayloadSymbol];
	if (!pending) {
		return false;
	}
	const source = detail?.source || detail?.payload;
	if (!source || typeof source !== "object") {
		return true;
	}
	if (isString(source.sku) && source.sku !== pending.sku) {
		return false;
	}
	if (source.qty !== undefined && Number(source.qty) !== Number(pending.qty)) {
		return false;
	}
	return true;
}

/**
 * @private
 * @param {Object} data
 * @return {Object}
 */
function buildCartRequest(data) {
	const headers = Object.assign({}, this.getOption("cart.headers", {}));
	const method = this.getOption("cart.method") || "POST";
	const bodyTemplate = this.getOption("cart.bodyTemplate");
	let body = null;

	if (isString(bodyTemplate) && bodyTemplate.includes("${")) {
		body = new Formatter(stringifyForTemplate(data)).format(bodyTemplate);
	} else {
		body = JSON.stringify(data);
	}

	return { method, headers, body };
}

/**
 * @private
 * @param {Object|Array|string|number|boolean|null} value
 * @return {Object|Array|string}
 */
function stringifyForTemplate(value) {
	if (value === null || value === undefined) {
		return "";
	}
	if (Array.isArray(value)) {
		return value.map((entry) => stringifyForTemplate(entry));
	}
	if (typeof value === "object") {
		const result = {};
		for (const [key, entry] of Object.entries(value)) {
			result[key] = stringifyForTemplate(entry);
		}
		return result;
	}
	return `${value}`;
}

/**
 * @private
 * @param {number} value
 * @return {boolean}
 */
function isQuantityAllowed(value) {
	if (!Number.isFinite(value)) {
		return false;
	}

	const options = this.getOption("quantity", {});
	if (this.getOption("features.allowDecimal") !== true) {
		if (!Number.isInteger(value)) {
			return false;
		}
	}
	const min = Number(options.min);
	const max = Number(options.max);
	if (Number.isFinite(min) && value < min) {
		return false;
	}
	if (Number.isFinite(max) && value > max) {
		return false;
	}

	const allowed = options.allowed;
	if (!allowed) {
		return true;
	}

	const { list, ranges } = parseAllowed(allowed);
	if (list.length === 0 && ranges.length === 0) {
		return true;
	}

	const allowDecimal = this.getOption("features.allowDecimal") === true;
	const precision = Number.isFinite(options.precision)
		? options.precision
		: allowDecimal
			? 2
			: 0;
	const rounded = roundQuantity(value, precision, options.rounding);
	if (list.includes(rounded)) {
		return true;
	}
	for (const range of ranges) {
		if (rounded >= range.min && rounded <= range.max) {
			return true;
		}
	}
	return false;
}

/**
 * @private
 * @param {string|Array} allowed
 * @return {Object}
 */
function parseAllowed(allowed) {
	if (Array.isArray(allowed)) {
		return {
			list: allowed.filter((v) => Number.isFinite(v)),
			ranges: [],
		};
	}
	if (!isString(allowed)) {
		return { list: [], ranges: [] };
	}
	const list = [];
	const ranges = [];
	const parts = allowed.split(",");
	for (const part of parts) {
		const trimmed = part.trim();
		if (trimmed === "") {
			continue;
		}
		if (trimmed.includes("-")) {
			const [minRaw, maxRaw] = trimmed.split("-");
			const min = minRaw === "" ? Number.NEGATIVE_INFINITY : Number(minRaw);
			const max = maxRaw === "" ? Number.POSITIVE_INFINITY : Number(maxRaw);
			if (Number.isFinite(min) || Number.isFinite(max)) {
				ranges.push({ min, max });
			}
			continue;
		}
		const value = Number(trimmed);
		if (Number.isFinite(value)) {
			list.push(value);
		}
	}
	return { list, ranges };
}

/**
 * @private
 * @param {HTMLElement|null} variants
 * @param {Object} selection
 * @return {Object|null}
 */
function findSelectedVariantData(variants, selection) {
	if (!(variants instanceof HTMLElement)) {
		return null;
	}
	const data = variants.getOption?.("data") || [];
	const dimensions = variants.getOption?.("dimensions") || [];
	if (!Array.isArray(data) || data.length === 0) {
		return null;
	}
	if (!selection || Object.keys(selection).length === 0) {
		return null;
	}
	for (const item of data) {
		let matches = true;
		for (const def of dimensions) {
			const key = def?.key;
			if (!isString(key) || key === "") {
				continue;
			}
			const expected = selection?.[key];
			if (!isString(expected) || expected === "") {
				matches = false;
				break;
			}
			const value = getDimensionValueFromItem(item, def);
			if (String(value) !== String(expected)) {
				matches = false;
				break;
			}
		}
		if (matches) {
			return item;
		}
	}
	return null;
}

/**
 * @private
 * @param {HTMLElement|null} variants
 * @return {boolean}
 */
function hasVariantPricing(variants) {
	if (!(variants instanceof HTMLElement)) {
		return false;
	}
	const data = variants.getOption?.("data") || [];
	if (!Array.isArray(data) || data.length === 0) {
		return false;
	}
	const pricingKeys = getVariantPricingKeys.call(this);
	const priceKey = pricingKeys.priceKey;
	const tiersKey = pricingKeys.tiersKey;
	for (const item of data) {
		if (isString(priceKey) && priceKey !== "") {
			const value = item?.[priceKey];
			if (value !== null && value !== undefined && value !== "") {
				return true;
			}
		}
		if (isString(tiersKey) && tiersKey !== "") {
			const tiers = item?.[tiersKey];
			if (Array.isArray(tiers) && tiers.length > 0) {
				return true;
			}
		}
	}
	return false;
}

/**
 * @private
 * @param {Object} item
 * @param {Object} def
 * @return {string|null}
 */
function getDimensionValueFromItem(item, def) {
	const template = def?.valueTemplate;
	if (isString(template) && template !== "") {
		return new Formatter(item).format(template);
	}
	const key = def?.key;
	if (!isString(key) || key === "") {
		return null;
	}
	try {
		return new Pathfinder(item).getVia(key);
	} catch (_e) {
		return item?.[key];
	}
}

/**
 * @private
 * @param {Object} pricing
 * @param {Object} product
 * @param {Object|null} variantData
 * @param {number} qty
 * @return {{pricing: Object, product: Object}}
 */
function resolvePricing(
	pricing,
	product,
	variantData,
	qty,
	variantPricingActive = false,
) {
	let nextPricing = Object.assign({}, pricing);
	let nextProduct = Object.assign({}, product);
	let priceAvailable = true;

	if (variantPricingActive) {
		nextPricing = Object.assign({}, nextPricing, {
			price: null,
			listPrice: null,
			basePrice: null,
			basePriceUnit: null,
			tiers: [],
		});
		if (!variantData) {
			priceAvailable = false;
		}
	}

	if (variantData) {
		const pricingKeys = getVariantPricingKeys.call(this);
		const priceKey = pricingKeys.priceKey;
		const listPriceKey = pricingKeys.listPriceKey;
		const basePriceKey = pricingKeys.basePriceKey;
		const basePriceUnitKey = pricingKeys.basePriceUnitKey;
		const currencyKey = pricingKeys.currencyKey;
		const taxRateKey = pricingKeys.taxRateKey;
		const taxIncludedKey = pricingKeys.taxIncludedKey;
		const tiersKey = pricingKeys.tiersKey;

		const variantPrice =
			isString(priceKey) && priceKey !== "" ? variantData?.[priceKey] : null;
		const variantListPrice =
			isString(listPriceKey) && listPriceKey !== ""
				? variantData?.[listPriceKey]
				: null;
		const variantBasePrice =
			isString(basePriceKey) && basePriceKey !== ""
				? variantData?.[basePriceKey]
				: null;
		const variantBaseUnit =
			isString(basePriceUnitKey) && basePriceUnitKey !== ""
				? variantData?.[basePriceUnitKey]
				: null;
		const variantCurrency =
			isString(currencyKey) && currencyKey !== ""
				? variantData?.[currencyKey]
				: null;
		const variantTaxRate =
			isString(taxRateKey) && taxRateKey !== ""
				? variantData?.[taxRateKey]
				: null;
		const variantTaxIncluded =
			isString(taxIncludedKey) && taxIncludedKey !== ""
				? variantData?.[taxIncludedKey]
				: null;
		const variantTiers =
			isString(tiersKey) && tiersKey !== "" ? variantData?.[tiersKey] : null;

		if (variantPrice !== null && variantPrice !== undefined) {
			nextPricing.price = parseNumber(variantPrice, nextPricing.price);
		}
		if (variantListPrice !== null && variantListPrice !== undefined) {
			nextPricing.listPrice = parseNumber(
				variantListPrice,
				nextPricing.listPrice,
			);
		}
		if (variantBasePrice !== null && variantBasePrice !== undefined) {
			nextPricing.basePrice = parseNumber(
				variantBasePrice,
				nextPricing.basePrice,
			);
		}
		if (variantBaseUnit !== null && variantBaseUnit !== undefined) {
			nextPricing.basePriceUnit = variantBaseUnit;
		}
		if (variantCurrency !== null && variantCurrency !== undefined) {
			nextProduct.currency = variantCurrency;
		}
		if (variantTaxRate !== null && variantTaxRate !== undefined) {
			nextProduct.taxRate = parseNumber(variantTaxRate, nextProduct.taxRate);
		}
		if (variantTaxIncluded !== null && variantTaxIncluded !== undefined) {
			nextProduct.taxIncluded = parseBoolean(
				variantTaxIncluded,
				nextProduct.taxIncluded,
			);
		}
		if (Array.isArray(variantTiers)) {
			nextPricing.tiers = variantTiers;
		}
	}

	const tiers = Array.isArray(nextPricing.tiers)
		? nextPricing.tiers
		: Array.isArray(this.getOption("pricing.tiers"))
			? this.getOption("pricing.tiers")
			: [];
	if (tiers.length > 0 && Number.isFinite(qty)) {
		const tier = findTierForQuantity(tiers, qty);
		if (tier) {
			const tierResult = applyTier(tier, nextPricing);
			nextPricing = Object.assign({}, nextPricing, tierResult.pricing);
			if (tierResult.currency) {
				nextProduct.currency = tierResult.currency;
			}
		}
	}

	if (variantPricingActive) {
		priceAvailable = Number.isFinite(nextPricing.price);
	}

	return { pricing: nextPricing, product: nextProduct, priceAvailable };
}

/**
 * @private
 * @return {Object}
 */
function getVariantPricingKeys() {
	const defaults = {
		priceKey: "price",
		listPriceKey: "listPrice",
		basePriceKey: "basePrice",
		basePriceUnitKey: "basePriceUnit",
		currencyKey: "currency",
		taxRateKey: "taxRate",
		taxIncludedKey: "taxIncluded",
		tiersKey: "tiers",
	};
	const configured = this.getOption("variants.pricing");
	if (!isObject(configured)) {
		return defaults;
	}
	return Object.assign({}, defaults, configured);
}

/**
 * @private
 * @param {Array} tiers
 * @param {number} qty
 * @return {Object|null}
 */
function findTierForQuantity(tiers, qty) {
	let match = null;
	for (const tier of tiers) {
		const minRaw = tier?.min ?? tier?.from ?? tier?.qty ?? null;
		const maxRaw = tier?.max ?? tier?.to ?? null;
		const min = minRaw === null ? 1 : Number(minRaw);
		const max = maxRaw === null || maxRaw === undefined ? null : Number(maxRaw);
		if (!Number.isFinite(min)) {
			continue;
		}
		if (qty < min) {
			continue;
		}
		if (max !== null && Number.isFinite(max) && qty > max) {
			continue;
		}
		if (!match || min > Number(match.min ?? 0)) {
			match = Object.assign({}, tier, { min, max });
		}
	}
	return match;
}

/**
 * @private
 * @param {Object} tier
 * @param {Object} fallback
 * @return {Object}
 */
function applyTier(tier, fallback) {
	const next = Object.assign({}, fallback);
	let currency = null;
	if (tier.price !== undefined) {
		next.price = parseNumber(tier.price, next.price);
	}
	if (tier.listPrice !== undefined) {
		next.listPrice = parseNumber(tier.listPrice, next.listPrice);
	}
	if (tier.basePrice !== undefined) {
		next.basePrice = parseNumber(tier.basePrice, next.basePrice);
	}
	if (tier.basePriceUnit !== undefined) {
		next.basePriceUnit = tier.basePriceUnit;
	}
	if (tier.currency !== undefined) {
		currency = tier.currency;
	}
	return { pricing: next, currency };
}

/**
 * @private
 * @param {string|null} sku
 * @return {string}
 */
function getPricingCacheKey(sku) {
	if (isString(sku) && sku !== "") {
		return sku;
	}
	return "__base__";
}

/**
 * @private
 * @param {{product: Object, pricing: Object}|null} cached
 */
function applyPricingCache(cached) {
	if (!cached) {
		return;
	}
	this[productDataSymbol] = Object.assign({}, this[productDataSymbol], {
		product: cached.product,
		pricing: cached.pricing,
	});
}

/**
 * @private
 * @param {Object} options
 * @return {boolean}
 */
function isFixedQuantity(options) {
	const min = Number(options.min);
	const max = Number(options.max);
	if (!Number.isFinite(min) || !Number.isFinite(max)) {
		return false;
	}
	return min === 1 && max === 1;
}

/**
 * @private
 * @param {HTMLElement|null} variants
 * @return {boolean}
 */
function hasVariantOptions(variants) {
	if (!(variants instanceof HTMLElement)) {
		return false;
	}
	const data = variants.getOption?.("data") || [];
	const url = variants.getOption?.("url");
	return (
		(Array.isArray(data) && data.length > 0) || (isString(url) && url !== "")
	);
}

/**
 * @private
 * @param {HTMLElement|null} quantityControl
 * @return {number|null}
 */
function getQuantityValue(quantityControl) {
	const useDecimalInput = this.getOption("features.allowDecimal") === true;
	if (useDecimalInput) {
		const raw = this[quantityInputElementSymbol]?.value ?? "";
		const parsed = parseLocaleNumber(raw);
		if (Number.isFinite(parsed)) {
			return parsed;
		}
	}
	const fallback =
		this.getOption("quantity.value") ?? this.getOption("quantity.min") ?? 1;
	if (useDecimalInput) {
		return Number(fallback);
	}
	const value = quantityControl?.value;
	if (Number.isFinite(value)) {
		return value;
	}
	return Number(fallback);
}

/**
 * @private
 * @param {number} value
 * @param {number|null} precision
 * @param {string} rounding
 * @return {number}
 */
function roundQuantity(value, precision, rounding) {
	if (!Number.isFinite(value)) {
		return value;
	}
	if (!Number.isFinite(precision)) {
		return value;
	}
	const factor = 10 ** precision;
	const scaled = value * factor;
	if (rounding === "half-up") {
		return Math.round(scaled) / factor;
	}
	if (rounding === "floor") {
		return Math.floor(scaled) / factor;
	}
	if (rounding === "ceil") {
		return Math.ceil(scaled) / factor;
	}
	return Math.round(scaled) / factor;
}

/**
 * @private
 * @param {string} value
 * @param {string} currency
 * @return {string}
 */
function formatMoney(value, currency) {
	try {
		const locale = getLocaleOfDocument();
		return new Intl.NumberFormat(locale.locale, {
			style: "currency",
			currency,
		}).format(value);
	} catch (_e) {
		return `${value} ${currency}`;
	}
}

/**
 * @private
 * @param {number} value
 * @param {number} precision
 * @return {string}
 */
function formatQuantityForLocale(value, precision) {
	try {
		const locale = getLocaleOfDocument();
		return new Intl.NumberFormat(locale.locale, {
			minimumFractionDigits: precision,
			maximumFractionDigits: precision,
		}).format(value);
	} catch (_e) {
		return `${value}`;
	}
}

/**
 * @private
 * @param {string} value
 * @return {number}
 */
function parseLocaleNumber(value) {
	if (!isString(value)) {
		return Number.NaN;
	}
	const trimmed = value.trim();
	if (trimmed === "") {
		return Number.NaN;
	}
	const locale = getLocaleOfDocument();
	const decimal = getDecimalSeparator(locale.locale);
	const group = decimal === "," ? "." : ",";
	const normalized = trimmed
		.replace(new RegExp(`\\${group}`, "g"), "")
		.replace(new RegExp(`\\${decimal}`), ".");
	return Number(normalized);
}

/**
 * @private
 * @param {string} locale
 * @return {string}
 */
function getDecimalSeparator(locale) {
	try {
		const parts = new Intl.NumberFormat(locale).formatToParts(1.1);
		const separator = parts.find((part) => part.type === "decimal");
		return separator?.value || ".";
	} catch (_e) {
		return ".";
	}
}

/**
 * @private
 */
function updateSubmitLabel() {
	if (!this[submitLabelElementSymbol]) {
		return;
	}
	const label = this.getOption("labels.addToCart") || "Add to cart";
	this[submitLabelElementSymbol].textContent = label;
}

/**
 * @private
 * @param {HTMLElement} element
 * @param {string} text
 */
function setElementText(element, text) {
	element.textContent = text;
	element.hidden = text === "";
}

/**
 * @private
 * @param {string|null} sku
 * @return {Object|null}
 */
function findCartItem(sku) {
	if (!isString(sku) || sku === "") {
		return null;
	}
	const cart = this[cartDataSymbol];
	if (!Array.isArray(cart)) {
		return null;
	}

	const skuTemplate = this.getOption("cart.mapping.skuTemplate");
	const qtyTemplate = this.getOption("cart.mapping.qtyTemplate");

	for (const item of cart) {
		const itemSku =
			mapValueFromSource(item, skuTemplate) ?? item?.sku ?? item?.id ?? null;
		if (itemSku === sku) {
			const qty = parseNumber(
				mapValueFromSource(item, qtyTemplate),
				item?.qty ?? item?.quantity ?? null,
			);
			return { sku: itemSku, qty, source: item };
		}
	}
	return null;
}

/**
 * @private
 */
function checkStockIfNeeded() {
	const options = this.getOption("stock", {});
	const url = options?.url;
	if (!isString(url) || url === "") {
		return;
	}
	if (options?.features?.autoCheck === false) {
		return;
	}

	if (this[stockRequestSymbol]) {
		this[stockRequestSymbol].abort?.();
		this[stockRequestSymbol] = null;
	}

	const controller = new AbortController();
	this[stockRequestSymbol] = controller;
	const sku = this.shadowRoot.querySelector("monster-variant-select")?.value;
	if (!isString(sku) || sku === "") {
		return;
	}
	const query = sku ? `?sku=${encodeURIComponent(sku)}` : "";

	fetch(url + query, {
		...options.fetch,
		signal: controller.signal,
	})
		.then((response) => {
			if (!response.ok) {
				throw new Error("stock check failed");
			}
			return response.json();
		})
		.then((json) => {
			const selector = options?.mapping?.selector || "*";
			let data = json;
			if (selector !== "*" && selector !== "") {
				data = new Pathfinder(json).getVia(selector);
			}
			const stock = mapValueFromSource(data, options?.mapping?.stockTemplate);
			const product = this[productDataSymbol]?.product || {};
			this[productDataSymbol] = Object.assign({}, this[productDataSymbol], {
				product: Object.assign({}, product, {
					stock: parseNumber(stock, product.stock),
				}),
			});
			updateState.call(this);
		})
		.catch((e) => {
			if (e.name === "AbortError") {
				return;
			}
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
		});
}

/**
 * @private
 */
function checkPricingIfNeeded() {
	const api = this.getOption("pricing.api", {});
	const url = api?.url;
	if (!isString(url) || url === "") {
		return;
	}
	if (this[pricingRequestSymbol]) {
		this[pricingRequestSymbol].abort?.();
		this[pricingRequestSymbol] = null;
	}
	const variants = this.shadowRoot.querySelector("monster-variant-select");
	const quantity = this.shadowRoot.querySelector("monster-quantity");
	const hasVariants = hasVariantOptions.call(this, variants);
	const sku = hasVariants ? variants?.value : this.getOption("product.sku");
	const qty = getQuantityValue.call(this, quantity);
	const selection = variants?.getOption?.("selection") || {};
	if (hasVariants && (!isString(sku) || sku === "")) {
		return;
	}
	const alwaysFetch = api?.features?.alwaysFetch === true;
	const cacheKey = getPricingCacheKey.call(this, sku);
	if (!alwaysFetch && cacheKey && this[pricingCacheSymbol]?.has(cacheKey)) {
		const cached = this[pricingCacheSymbol].get(cacheKey);
		applyPricingCache.call(this, cached);
		updateState.call(this);
		return;
	}
	const requestData = stringifyForTemplate({
		sku,
		qty: Number.isFinite(qty) ? qty : "",
		selection,
	});
	const requestUrl = url.includes("${")
		? new Formatter(requestData).format(url)
		: url;
	const controller = new AbortController();
	this[pricingRequestSymbol] = controller;

	fetch(requestUrl, { ...(api?.fetch || {}), signal: controller.signal })
		.then((response) => {
			if (!response.ok) {
				throw new Error("pricing fetch failed");
			}
			return response.json();
		})
		.then((json) => {
			const selector = api?.mapping?.selector || "*";
			let data = json;
			if (selector !== "*" && selector !== "") {
				data = new Pathfinder(json).getVia(selector);
			}
			const price = mapValueFromSource(data, api?.mapping?.priceTemplate);
			const listPrice = mapValueFromSource(
				data,
				api?.mapping?.listPriceTemplate,
			);
			const basePrice = mapValueFromSource(
				data,
				api?.mapping?.basePriceTemplate,
			);
			const baseUnit = mapValueFromSource(
				data,
				api?.mapping?.basePriceUnitTemplate,
			);
			const tiersPath = api?.mapping?.tiersPath;
			let tiers = null;
			if (tiersPath) {
				try {
					tiers = new Pathfinder(data).getVia(tiersPath);
				} catch (_e) {
					tiers = null;
				}
			}
			const taxRate = mapValueFromSource(data, api?.mapping?.taxRateTemplate);
			const taxIncluded = mapValueFromSource(
				data,
				api?.mapping?.taxIncludedTemplate,
			);

			const current = this[productDataSymbol]?.product || {};
			const pricing = Object.assign({}, this[productDataSymbol]?.pricing, {
				price: parseNumber(price, this.getOption("pricing.price")),
				listPrice: parseNumber(listPrice, this.getOption("pricing.listPrice")),
				basePrice: parseNumber(basePrice, this.getOption("pricing.basePrice")),
				basePriceUnit: baseUnit ?? this.getOption("pricing.basePriceUnit"),
				tiers: Array.isArray(tiers) ? tiers : this.getOption("pricing.tiers"),
			});

			const product = Object.assign({}, current, {
				taxRate: parseNumber(taxRate, current.taxRate),
				taxIncluded: parseBoolean(taxIncluded, current.taxIncluded),
			});

			const cacheEntry = { product, pricing };
			if (cacheKey) {
				this[pricingCacheSymbol]?.set(cacheKey, cacheEntry);
			}
			applyPricingCache.call(this, cacheEntry);
			updateState.call(this);
		})
		.catch((e) => {
			if (e.name === "AbortError") {
				return;
			}
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, `${e}`);
		});
}

/**
 * @private
 * @param {string|null} value
 * @param {boolean} fallback
 * @return {boolean}
 */
function parseBoolean(value, fallback) {
	if (value === null || value === undefined) {
		return fallback;
	}
	if (typeof value === "boolean") {
		return value;
	}
	if (isString(value)) {
		return value.toLowerCase() === "true";
	}
	return fallback;
}

/**
 * @private
 * @param {string|null} value
 * @param {number|null} fallback
 * @return {number|null}
 */
function parseNumber(value, fallback) {
	if (value === null || value === undefined) {
		return fallback;
	}
	const num = Number(value);
	return Number.isFinite(num) ? num : fallback;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <div data-monster-role="header" part="header">
                <slot name="title"></slot>
            </div>
            <div data-monster-role="body" part="body">
                <div data-monster-role="variants" part="variants">
                    <monster-variant-select part="variants-control"></monster-variant-select>
                </div>
                <div data-monster-role="quantity" part="quantity">
                    <div data-monster-role="quantity-label" part="quantity-label">
                        <span data-monster-role="quantity-label-text" part="quantity-label-text"
                              data-monster-replace="path:labels.quantity"></span>
                        <span data-monster-role="quantity-unit" part="quantity-unit"></span>
                    </div>
                    <monster-quantity part="quantity-control"></monster-quantity>
                    <input data-monster-role="quantity-input" part="quantity-input"
                           type="text" inputmode="decimal" />
                    <div data-monster-role="quantity-static" part="quantity-static"></div>
                </div>
                <div data-monster-role="prices" part="prices">
                    <div data-monster-role="list-price" part="list-price"></div>
                    <div data-monster-role="price" part="price"></div>
                    <div data-monster-role="sum" part="sum"></div>
                    <div data-monster-role="base-price" part="base-price"></div>
                    <div data-monster-role="tax" part="tax"></div>
                    <div data-monster-role="total-label" part="total-label"
                         data-monster-replace="path:labels.total"></div>
                    <div data-monster-role="total" part="total"></div>
                </div>
                <div data-monster-role="delivery" part="delivery"></div>
                <div data-monster-role="message" part="message"></div>
                <div data-monster-role="actions" part="actions">
                    <monster-message-state-button data-monster-role="submit" part="submit">
                        <span data-monster-role="submit-label" part="submit-label"></span>
                    </monster-message-state-button>
                </div>
            </div>
            <div data-monster-role="slots" part="slots">
                <slot name="info"></slot>
                <slot name="shipping"></slot>
                <slot name="footer"></slot>
            </div>
        </div>
    `;
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				addToCart: "In den Warenkorb",
				addToCartAria: "In den Warenkorb",
				quantity: "Menge",
				total: "Summe",
				variantsAria: "Varianten",
				quantityAria: "Menge",
				selectVariant: "Bitte eine Variante waehlen.",
				selectQuantity: "Bitte Menge waehlen.",
				priceUnavailable: "Preis nicht verfuegbar.",
				invalidQuantity: "Menge ist nicht erlaubt.",
				outOfStock: "Nicht genuegend Lagerbestand.",
				addToCartError: "Hinzufuegen fehlgeschlagen.",
				addToCartNotConfigured: "Warenkorb ist nicht konfiguriert.",
				taxIncluded: "inkl. MwSt.",
				taxExcluded: "zzgl. MwSt.",
				deliveryUnknown: "Lieferzeit unbekannt",
			};
		case "es":
			return {
				addToCart: "Anadir al carrito",
				addToCartAria: "Anadir al carrito",
				quantity: "Cantidad",
				total: "Total",
				variantsAria: "Variantes",
				quantityAria: "Cantidad",
				selectVariant: "Seleccione una variante.",
				selectQuantity: "Seleccione una cantidad.",
				priceUnavailable: "Precio no disponible.",
				invalidQuantity: "Cantidad no permitida.",
				outOfStock: "Stock insuficiente.",
				addToCartError: "Error al anadir.",
				addToCartNotConfigured: "Carrito no configurado.",
				taxIncluded: "incl. impuestos",
				taxExcluded: "excl. impuestos",
				deliveryUnknown: "Plazo desconocido",
			};
		case "zh":
			return {
				addToCart: "加入购物车",
				addToCartAria: "加入购物车",
				quantity: "数量",
				total: "总计",
				variantsAria: "变体选择",
				quantityAria: "数量",
				selectVariant: "请选择一个变体。",
				selectQuantity: "请选择数量。",
				priceUnavailable: "价格不可用。",
				invalidQuantity: "数量无效。",
				outOfStock: "库存不足。",
				addToCartError: "添加失败。",
				addToCartNotConfigured: "购物车未配置。",
				taxIncluded: "含税",
				taxExcluded: "不含税",
				deliveryUnknown: "交付时间未知",
			};
		case "hi":
			return {
				addToCart: "कार्ट में जोड़ें",
				addToCartAria: "कार्ट में जोड़ें",
				quantity: "मात्रा",
				total: "कुल",
				variantsAria: "विकल्प",
				quantityAria: "मात्रा",
				selectVariant: "कृपया एक विकल्प चुनें।",
				selectQuantity: "कृपया मात्रा चुनें।",
				priceUnavailable: "मूल्य उपलब्ध नहीं।",
				invalidQuantity: "मात्रा अनुमत नहीं है।",
				outOfStock: "स्टॉक पर्याप्त नहीं है।",
				addToCartError: "जोड़ने में त्रुटि।",
				addToCartNotConfigured: "कार्ट कॉन्फ़िगर नहीं है।",
				taxIncluded: "कर सहित",
				taxExcluded: "कर रहित",
				deliveryUnknown: "डिलीवरी समय अज्ञात",
			};
		case "bn":
			return {
				addToCart: "কার্টে যোগ করুন",
				addToCartAria: "কার্টে যোগ করুন",
				quantity: "পরিমাণ",
				total: "মোট",
				variantsAria: "ভ্যারিয়েন্ট",
				quantityAria: "পরিমাণ",
				selectVariant: "দয়া করে একটি ভ্যারিয়েন্ট বেছে নিন।",
				selectQuantity: "দয়া করে পরিমাণ বেছে নিন।",
				priceUnavailable: "মূল্য পাওয়া যাচ্ছে না।",
				invalidQuantity: "পরিমাণ অনুমোদিত নয়।",
				outOfStock: "স্টক পর্যাপ্ত নয়।",
				addToCartError: "যোগ করতে ব্যর্থ।",
				addToCartNotConfigured: "কার্ট কনফিগার করা হয়নি।",
				taxIncluded: "করসহ",
				taxExcluded: "কর ছাড়া",
				deliveryUnknown: "ডেলিভারি সময় অজানা",
			};
		case "pt":
			return {
				addToCart: "Adicionar ao carrinho",
				addToCartAria: "Adicionar ao carrinho",
				quantity: "Quantidade",
				total: "Total",
				variantsAria: "Variantes",
				quantityAria: "Quantidade",
				selectVariant: "Escolha uma variante.",
				selectQuantity: "Escolha uma quantidade.",
				priceUnavailable: "Preco indisponivel.",
				invalidQuantity: "Quantidade nao permitida.",
				outOfStock: "Estoque insuficiente.",
				addToCartError: "Falha ao adicionar.",
				addToCartNotConfigured: "Carrinho nao configurado.",
				taxIncluded: "incl. imposto",
				taxExcluded: "excl. imposto",
				deliveryUnknown: "Prazo desconhecido",
			};
		case "ru":
			return {
				addToCart: "V korzinu",
				addToCartAria: "V korzinu",
				quantity: "Kolichestvo",
				total: "Itogo",
				variantsAria: "Varianty",
				quantityAria: "Kolichestvo",
				selectVariant: "Vyberite variant.",
				selectQuantity: "Vyberite kolichestvo.",
				priceUnavailable: "Tsena nedostupna.",
				invalidQuantity: "Nedopustimoe kol-vo.",
				outOfStock: "Nedostatochno na sklade.",
				addToCartError: "Ne udalos dobavit.",
				addToCartNotConfigured: "Korzina ne nastroena.",
				taxIncluded: "s NDS",
				taxExcluded: "bez NDS",
				deliveryUnknown: "Srok neizvesten",
			};
		case "ja":
			return {
				addToCart: "カートに追加",
				addToCartAria: "カートに追加",
				quantity: "数量",
				total: "合計",
				variantsAria: "バリエーション",
				quantityAria: "数量",
				selectVariant: "バリエーションを選択してください。",
				selectQuantity: "数量を選択してください。",
				priceUnavailable: "価格が利用できません。",
				invalidQuantity: "数量が無効です。",
				outOfStock: "在庫不足。",
				addToCartError: "追加に失敗。",
				addToCartNotConfigured: "カートが未設定です。",
				taxIncluded: "税込",
				taxExcluded: "税別",
				deliveryUnknown: "配送情報なし",
			};
		case "pa":
			return {
				addToCart: "ਕਾਰਟ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ",
				addToCartAria: "ਕਾਰਟ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ",
				quantity: "ਮਾਤਰਾ",
				total: "ਕੁੱਲ",
				variantsAria: "ਵੈਰੀਐਂਟ",
				quantityAria: "ਮਾਤਰਾ",
				selectVariant: "ਕਿਰਪਾ ਕਰਕੇ ਇਕ ਵੈਰੀਐਂਟ ਚੁਣੋ।",
				selectQuantity: "ਕਿਰਪਾ ਕਰਕੇ ਮਾਤਰਾ ਚੁਣੋ।",
				priceUnavailable: "ਕੀਮਤ ਉਪਲਬਧ ਨਹੀਂ।",
				invalidQuantity: "ਮਾਤਰਾ ਮਨਜ਼ੂਰ ਨਹੀਂ।",
				outOfStock: "ਸਟਾਕ ਘੱਟ ਹੈ।",
				addToCartError: "ਜੋੜਨਾ ਅਸਫਲ।",
				addToCartNotConfigured: "ਕਾਰਟ ਕਨਫਿਗਰ ਨਹੀਂ ਹੈ।",
				taxIncluded: "ਟੈਕਸ ਸਮੇਤ",
				taxExcluded: "ਟੈਕਸ ਤੋਂ ਬਿਨਾਂ",
				deliveryUnknown: "ਡਿਲਿਵਰੀ ਸਮਾਂ ਅਣਜਾਣ",
			};
		case "mr":
			return {
				addToCart: "कार्टमध्ये जोडा",
				addToCartAria: "कार्टमध्ये जोडा",
				quantity: "प्रमाण",
				total: "एकूण",
				variantsAria: "पर्याय",
				quantityAria: "प्रमाण",
				selectVariant: "कृपया एक पर्याय निवडा.",
				selectQuantity: "कृपया प्रमाण निवडा.",
				priceUnavailable: "किंमत उपलब्ध नाही.",
				invalidQuantity: "प्रमाण अनुमती नाही.",
				outOfStock: "साठा अपुरा आहे.",
				addToCartError: "जोडण्यात त्रुटी.",
				addToCartNotConfigured: "कार्ट संरचित नाही.",
				taxIncluded: "करासह",
				taxExcluded: "कराशिवाय",
				deliveryUnknown: "डिलिव्हरी वेळ अज्ञात",
			};
		case "fr":
			return {
				addToCart: "Ajouter au panier",
				addToCartAria: "Ajouter au panier",
				quantity: "Quantite",
				total: "Total",
				variantsAria: "Variantes",
				quantityAria: "Quantite",
				selectVariant: "Veuillez choisir une variante.",
				selectQuantity: "Veuillez choisir une quantite.",
				priceUnavailable: "Prix indisponible.",
				invalidQuantity: "Quantite non autorisee.",
				outOfStock: "Stock insuffisant.",
				addToCartError: "Ajout echoue.",
				addToCartNotConfigured: "Panier non configure.",
				taxIncluded: "TTC",
				taxExcluded: "HT",
				deliveryUnknown: "Delai inconnu",
			};
		case "it":
			return {
				addToCart: "Aggiungi al carrello",
				addToCartAria: "Aggiungi al carrello",
				quantity: "Quantita",
				total: "Totale",
				variantsAria: "Varianti",
				quantityAria: "Quantita",
				selectVariant: "Seleziona una variante.",
				selectQuantity: "Seleziona una quantita.",
				priceUnavailable: "Prezzo non disponibile.",
				invalidQuantity: "Quantita non consentita.",
				outOfStock: "Scorte insufficienti.",
				addToCartError: "Aggiunta fallita.",
				addToCartNotConfigured: "Carrello non configurato.",
				taxIncluded: "IVA inclusa",
				taxExcluded: "IVA esclusa",
				deliveryUnknown: "Tempi sconosciuti",
			};
		case "nl":
			return {
				addToCart: "In winkelwagen",
				addToCartAria: "In winkelwagen",
				quantity: "Aantal",
				total: "Totaal",
				variantsAria: "Varianten",
				quantityAria: "Aantal",
				selectVariant: "Kies een variant.",
				selectQuantity: "Kies een hoeveelheid.",
				priceUnavailable: "Prijs niet beschikbaar.",
				invalidQuantity: "Hoeveelheid niet toegestaan.",
				outOfStock: "Onvoldoende voorraad.",
				addToCartError: "Toevoegen mislukt.",
				addToCartNotConfigured: "Winkelwagen niet geconfigureerd.",
				taxIncluded: "incl. btw",
				taxExcluded: "excl. btw",
				deliveryUnknown: "Levertijd onbekend",
			};
		case "sv":
			return {
				addToCart: "Lag i kundvagn",
				addToCartAria: "Lag i kundvagn",
				quantity: "Antal",
				total: "Summa",
				variantsAria: "Varianter",
				quantityAria: "Antal",
				selectVariant: "Valj en variant.",
				selectQuantity: "Valj antal.",
				priceUnavailable: "Pris ej tillgangligt.",
				invalidQuantity: "Antal ej tillatet.",
				outOfStock: "Otillrackligt lager.",
				addToCartError: "Kunde inte lagga till.",
				addToCartNotConfigured: "Kundvagn ej konfigurerad.",
				taxIncluded: "inkl. moms",
				taxExcluded: "exkl. moms",
				deliveryUnknown: "Leverans okand",
			};
		case "pl":
			return {
				addToCart: "Dodaj do koszyka",
				addToCartAria: "Dodaj do koszyka",
				quantity: "Ilosc",
				total: "Suma",
				variantsAria: "Warianty",
				quantityAria: "Ilosc",
				selectVariant: "Wybierz wariant.",
				selectQuantity: "Wybierz ilosc.",
				priceUnavailable: "Cena niedostepna.",
				invalidQuantity: "Niepoprawna ilosc.",
				outOfStock: "Brak na stanie.",
				addToCartError: "Nie udalo sie dodac.",
				addToCartNotConfigured: "Koszyk nie skonfigurowany.",
				taxIncluded: "z VAT",
				taxExcluded: "bez VAT",
				deliveryUnknown: "Czas dostawy nieznany",
			};
		case "da":
			return {
				addToCart: "Laeg i kurv",
				addToCartAria: "Laeg i kurv",
				quantity: "Maengde",
				total: "Total",
				variantsAria: "Varianter",
				quantityAria: "Maengde",
				selectVariant: "Vaelg en variant.",
				selectQuantity: "Vaelg maengde.",
				priceUnavailable: "Pris ikke tilgaengelig.",
				invalidQuantity: "Maengde ikke tilladt.",
				outOfStock: "Ikke nok lager.",
				addToCartError: "Kunne ikke tilfoeje.",
				addToCartNotConfigured: "Kurv ikke konfigureret.",
				taxIncluded: "inkl. moms",
				taxExcluded: "ekskl. moms",
				deliveryUnknown: "Leveringstid ukendt",
			};
		case "fi":
			return {
				addToCart: "Lisaa koriin",
				addToCartAria: "Lisaa koriin",
				quantity: "Maara",
				total: "Yhteensa",
				variantsAria: "Vaihtoehdot",
				quantityAria: "Maara",
				selectVariant: "Valitse vaihtoehto.",
				selectQuantity: "Valitse maara.",
				priceUnavailable: "Hinta ei saatavilla.",
				invalidQuantity: "Maara ei sallittu.",
				outOfStock: "Varasto ei riita.",
				addToCartError: "Lisaaminen epaonnistui.",
				addToCartNotConfigured: "Ostoskori ei ole konfiguroitu.",
				taxIncluded: "sis. alv",
				taxExcluded: "ei sis. alv",
				deliveryUnknown: "Toimitusaika tuntematon",
			};
		case "no":
			return {
				addToCart: "Legg i handlekurv",
				addToCartAria: "Legg i handlekurv",
				quantity: "Antall",
				total: "Sum",
				variantsAria: "Varianter",
				quantityAria: "Antall",
				selectVariant: "Velg en variant.",
				selectQuantity: "Velg antall.",
				priceUnavailable: "Pris ikke tilgjengelig.",
				invalidQuantity: "Mengde ikke tillatt.",
				outOfStock: "Ikke nok lager.",
				addToCartError: "Kunne ikke legge til.",
				addToCartNotConfigured: "Handlekurv ikke konfigurert.",
				taxIncluded: "inkl. mva",
				taxExcluded: "ekskl. mva",
				deliveryUnknown: "Leveringstid ukjent",
			};
		case "cs":
			return {
				addToCart: "Pridat do kosiku",
				addToCartAria: "Pridat do kosiku",
				quantity: "Mnozstvi",
				total: "Celkem",
				variantsAria: "Varianty",
				quantityAria: "Mnozstvi",
				selectVariant: "Vyberte variantu.",
				selectQuantity: "Vyberte mnozstvi.",
				priceUnavailable: "Cena neni dostupna.",
				invalidQuantity: "Neplatne mnozstvi.",
				outOfStock: "Nedostatek na sklade.",
				addToCartError: "Nelze pridat.",
				addToCartNotConfigured: "Kosik neni nakonfigurovan.",
				taxIncluded: "vc. DPH",
				taxExcluded: "bez DPH",
				deliveryUnknown: "Dodaci lhuta neznama",
			};
		default:
			return {
				addToCart: "Add to cart",
				addToCartAria: "Add to cart",
				quantity: "Quantity",
				total: "Total",
				variantsAria: "Variants",
				quantityAria: "Quantity",
				selectVariant: "Please select a variant.",
				selectQuantity: "Please choose a quantity.",
				priceUnavailable: "Price unavailable.",
				invalidQuantity: "Quantity is not allowed.",
				outOfStock: "Not enough stock.",
				addToCartError: "Failed to add to cart.",
				addToCartNotConfigured: "Cart is not configured.",
				taxIncluded: "incl. tax",
				taxExcluded: "excl. tax",
				deliveryUnknown: "Delivery unknown",
			};
	}
}

registerCustomElement(BuyBox);
