/**
 * 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,
	ATTRIBUTE_ROLE,
	ATTRIBUTE_TEMPLATE_PREFIX,
	ATTRIBUTE_UPDATER_INSERT,
} from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { getDocumentTheme } from "../../dom/theme.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { datasourceLinkedElementSymbol } from "../datatable/util.mjs";
import { Observer } from "../../types/observer.mjs";
import { isArray, isNumber, isObject, isString } from "../../types/is.mjs";
import { ID } from "../../types/id.mjs";
import { clone } from "../../util/clone.mjs";
import { FieldSetStyleSheet } from "./stylesheet/field-set.mjs";
import { RepeatFieldSetStyleSheet } from "./stylesheet/repeat-field-set.mjs";
import { RepeatFieldSetItemsStyleSheet } from "./stylesheet/repeat-field-set-items.mjs";

export { RepeatFieldSet };

/**
 * @private
 * @type {symbol}
 */
const addButtonSymbol = Symbol("addButton");

/**
 * @private
 * @type {symbol}
 */
const removeButtonSymbol = Symbol("removeButton");

/**
 * @private
 * @type {symbol}
 */
const itemSlotSymbol = Symbol("itemSlot");

/**
 * @private
 * @type {symbol}
 */
const itemsContainerSymbol = Symbol("itemsContainer");
const itemDefaultsSymbol = Symbol("itemDefaults");

/**
 * A repeatable field-set control that manages array data.
 *
 * @fragments /fragments/components/form/repeat-field-set/
 * @example /examples/components/form/repeat-field-set-simple Repeatable contacts
 *
 * @since 3.78.0
 * @summary A repeatable field-set control
 * @fires monster-repeat-add
 * @fires monster-repeat-remove
 * @fires monster-repeat-change
 */
class RepeatFieldSet extends CustomControl {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for(
			"@schukai/monster/components/form/repeat-field-set@@instance",
		);
	}

	/**
	 *
	 * @return {RepeatFieldSet}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		ensureItemsStyleSheet.call(this);
		initTemplateBridge.call(this);
		syncOptionData.call(this);
		initEventHandler.call(this);
		initObservers.call(this);
		updateColumns.call(this);
		normalizeOnLoad.call(this);
		syncButtons.call(this);
		return this;
	}

	/**
	 * 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 Label definitions
	 * @property {string} labels.add Add label
	 * @property {string} labels.remove Remove label
	 * @property {Object} classes Class definitions
	 * @property {string} classes.content Content class
	 * @property {string} path Data path relative to the form record
	 * @property {number|null} min Minimum items
	 * @property {number|null} max Maximum items
	 * @property {boolean} disabled Disabled state
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: getTranslations(),
			classes: {
				content: "collapse-alignment-no-padding",
			},
			features: {
				multipleColumns: true,
			},
			path: null,
			min: null,
			max: null,
			disabled: false,
			value: null,
		});
	}

	/**
	 *
	 * @return {string}
	 */
	static getTag() {
		return "monster-repeat-field-set";
	}

	/**
	 *
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [FieldSetStyleSheet, RepeatFieldSetStyleSheet];
	}

	/**
	 * The current value of the control.
	 *
	 * @return {string}
	 */
	get value() {
		return this.getOption("value");
	}

	/**
	 * Set the value of the control.
	 *
	 * @param {string} value
	 * @return {void}
	 */
	set value(value) {
		this.setOption("value", value);
		try {
			this?.setFormValue(this.value);
		} catch (e) {
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
		}
	}
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return { add: "Hinzufügen", remove: "Entfernen" };
		case "fr":
			return { add: "Ajouter", remove: "Supprimer" };
		case "es":
			return { add: "Agregar", remove: "Eliminar" };
		case "zh":
			return { add: "添加", remove: "删除" };
		case "hi":
			return { add: "जोड़ें", remove: "हटाएं" };
		case "bn":
			return { add: "যোগ করুন", remove: "মুছে ফেলুন" };
		case "pt":
			return { add: "Adicionar", remove: "Remover" };
		case "ru":
			return { add: "Добавить", remove: "Удалить" };
		case "ja":
			return { add: "追加", remove: "削除" };
		case "pa":
			return { add: "ਸ਼ਾਮਲ ਕਰੋ", remove: "ਹਟਾਓ" };
		case "mr":
			return { add: "जोडा", remove: "काढा" };
		case "it":
			return { add: "Aggiungi", remove: "Rimuovi" };
		case "nl":
			return { add: "Toevoegen", remove: "Verwijderen" };
		case "sv":
			return { add: "Lägg till", remove: "Ta bort" };
		case "pl":
			return { add: "Dodaj", remove: "Usuń" };
		case "da":
			return { add: "Tilføj", remove: "Fjern" };
		case "fi":
			return { add: "Lisää", remove: "Poista" };
		case "no":
			return { add: "Legg til", remove: "Fjern" };
		case "cs":
			return { add: "Přidat", remove: "Odebrat" };
		default:
			return { add: "Add", remove: "Remove" };
	}
}

