/**
 * 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 { 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 { CartControlStyleSheet } from "./stylesheet/cart-control.mjs";
import "../datatable/datasource/rest.mjs";

export { CartControl };

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

/**
 * @private
 * @type {symbol}
 */
const countElementSymbol = Symbol("countElement");

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

/**
 * @private
 * @type {symbol}
 */
const statusElementSymbol = Symbol("statusElement");

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

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

/**
 * @private
 * @type {symbol}
 */
const pendingPayloadSymbol = Symbol("pendingPayload");

/**
 * @private
 * @type {symbol}
 */
const pendingRequestSymbol = Symbol("pendingRequest");

/**
 * CartControl
 *
 * @summary Cart control that syncs with a datasource and exposes pending vs verified state.
 * @fires monster-cart-control-pending
 * @fires monster-cart-control-verified
 * @fires monster-cart-control-error
 * @fires monster-cart-control-update
 */
class CartControl extends CustomControl {
	static get [instanceSymbol]() {
		return Symbol.for(
			"@schukai/monster/components/form/cart-control@@instance",
		);
	}

	/**
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {Object} labels Labels
	 * @property {Object} datasource Datasource configuration
	 * @property {string} datasource.selector Selector for datasource
	 * @property {Object} datasource.rest Rest datasource config
	 * @property {Object} cart Cart write configuration
	 * @property {Object} mapping Response mapping
	 * @property {Object} state Control state
	 * @property {Object} actions Callback actions
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: getTranslations(),
			datasource: {
				selector: null,
				rest: null,
			},
			cart: {
				url: null,
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				bodyTemplate: null,
			},
			mapping: {
				selector: "*",
				itemsPath: "items",
				totalTemplate: null,
				currencyTemplate: null,
				qtyTemplate: "qty",
			},
			state: {
				status: "idle",
			},
			actions: {
				onpending: null,
				onverified: null,
				onerror: null,
				onupdate: null,
			},
		});
	}

	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initDatasource.call(this);
		applyData.call(this);
		setStatus.call(this, this.getOption("state.status") || "idle");
		return this;
	}

	/**
	 * Re-apply mapped data from options or datasource.
	 * @return {CartControl}
	 */
	refresh() {
		applyData.call(this);
		return this;
	}

	static getTag() {
		return "monster-cart-control";
	}

	static getCSSStyleSheet() {
		return [CommonStyleSheet, FormStyleSheet, CartControlStyleSheet];
	}

	/**
	 * Add or change an item in the cart.
	 * @param {Object} payload
	 */
	addOrChange(payload) {
		const request = buildPayload.call(this, payload);
		this[pendingPayloadSymbol] = payload;
		this[pendingRequestSymbol] = request;
		setStatus.call(this, "pending");
		fireCustomEvent(this, "monster-cart-control-pending", {
			payload: request,
			source: payload,
		});
		const action = this.getOption("actions.onpending");
		if (isFunction(action)) {
			action.call(this, { payload: request, source: payload });
		}

		if (!this[datasourceElementSymbol]) {
			initDatasource.call(this);
		}

		if (!this[datasourceElementSymbol]) {
			handleError.call(this, new Error("cart datasource not configured"));
			return;
		}

		this[datasourceElementSymbol].data = request;
		this[datasourceElementSymbol].write().catch((e) => {
			handleError.call(this, e);
		});
	}
}

/**
 * @private
 */
function initControlReferences() {
	this[controlElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=control]",
	);
	this[countElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=count]",
	);
	this[totalElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=total]",
	);
	this[statusElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=status]",
	);
}