/**
 * @private
 */
function initControlReferences() {
	this[addButtonSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="add"]`,
	);
	this[removeButtonSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="remove"]`,
	);
	this[itemSlotSymbol] = this.shadowRoot.querySelector('slot[name="item"]');
	this[itemsContainerSymbol] = ensureItemsContainer.call(this);
}

/**
 * @private
 * @return {HTMLElement}
 */
function ensureItemsContainer() {
	let container = this.querySelector(`[${ATTRIBUTE_ROLE}="items"]`);
	if (!(container instanceof HTMLElement)) {
		container = document.createElement("div");
		container.setAttribute(ATTRIBUTE_ROLE, "items");
		this.appendChild(container);
	}

	container.classList.add("grid-span-full");
	container.setAttribute("slot", "items");
	return container;
}

/**
 * @private
 * @return {HTMLStyleElement}
 */
function ensureItemsStyleSheet() {
	const root = this.getRootNode?.() || this.ownerDocument || document;
	if (!root || !("adoptedStyleSheets" in root)) {
		return;
	}
	const sheets = root.adoptedStyleSheets || [];
	if (!sheets.includes(RepeatFieldSetItemsStyleSheet)) {
		root.adoptedStyleSheets = [...sheets, RepeatFieldSetItemsStyleSheet];
	}
}

/**
 * @private
 */
function initTemplateBridge() {
	const slot = this[itemSlotSymbol];
	if (slot) {
		slot.addEventListener("slotchange", () => {
			this[itemDefaultsSymbol] = null;
			syncInsertDefinition.call(this);
		});
	}
	syncInsertDefinition.call(this);
}

/**
 * @private
 */
function initEventHandler() {
	if (this[addButtonSymbol]) {
		this[addButtonSymbol].addEventListener("click", (event) => {
			event.stopPropagation();
			addItem.call(this);
		});
	}

	if (this[removeButtonSymbol]) {
		this[removeButtonSymbol].addEventListener("click", (event) => {
			event.stopPropagation();
			removeItem.call(this);
		});
	}
}

/**
 * @private
 */
function updateColumns() {
	if (this.getOption("features.multipleColumns") !== true) {
		this.classList.remove("multiple-columns");
		return;
	}

	this.classList.add("multiple-columns");
}

/**
 * @private
 */
function initObservers() {
	this.attachObserver(
		new Observer(() => {
			syncInsertDefinition.call(this);
			syncButtons.call(this);
		}),
	);

	const form = this.closest("monster-form");
	if (form && typeof form.attachObserver === "function") {
		form.attachObserver(
			new Observer(() => {
				syncOptionData.call(this);
				syncButtons.call(this);
			}),
		);
	}

	const datasource = getDatasourceElement.call(this);
	if (datasource?.datasource?.attachObserver) {
		datasource.datasource.attachObserver(
			new Observer(() => {
				syncOptionData.call(this);
				syncButtons.call(this);
			}),
		);
	}

	if (datasource) {
		datasource.addEventListener("monster-datasource-fetched", () => {
			syncOptionData.call(this);
			normalizeOnLoad.call(this);
		});
	}
}

/**
 * @private
 * @return {string|null}
 */
function getPathOption() {
	const path = this.getOption("path");
	if (isString(path) && path.trim() !== "") {
		return path.trim();
	}
	return null;
}

/**
 * @private
 * @param {unknown} value
 * @return {number|null}
 */
function normalizeLimit(value) {
	if (value === null || value === undefined) {
		return null;
	}
	if (isNumber(value) && Number.isFinite(value)) {
		return value;
	}
	if (isString(value)) {
		const trimmed = value.trim();
		if (trimmed === "" || trimmed.toLowerCase() === "null") {
			return null;
		}
		const numeric = Number(trimmed);
		return Number.isFinite(numeric) ? numeric : null;
	}
	return null;
}

/**
 * @private
 * @return {number|null}
 */
function getMinLimit() {
	return normalizeLimit(this.getOption("min"));
}

/**
 * @private
 * @return {number|null}
 */
function getMaxLimit() {
	return normalizeLimit(this.getOption("max"));
}

/**
 * @private
 * @return {HTMLTemplateElement|null}
 */
function getItemTemplate() {
	const slot = this[itemSlotSymbol];
	if (slot && typeof slot.assignedElements === "function") {
		const assigned = slot.assignedElements({ flatten: true });
		for (const element of assigned) {
			if (element instanceof HTMLTemplateElement) {
				return element;
			}
		}
	}

	const fallback = this.querySelector('template[slot="item"]');
	if (fallback instanceof HTMLTemplateElement) {
		return fallback;
	}

	return null;
}

/**
 * @private
 * @return {string}
 */
function ensureTemplatePrefix() {
	const container = this[itemsContainerSymbol];
	if (!(container instanceof HTMLElement)) {
		throw new Error("RepeatFieldSet is missing the items container.");
	}

	let prefix = container.getAttribute(ATTRIBUTE_TEMPLATE_PREFIX);
	if (!isString(prefix) || prefix.trim() === "") {
		prefix = new ID("repeat-field").toString();
		container.setAttribute(ATTRIBUTE_TEMPLATE_PREFIX, prefix);
	}
	return prefix;
}

/**
 * @private
 */
function syncInsertDefinition() {
	const path = getPathOption.call(this);
	if (!path) {
		addErrorAttribute(this, "RepeatFieldSet requires a path option.");
		return;
	}

	const container = this[itemsContainerSymbol];
	if (!(container instanceof HTMLElement)) {
		addErrorAttribute(this, "RepeatFieldSet is missing the items container.");
		return;
	}

	const template = getItemTemplate.call(this);
	if (!template) {
		addErrorAttribute(
			this,
			'RepeatFieldSet requires a <template slot="item"> definition.',
		);
		return;
	}

	const prefix = ensureTemplatePrefix.call(this);
	const themeName = getDocumentTheme().getName();
	const nextId = `${prefix}-item-${themeName}`;
	if (template.id !== nextId) {
		template.id = nextId;
	}

	const form = this.closest("monster-form");
	let record = null;
	if (form && typeof form.getOption === "function") {
		const formData = form.getOption("data");
		if (isObject(formData) || isArray(formData)) {
			record = formData;
		}
	}
	if (!record) {
		record = resolveFormRecord(form, getDatasourceElement.call(this));
	}
	const normalizedPath = normalizePathForRecord(path, record);
	const dataPath = normalizedPath.startsWith("data.")
		? normalizedPath
		: `data.${normalizedPath}`;
	const insertValue = `item path:${dataPath}`;
	if (container.getAttribute(ATTRIBUTE_UPDATER_INSERT) !== insertValue) {
		container.setAttribute(ATTRIBUTE_UPDATER_INSERT, insertValue);
	}

	if (this.getAttribute(ATTRIBUTE_UPDATER_INSERT)) {
		this.removeAttribute(ATTRIBUTE_UPDATER_INSERT);
	}
}