/**
 * @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("write", this.getOption("datasource.rest.write", {}));
		rest.setOption("features.autoInit", false);
		this.appendChild(rest);
		this[datasourceElementSymbol] = rest;
	} else if (isString(this.getOption("cart.url"))) {
		const rest = getDocument().createElement("monster-datasource-rest");
		rest.setOption("write.url", this.getOption("cart.url"));
		rest.setOption("write.method", this.getOption("cart.method"));
		rest.setOption("write.init", {
			method: this.getOption("cart.method"),
			headers: this.getOption("cart.headers"),
		});
		rest.setOption("features.autoInit", false);
		this.appendChild(rest);
		this[datasourceElementSymbol] = rest;
	}

	if (this[datasourceElementSymbol]) {
		this[datasourceElementSymbol].setOption(
			"write.responseCallback",
			(payload) => {
				this[datasourceElementSymbol].data = payload;
			},
		);
		this[datasourceElementSymbol].addEventListener(
			"monster-datasource-fetched",
			(event) => {
				const data = event?.detail?.data || this[datasourceElementSymbol]?.data;
				if (data) {
					this[cartDataSymbol] = data;
					applyData.call(this);
				}
				setStatus.call(this, "verified");
				fireCustomEvent(this, "monster-cart-control-verified", {
					data,
					payload: this[pendingRequestSymbol],
					source: this[pendingPayloadSymbol],
				});
				const action = this.getOption("actions.onverified");
				if (isFunction(action)) {
					action.call(this, {
						data,
						payload: this[pendingRequestSymbol],
						source: this[pendingPayloadSymbol],
					});
				}
				this[pendingPayloadSymbol] = null;
				this[pendingRequestSymbol] = null;
			},
		);
		this[datasourceElementSymbol].addEventListener(
			"monster-datasource-error",
			(event) => {
				handleError.call(this, event?.detail?.error);
			},
		);
	}
}

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

	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 itemsPath = mapping.itemsPath;
	const items = itemsPath
		? new Pathfinder(source).getVia(itemsPath)
		: source?.items;
	const count = Array.isArray(items)
		? items.reduce((sum, item) => {
				const qty = readNumber(item, mapping.qtyTemplate) ?? 0;
				return sum + qty;
			}, 0)
		: 0;
	const total = readNumber(source, mapping.totalTemplate);
	const currency = readString(source, mapping.currencyTemplate) || "EUR";

	if (this[countElementSymbol]) {
		setElementText(
			this[countElementSymbol],
			formatCount(count, this.getOption("labels.items")),
		);
	}
	if (this[totalElementSymbol]) {
		setElementText(
			this[totalElementSymbol],
			total !== null ? formatMoney(total, currency) : "",
		);
	}

	fireCustomEvent(this, "monster-cart-control-update", {
		count,
		total,
		currency,
		items,
	});
	const action = this.getOption("actions.onupdate");
	if (isFunction(action)) {
		action.call(this, { count, total, currency, items });
	}
}

/**
 * @private
 * @param {string} status
 */
function setStatus(status) {
	this.setOption("state.status", status);
	if (!this[statusElementSymbol]) {
		return;
	}
	let label = "";
	if (status === "pending") {
		label = this.getOption("labels.statusPending");
	} else if (status === "verified") {
		label = this.getOption("labels.statusVerified");
	} else if (status === "error") {
		label = this.getOption("labels.statusError");
	} else {
		label = this.getOption("labels.statusIdle");
	}
	setElementText(this[statusElementSymbol], label);
}

/**
 * @private
 * @param {Error} error
 */
function handleError(error) {
	setStatus.call(this, "error");
	fireCustomEvent(this, "monster-cart-control-error", {
		error,
		payload: this[pendingRequestSymbol],
		source: this[pendingPayloadSymbol],
	});
	const action = this.getOption("actions.onerror");
	if (isFunction(action)) {
		action.call(this, {
			error,
			payload: this[pendingRequestSymbol],
			source: this[pendingPayloadSymbol],
		});
	}
	this[pendingPayloadSymbol] = null;
	this[pendingRequestSymbol] = null;
}

/**
 * @private
 * @param {Object} payload
 * @return {Object}
 */
function buildPayload(payload) {
	if (!payload) {
		return {};
	}
	const data = Object.assign({}, payload);
	const template = this.getOption("cart.bodyTemplate");
	if (isString(template) && template.includes("${")) {
		const formatted = new Formatter(stringifyForTemplate(data)).format(
			template,
		);
		try {
			return JSON.parse(formatted);
		} catch (_e) {
			return { value: formatted };
		}
	}
	return data;
}

/**
 * @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 {Object} source
 * @param {string|null} template
 * @return {string|null}
 */
function readString(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} source
 * @param {string|null} template
 * @return {number|null}
 */
function readNumber(source, template) {
	const value = readString(source, template);
	if (value === null || value === undefined) {
		return null;
	}
	const num = Number(value);
	return Number.isFinite(num) ? num : null;
}

/**
 * @private
 * @param {number} value
 * @param {string} label
 * @return {string}
 */
function formatCount(value, label) {
	const safeValue = Number.isFinite(value) ? value : 0;
	return `${safeValue} ${label}`;
}

/**
 * @private
 * @param {number} 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 {HTMLElement} element
 * @param {string} text
 */