/**
 * @private
 * @return {HTMLElement|null}
 */
function getDatasourceElement() {
	const form = this.closest("monster-form");
	if (!form) {
		return null;
	}

	if (form[datasourceLinkedElementSymbol]) {
		return form[datasourceLinkedElementSymbol];
	}

	if (typeof form.getOption === "function") {
		const selector = form.getOption("datasource.selector");
		if (isString(selector) && selector.trim() !== "") {
			return document.querySelector(selector);
		}
	}

	return null;
}

/**
 * @private
 * @return {object|null}
 */
function getTargetContext() {
	const form = this.closest("monster-form");
	const path = getPathOption.call(this);
	if (!path) {
		return null;
	}

	if (form && typeof form.getOption === "function") {
		const formData = form.getOption("data");
		if (isObject(formData) || isArray(formData)) {
			const formPath = normalizePathForForm(path);
			return { type: "form", target: form, path: formPath };
		}
	}

	const datasource = getDatasourceElement.call(this);
	if (datasource) {
		const mappingData = form?.getOption?.("mapping.data");
		const mappingIndex = form?.getOption?.("mapping.index");
		const record = resolveFormRecord(form, datasource);
		const normalizedPath = normalizePathForRecord(path, record);
		let basePath = "";

		if (isString(mappingData) && mappingData.trim() !== "") {
			basePath = mappingData.trim();
		}

		if (mappingIndex !== null && mappingIndex !== undefined) {
			const indexValue = String(mappingIndex);
			basePath = basePath ? `${basePath}.${indexValue}` : indexValue;
		}

		const finalPath = basePath
			? `${basePath}.${normalizedPath}`
			: normalizedPath;
		return { type: "datasource", target: datasource, path: finalPath };
	}

	if (form && typeof form.getOption === "function") {
		return { type: "form", target: form, path };
	}

	return null;
}

/**
 * @private
 * @param {string} path
 * @return {string}
 */
function normalizePathForForm(path) {
	if (!isString(path)) {
		return path;
	}

	const trimmed = path.trim();
	if (trimmed.startsWith("data.")) {
		return trimmed;
	}

	return `data.${trimmed}`;
}

/**
 * @private
 * @param {string} path
 * @param {object} record
 * @return {string}
 */
function normalizePathForRecord(path, record) {
	if (!isString(path)) {
		return path;
	}

	const trimmed = path.trim();
	if (!trimmed.startsWith("data.")) {
		return trimmed;
	}

	if (isObject(record?.data) || isArray(record?.data)) {
		return trimmed;
	}

	return trimmed.slice(5);
}

/**
 * @private
 * @return {object}
 */
function getCurrentDataSnapshot() {
	const context = getTargetContext.call(this);
	if (!context) {
		return { context: null, data: {} };
	}

	let data;
	if (context.type === "datasource") {
		data = context.target?.data;
	} else {
		data = context.target?.getOption?.("data");
	}

	if (!isObject(data) && !isArray(data)) {
		data = {};
	}

	return { context, data };
}

/**
 * @private
 * @return {Array}
 */
function getCurrentItems() {
	const { context, data } = getCurrentDataSnapshot.call(this);
	if (!context) {
		return [];
	}

	try {
		if (context.type === "form") {
			const value = context.target?.getOption?.(context.path);
			return isArray(value) ? [...value] : [];
		}

		const value = new Pathfinder(data).getVia(context.path);
		return isArray(value) ? [...value] : [];
	} catch {
		return [];
	}
}

/**
 * @private
 * @return {object}
 */
function buildEmptyItem() {
	const defaults = getItemDefaults.call(this);
	return clone(defaults);
}

/**
 * @private
 * @return {object}
 */
function getItemDefaults() {
	if (this[itemDefaultsSymbol]) {
		return this[itemDefaultsSymbol];
	}

	const template = getItemTemplate.call(this);
	const defaults = {};
	if (!template?.content) {
		this[itemDefaultsSymbol] = defaults;
		return defaults;
	}

	const elements = template.content.querySelectorAll(
		"[data-monster-bind],[data-monster-attributes]",
	);

	for (const element of elements) {
		const bindAttr = element.getAttribute("data-monster-bind");
		const attrAttr = element.getAttribute("data-monster-attributes");
		const paths = new Set([
			...extractItemPaths(bindAttr),
			...extractItemPathsFromAttributes(attrAttr),
		]);

		if (paths.size === 0) {
			continue;
		}

		const value = getDefaultValueForElement(element);
		for (const path of paths) {
			try {
				new Pathfinder(defaults).setVia(path, value);
			} catch (error) {
				addErrorAttribute(
					this,
					error?.message || "Failed to build default repeat item.",
				);
			}
		}
	}

	this[itemDefaultsSymbol] = defaults;
	return defaults;
}

/**
 * @private
 * @param {string|null} value
 * @return {string[]}
 */
function extractItemPaths(value) {
	if (!isString(value)) {
		return [];
	}

	const paths = [];
	const regex = /path:item\.([A-Za-z0-9_.-]+)/g;
	for (const match of value.matchAll(regex)) {
		if (match[1]) {
			paths.push(match[1]);
		}
	}
	return paths;
}

/**
 * @private
 * @param {string|null} value
 * @return {string[]}
 */
function extractItemPathsFromAttributes(value) {
	if (!isString(value)) {
		return [];
	}

	const paths = [];
	const entries = value.split(",");
	for (const entry of entries) {
		const def = entry.trim();
		if (
			def.startsWith("value ") ||
			def.startsWith("checked ") ||
			def.startsWith("selected ")
		) {
			paths.push(...extractItemPaths(def));
		}
	}

	return paths;
}

/**
 * @private
 * @param {Element} element
 * @return {*}
 */
function getDefaultValueForElement(element) {
	const bindType = element.getAttribute("data-monster-bind-type");
	const defaultValue = element.getAttribute("data-monster-defaultvalue");

	if (defaultValue !== null) {
		return coerceDefaultValue(defaultValue, bindType);
	}

	if (bindType === "boolean") {
		return false;
	}

	if (bindType === "number") {
		return null;
	}

	const tagName = element.tagName?.toLowerCase();
	if (tagName === "monster-toggle-switch") {
		return false;
	}

	if (tagName === "monster-select") {
		const optionType = element.getAttribute("data-monster-option-type");
		if (optionType === "checkbox" || element.hasAttribute("multiple")) {
			return [];
		}
		return "";
	}

	if (tagName === "select" && element.hasAttribute("multiple")) {
		return [];
	}

	const inputType = element.getAttribute("type");
	if (inputType === "checkbox" || inputType === "radio") {
		return false;
	}

	return "";
}

/**
 * @private
 * @param {string} value
 * @param {string|null} bindType
 * @return {*}
 */
function coerceDefaultValue(value, bindType) {
	if (bindType === "boolean") {
		const normalized = value.trim().toLowerCase();
		return normalized === "true" || normalized === "1";
	}

	if (bindType === "number") {
		const numeric = Number(value);
		return Number.isFinite(numeric) ? numeric : null;
	}

	return value;
}

/**
 * @private
 * @param {Array} items
 * @return {Array}
 */
function enforceLimits(items) {
	let list = isArray(items) ? [...items] : [];
	const min = getMinLimit.call(this);
	const max = getMaxLimit.call(this);

	if (isNumber(max) && Number.isFinite(max)) {
		if (list.length > max) {
			list = list.slice(0, max);
		}
	}

	if (isNumber(min) && Number.isFinite(min)) {
		while (list.length < min) {
			list.push(buildEmptyItem.call(this));
		}
	}

	return list;
}