function setElementText(element, text) {
	element.textContent = text;
	element.hidden = text === "";
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <div data-monster-role="summary" part="summary">
                <div data-monster-role="count" part="count"></div>
                <div data-monster-role="total" part="total"></div>
            </div>
            <div data-monster-role="status" part="status"></div>
        </div>
    `;
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				items: "Artikel",
				statusIdle: "Warenkorb bereit",
				statusPending: "Warenkorb wird aktualisiert",
				statusVerified: "Warenkorb bestaetigt",
				statusError: "Warenkorb Fehler",
			};
		case "es":
			return {
				items: "Articulos",
				statusIdle: "Carrito listo",
				statusPending: "Carrito actualizando",
				statusVerified: "Carrito verificado",
				statusError: "Error del carrito",
			};
		case "zh":
			return {
				items: "商品",
				statusIdle: "购物车已就绪",
				statusPending: "购物车更新中",
				statusVerified: "购物车已确认",
				statusError: "购物车错误",
			};
		case "hi":
			return {
				items: "आइटम",
				statusIdle: "कार्ट तैयार",
				statusPending: "कार्ट अपडेट हो रहा है",
				statusVerified: "कार्ट सत्यापित",
				statusError: "कार्ट त्रुटि",
			};
		case "bn":
			return {
				items: "আইটেম",
				statusIdle: "কার্ট প্রস্তুত",
				statusPending: "কার্ট আপডেট হচ্ছে",
				statusVerified: "কার্ট নিশ্চিত",
				statusError: "কার্ট ত্রুটি",
			};
		case "pt":
			return {
				items: "Itens",
				statusIdle: "Carrinho pronto",
				statusPending: "Carrinho atualizando",
				statusVerified: "Carrinho verificado",
				statusError: "Erro no carrinho",
			};
		case "ru":
			return {
				items: "Tovary",
				statusIdle: "Korzina gotova",
				statusPending: "Korzina obnovlyaetsya",
				statusVerified: "Korzina podtverzhdena",
				statusError: "Oshibka korziny",
			};
		case "ja":
			return {
				items: "商品",
				statusIdle: "カート準備完了",
				statusPending: "カート更新中",
				statusVerified: "カート確認済み",
				statusError: "カートエラー",
			};
		case "pa":
			return {
				items: "ਆਈਟਮ",
				statusIdle: "ਕਾਰਟ ਤਿਆਰ",
				statusPending: "ਕਾਰਟ ਅੱਪਡੇਟ ਹੋ ਰਿਹਾ ਹੈ",
				statusVerified: "ਕਾਰਟ ਪੁਸ਼ਟੀਤ",
				statusError: "ਕਾਰਟ ਗਲਤੀ",
			};
		case "mr":
			return {
				items: "आयटम",
				statusIdle: "कार्ट तयार",
				statusPending: "कार्ट अद्ययावत होत आहे",
				statusVerified: "कार्ट सत्यापित",
				statusError: "कार्ट त्रुटी",
			};
		case "fr":
			return {
				items: "Articles",
				statusIdle: "Panier pret",
				statusPending: "Panier en mise a jour",
				statusVerified: "Panier verifie",
				statusError: "Erreur du panier",
			};
		case "it":
			return {
				items: "Articoli",
				statusIdle: "Carrello pronto",
				statusPending: "Carrello in aggiornamento",
				statusVerified: "Carrello verificato",
				statusError: "Errore carrello",
			};
		case "nl":
			return {
				items: "Artikelen",
				statusIdle: "Winkelwagen klaar",
				statusPending: "Winkelwagen bijwerken",
				statusVerified: "Winkelwagen bevestigd",
				statusError: "Winkelwagen fout",
			};
		case "sv":
			return {
				items: "Artiklar",
				statusIdle: "Kundvagn redo",
				statusPending: "Kundvagn uppdateras",
				statusVerified: "Kundvagn verifierad",
				statusError: "Kundvagn fel",
			};
		case "pl":
			return {
				items: "Produkty",
				statusIdle: "Koszyk gotowy",
				statusPending: "Koszyk aktualizowany",
				statusVerified: "Koszyk potwierdzony",
				statusError: "Blad koszyka",
			};
		case "da":
			return {
				items: "Varer",
				statusIdle: "Kurv klar",
				statusPending: "Kurv opdateres",
				statusVerified: "Kurv bekraeftet",
				statusError: "Kurv fejl",
			};
		case "fi":
			return {
				items: "Tuotteet",
				statusIdle: "Ostoskori valmis",
				statusPending: "Ostoskoria paivitetaan",
				statusVerified: "Ostoskori vahvistettu",
				statusError: "Ostoskorin virhe",
			};
		case "no":
			return {
				items: "Varer",
				statusIdle: "Handlekurv klar",
				statusPending: "Handlekurv oppdateres",
				statusVerified: "Handlekurv bekreftet",
				statusError: "Handlekurv feil",
			};
		case "cs":
			return {
				items: "Polozky",
				statusIdle: "Kosik pripraven",
				statusPending: "Kosik se aktualizuje",
				statusVerified: "Kosik potvrzen",
				statusError: "Chyba kosiku",
			};
		default:
			return {
				items: "Items",
				statusIdle: "Cart ready",
				statusPending: "Cart updating",
				statusVerified: "Cart verified",
				statusError: "Cart error",
			};
	}
}

registerCustomElement(CartControl);