/**
 * @private
 * @param {Array} items
 */
function persistItems(items, options = {}) {
	const { context, data } = getCurrentDataSnapshot.call(this);
	if (!context) {
		return;
	}

	const { syncDatasource = true } = options;

	if (context.type === "form") {
		try {
			context.target.setOption(context.path, items);
		} catch (error) {
			addErrorAttribute(this, error?.message || "Failed to update form data.");
			return;
		}

		if (syncDatasource) {
			const datasourceContext = getDatasourceContext.call(this);
			const datasourceData = datasourceContext?.target?.data;
			if (
				datasourceContext &&
				(isObject(datasourceData) || isArray(datasourceData))
			) {
				const nextData = clone(datasourceData);
				try {
					new Pathfinder(nextData).setVia(datasourceContext.path, items);
					datasourceContext.target.data = nextData;
				} catch (error) {
					addErrorAttribute(
						this,
						error?.message || "Failed to update datasource data.",
					);
					return;
				}
			}
		}
	} else {
		const nextData = clone(data);
		try {
			new Pathfinder(nextData).setVia(context.path, items);
		} catch (error) {
			addErrorAttribute(this, error?.message || "Failed to update path.");
			return;
		}

		context.target.data = nextData;
	}

	syncOptionData.call(this);
	syncButtons.call(this);
	fireCustomEvent(this, "monster-repeat-change", {
		detail: {
			length: items.length,
			path: context.path,
		},
	});
}

/**
 * @private
 */
function addItem() {
	if (this.getOption("disabled") === true) {
		return;
	}

	const items = getCurrentItems.call(this);
	const max = getMaxLimit.call(this);
	if (isNumber(max) && Number.isFinite(max) && items.length >= max) {
		return;
	}

	const nextItems = [...items, buildEmptyItem.call(this)];
	persistItems.call(this, nextItems);
	fireCustomEvent(this, "monster-repeat-add", {
		detail: { length: nextItems.length },
	});
}

/**
 * @private
 */
function removeItem() {
	if (this.getOption("disabled") === true) {
		return;
	}

	const items = getCurrentItems.call(this);
	const min = getMinLimit.call(this);
	if (isNumber(min) && Number.isFinite(min) && items.length <= min) {
		return;
	}
	if (items.length === 0) {
		return;
	}

	const nextItems = items.slice(0, -1);
	persistItems.call(this, nextItems);
	fireCustomEvent(this, "monster-repeat-remove", {
		detail: { length: nextItems.length },
	});
}

/**
 * @private
 */
function normalizeOnLoad() {
	const items = getCurrentItems.call(this);
	const normalized = enforceLimits.call(this, items);

	if (normalized.length !== items.length) {
		persistItems.call(this, normalized, { syncDatasource: false });
		return;
	}

	syncOptionData.call(this);
}

/**
 * @private
 */
function syncButtons() {
	const items = getCurrentItems.call(this);
	const min = getMinLimit.call(this);
	const max = getMaxLimit.call(this);
	const isDisabled = this.getOption("disabled") === true;

	if (this[addButtonSymbol]) {
		const reachedMax =
			isNumber(max) && Number.isFinite(max) && items.length >= max;
		this[addButtonSymbol].disabled = isDisabled || reachedMax;
	}

	if (this[removeButtonSymbol]) {
		const reachedMin =
			isNumber(min) && Number.isFinite(min) && items.length <= min;
		this[removeButtonSymbol].disabled =
			isDisabled || reachedMin || items.length === 0;
	}
}

/**
 * @private
 */
function syncOptionData() {
	const form = this.closest("monster-form");
	if (!form) {
		return;
	}

	const formData = form.getOption?.("data");
	if (isObject(formData) || isArray(formData)) {
		this.setOption("data", clone(formData));
	} else {
		const datasource = getDatasourceElement.call(this);
		if (datasource) {
			const record = resolveFormRecord.call(this, form, datasource);
			if (record) {
				this.setOption("data", clone(record));
			}
		}
	}
}

/**
 * @private
 * @param {HTMLElement} form
 * @param {HTMLElement} datasource
 * @return {object}
 */
function resolveFormRecord(form, datasource) {
	const { data, dataPath } = resolveDatasourceBasePath(form, datasource);
	if (!isObject(data) && !isArray(data)) {
		return {};
	}

	let resolvedData = data;
	if (isObject(resolvedData) && !isArray(resolvedData)) {
		if (isArray(resolvedData.data)) {
			resolvedData = resolvedData.data;
		} else if (isArray(resolvedData.dataset)) {
			resolvedData = resolvedData.dataset;
		} else if (dataPath === "") {
			resolvedData = Object.values(resolvedData);
		}
	}

	const mappingIndex = form.getOption?.("mapping.index");
	if (mappingIndex !== null && mappingIndex !== undefined) {
		resolvedData = resolvedData?.[mappingIndex];
	}

	if (!isObject(resolvedData) && !isArray(resolvedData)) {
		return {};
	}

	return resolvedData;
}

/**
 * @private
 * @return {{target: HTMLElement, path: string}|null}
 */
function getDatasourceContext() {
	const form = this.closest("monster-form");
	const datasource = getDatasourceElement.call(this);
	const path = getPathOption.call(this);
	if (!datasource || !path) {
		return null;
	}

	const mappingIndex = form?.getOption?.("mapping.index");
	const { dataPath } = resolveDatasourceBasePath(form, datasource);
	const record = resolveFormRecord(form, datasource);
	const normalizedPath = normalizePathForRecord(path, record);
	let basePath = "";

	if (isString(dataPath) && dataPath.trim() !== "") {
		basePath = dataPath.trim();
	}

	if (mappingIndex !== null && mappingIndex !== undefined) {
		const indexValue = String(mappingIndex);
		basePath = basePath ? `${basePath}.${indexValue}` : indexValue;
	}

	const finalPath = basePath ? `${basePath}.${normalizedPath}` : normalizedPath;
	return { target: datasource, path: finalPath };
}

/**
 * @private
 * @param {HTMLElement} form
 * @param {HTMLElement} datasource
 * @return {{data: object|Array, dataPath: string}}
 */
function resolveDatasourceBasePath(form, datasource) {
	let data = datasource?.data;
	if (!isObject(data) && !isArray(data)) {
		return { data: {}, dataPath: "" };
	}

	let dataPath = "";
	const mappingData = form?.getOption?.("mapping.data");
	if (isString(mappingData) && mappingData.trim() !== "") {
		const trimmed = mappingData.trim();
		try {
			const mapped = new Pathfinder(data).getVia(trimmed);
			if (mapped !== undefined) {
				data = mapped;
				dataPath = trimmed;
			}
		} catch {}
	}

	if (dataPath === "" && isObject(data) && !isArray(data)) {
		if (isArray(data.data)) {
			dataPath = "data";
		} else if (isArray(data.dataset)) {
			dataPath = "dataset";
		}
	}

	return { data, dataPath };
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <div data-monster-role="container" part="container">
                <div data-monster-attributes="class path:classes.content" part="content">
                    <slot></slot>
                    <slot name="items"></slot>
                </div>
                <div data-monster-role="actions" part="actions">
                    <slot name="actions"></slot>
                    <button type="button" data-monster-role="add" part="add">
                        <span data-monster-replace="path:labels.add"></span>
                    </button>
                    <button type="button" data-monster-role="remove" part="remove">
                        <span data-monster-replace="path:labels.remove"></span>
                    </button>
                </div>
                <slot name="item"></slot>
            </div>
        </div>`;
}

/**
 * @private
 * @return {string}
 */
registerCustomElement(RepeatFieldSet);
