/**
 * 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 {
	assembleMethodSymbol,
	CustomElement,
	getSlottedElements,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { isFunction, isObject } from "../../types/is.mjs";
import { Observer } from "../../types/observer.mjs";
import { WizardStyleSheet } from "./stylesheet/wizard.mjs";

import "../navigation/wizard-navigation.mjs";
import "./button-bar.mjs";
import "./message-state-button.mjs";

export { Wizard };

const controlElementSymbol = Symbol("controlElement");
const navigationElementSymbol = Symbol("navigationElement");
const slotElementSymbol = Symbol("slotElement");
const previousButtonElementSymbol = Symbol("previousButtonElement");
const nextButtonElementSymbol = Symbol("nextButtonElement");
const submitButtonElementSymbol = Symbol("submitButtonElement");
const statusElementSymbol = Symbol("statusElement");
const panelRecordsSymbol = Symbol("panelRecords");
const groupsSymbol = Symbol("groups");
const currentPanelIndexSymbol = Symbol("currentPanelIndex");
const optionObserverSymbol = Symbol("optionObserver");
const idsSymbol = Symbol("ids");
const finalRecordSymbol = Symbol("finalRecord");

/**
 * A reusable wizard container that combines content panels with `monster-wizard-navigation`.
 *
 * Every direct child panel that defines `data-monster-option-navigation-label` becomes a wizard panel.
 * Panels sharing the same label are grouped into one main step. Optional sub labels can be defined with
 * `data-monster-option-navigation-sub-label`.
 *
 * @fragments /fragments/components/form/wizard/
 * @example /examples/components/form/wizard-basic
 * @since 4.126.0
 * @summary A generic wizard control for checkout, creation and multi-step forms.
 * @fires monster-wizard-refresh
 * @fires monster-wizard-change-request
 * @fires monster-wizard-validation
 * @fires monster-wizard-panel-change
 * @fires monster-wizard-invalid
 * @fires monster-wizard-complete
 * @fires monster-wizard-final-panel-show
 */
class Wizard extends CustomElement {
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/form/wizard@@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 Text labels
	 * @property {string} labels.previous Previous button label
	 * @property {string} labels.next Next button label
	 * @property {string} labels.submit Submit button label
	 * @property {string} labels.stepStatus Step status pattern with `{current}` and `{total}`
	 * @property {Object} features Feature toggles
	 * @property {boolean} features.allowBackNavigation Allows navigation to previous panels
	 * @property {boolean} features.allowForwardNavigation Allows navigation to next panels
	 * @property {boolean} features.allowDirectNavigation Allows skipping ahead using the navigation
	 * @property {boolean} features.showActions Shows the action buttons row
	 * @property {boolean} features.hideSubmittedActions Hides buttons after completion
	 * @property {boolean} features.autoFocus Focuses the first focusable element of the active panel
	 * @property {boolean} features.keepCompletedNavigationState Marks visited items as completed in the navigation
	 * @property {Object} validation Validation settings
	 * @property {boolean} validation.native Runs `reportValidity()` on native forms and form controls before moving forward
	 * @property {string} validation.selector Selector for standalone controls that should run `reportValidity()`
	 * @property {function|null} validation.callback Global validation callback receiving a transition context
	 * @property {Object<string,function>} validation.panels Panel validation callbacks, keyed by panel id
	 * @property {Object<string,function>} validation.groups Group validation callbacks, keyed by group id
	 * @property {Object} actions Callback actions
	 * @property {function|null} actions.beforeStepChange Called before a panel change. Return `false` to cancel.
	 * @property {function|null} actions.afterStepChange Called after a panel change.
	 * @property {function|null} actions.invalid Called when validation blocks navigation.
	 * @property {function|null} actions.complete Called when submit is triggered on the last panel.
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: getTranslations(),
			features: {
				mutationObserver: true,
				allowBackNavigation: true,
				allowForwardNavigation: true,
				allowDirectNavigation: true,
				showActions: true,
				showStatus: false,
				hideSubmittedActions: false,
				autoFocus: false,
				keepCompletedNavigationState: true,
			},
			validation: {
				native: true,
				selector:
					"input,select,textarea,button,monster-select,monster-password,monster-toggle-switch,monster-digits",
				callback: null,
				panels: {},
				groups: {},
			},
			actions: {
				beforeStepChange: null,
				afterStepChange: null,
				invalid: null,
				complete: null,
			},
		});
	}

	static getTag() {
		return "monster-wizard";
	}

	static getCSSStyleSheet() {
		return [WizardStyleSheet];
	}

	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		this[currentPanelIndexSymbol] = -1;
		this[idsSymbol] = {
			panel: 0,
			group: 0,
		};

		initControlReferences.call(this);
		initEventHandler.call(this);
		initOptionObserver.call(this);
		ensureLabels.call(this);

		queueMicrotask(() => {
			this.refresh();
		});

		return this;
	}

	/**
	 * Rebuild the internal navigation structure from the slotted panels.
	 *
	 * @return {Wizard}
	 */
	refresh() {
		const records = collectPanels.call(this);
		const groups = buildGroups.call(this, records);

		this[panelRecordsSymbol] = records;
		this[groupsSymbol] = groups;

		renderNavigation.call(this);
		syncActionLabels.call(this);

		if (!records.length) {
			syncStatus.call(this);
			return this;
		}

		const currentIndex =
			this[currentPanelIndexSymbol] >= 0 &&
			this[currentPanelIndexSymbol] < records.length
				? this[currentPanelIndexSymbol]
				: 0;

		syncNavigationInteraction.call(this);
		setActivePanel.call(this, currentIndex, { force: true });

		fireCustomEvent(this, "monster-wizard-refresh", {
			panelCount: records.length,
			groupCount: groups.length,
		});

		return this;
	}

	/**
	 * Returns the current panel index.
	 *
	 * @return {number}
	 */
	getCurrentIndex() {
		return this[currentPanelIndexSymbol];
	}

	/**
	 * Returns the current panel element or `null`.
	 *
	 * @return {HTMLElement|null}
	 */
	getCurrentPanel() {
		const record = this[panelRecordsSymbol]?.[this[currentPanelIndexSymbol]];
		return record?.element || null;
	}

	/**
	 * Navigate to the next panel.
	 *
	 * @return {Promise<boolean>}
	 */
	async next() {
		return this.goTo(this[currentPanelIndexSymbol] + 1);
	}

	/**
	 * Navigate to the previous panel.
	 *
	 * @return {Promise<boolean>}
	 */
	async previous() {
		return this.goTo(this[currentPanelIndexSymbol] - 1);
	}

	/**
	 * Navigate to the requested panel index.
	 *
	 * @param {number} targetIndex
	 * @param {Object} [options]
	 * @param {boolean} [options.force=false]
	 * @param {string} [options.reason="api"]
	 * @return {Promise<boolean>}
	 */
	async goTo(targetIndex, options = {}) {
		if (typeof targetIndex !== "number" || Number.isNaN(targetIndex)) {
			return false;
		}

		const records = this[panelRecordsSymbol] || [];
		if (!records.length || targetIndex < 0 || targetIndex >= records.length) {
			return false;
		}

		const currentIndex = this[currentPanelIndexSymbol];
		if (targetIndex === currentIndex) {
			return true;
		}

		const { force = false, reason = "api" } = options;
		const direction =
			currentIndex === -1
				? "initial"
				: targetIndex > currentIndex
					? "forward"
					: "backward";
		const context = createTransitionContext.call(
			this,
			currentIndex,
			targetIndex,
			direction,
			reason,
		);

		if (!force) {
			const allowed = await allowTransition.call(this, context);
			if (!allowed) {
				return false;
			}
		}

		setActivePanel.call(this, targetIndex, options);
		return true;
	}

	/**
	 * Validates the current panel without changing the current position.
	 *
	 * @return {Promise<boolean>}
	 */
	async validateCurrent() {
		const index = this[currentPanelIndexSymbol];
		if (index < 0) {
			return true;
		}

		const context = createTransitionContext.call(
			this,
			index,
			Math.min(index + 1, (this[panelRecordsSymbol] || []).length - 1),
			"forward",
			"validate",
		);

		return runValidation.call(this, context);
	}

	/**
	 * Trigger the completion flow on the last panel.
	 *
	 * @return {Promise<boolean>}
	 */
	async submit() {
		const index = this[currentPanelIndexSymbol];
		const records = this[panelRecordsSymbol] || [];
		if (index < 0 || index !== records.length - 1) {
			return false;
		}

		const context = createTransitionContext.call(
			this,
			index,
			index,
			"submit",
			"submit",
		);
		const valid = await runValidation.call(this, context);
		if (!valid) {
			return false;
		}

		const callback = this.getOption("actions.complete");
		if (isFunction(callback)) {
			const result = await Promise.resolve(callback.call(this, context));
			if (result === false) {
				return false;
			}
		}

		fireCustomEvent(this, "monster-wizard-complete", context);
		this[navigationElementSymbol]?.completeAll?.();

		if (this[finalRecordSymbol]) {
			showFinalPanel.call(this);
		}

		if (this.getOption("features.hideSubmittedActions") === true) {
			this[controlElementSymbol]?.setAttribute(
				"data-monster-state",
				"completed",
			);
		}

		this[submitButtonElementSymbol]?.removeState?.();
		this[submitButtonElementSymbol]?.clearMessage?.();
		this[submitButtonElementSymbol]?.setState?.("successful", 2000);

		return true;
	}
}

function initControlReferences() {
	this[controlElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="control"]`,
	);
	this[navigationElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="navigation"]`,
	);
	this[slotElementSymbol] = this.shadowRoot.querySelector("slot");
	this[previousButtonElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="previous"]`,
	);
	this[nextButtonElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="next"]`,
	);
	this[submitButtonElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="submit"]`,
	);
	this[statusElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="status"]`,
	);
}

function initEventHandler() {
	this[slotElementSymbol]?.addEventListener("slotchange", () => {
		this.refresh();
	});

	this[navigationElementSymbol]?.addEventListener(
		"monster-wizard-step-changed",
		(event) => {
			const mainIndex = event?.detail?.newIndex;
			if (typeof mainIndex !== "number") {
				return;
			}

			const targetRecord = this[groupsSymbol]?.[mainIndex]?.panels?.[0];
			if (
				!targetRecord ||
				targetRecord.index === this[currentPanelIndexSymbol]
			) {
				return;
			}

			setActivePanel.call(this, targetRecord.index, {
				force: true,
				reason: "navigation",
			});
		},
	);

	this[navigationElementSymbol]?.addEventListener(
		"monster-wizard-substep-changed",
		(event) => {
			const mainIndex = event?.detail?.mainIndex;
			const subIndex = event?.detail?.subIndex;
			if (typeof mainIndex !== "number" || typeof subIndex !== "number") {
				return;
			}

			const targetRecord = this[groupsSymbol]?.[mainIndex]?.panels?.[subIndex];
			if (
				!targetRecord ||
				targetRecord.index === this[currentPanelIndexSymbol]
			) {
				return;
			}

			setActivePanel.call(this, targetRecord.index, {
				force: true,
				reason: "navigation",
			});
		},
	);

	this[previousButtonElementSymbol]?.addEventListener("click", () => {
		this.previous();
	});
	this[nextButtonElementSymbol]?.addEventListener("click", () => {
		this.next();
	});
	this[submitButtonElementSymbol]?.addEventListener("click", () => {
		this.submit();
	});
}

function initOptionObserver() {
	if (this[optionObserverSymbol]) {
		return;
	}

	this[optionObserverSymbol] = true;
	this.attachObserver(
		new Observer(() => {
			ensureLabels.call(this);
			syncActionLabels.call(this);
			syncNavigationInteraction.call(this);
			syncActionState.call(this);
			syncStatus.call(this);
		}),
	);
}

function syncActionLabels() {
	const labels = this.getOption("labels");
	if (this[previousButtonElementSymbol]) {
		this[previousButtonElementSymbol].setOption(
			"labels.button",
			labels.previous || "Back",
		);
	}
	if (this[nextButtonElementSymbol]) {
		this[nextButtonElementSymbol].setOption(
			"labels.button",
			labels.next || "Next",
		);
	}
	if (this[submitButtonElementSymbol]) {
		this[submitButtonElementSymbol].setOption(
			"labels.button",
			labels.submit || "Finish",
		);
	}
}

function syncNavigationInteraction() {
	const navigation = this[navigationElementSymbol];
	if (!(navigation instanceof HTMLElement)) {
		return;
	}

	const allowDirect = this.getOption("features.allowDirectNavigation");
	navigation.style.pointerEvents = allowDirect ? "" : "none";
	navigation.toggleAttribute("data-monster-direct-navigation", allowDirect);
}

function syncActionState() {
	const records = this[panelRecordsSymbol] || [];
	const currentIndex = this[currentPanelIndexSymbol];
	const hasPanels = records.length > 0;
	const atFirst = currentIndex <= 0;
	const atLast = hasPanels && currentIndex === records.length - 1;
	const finalVisible =
		this[finalRecordSymbol]?.element?.hasAttribute?.("data-monster-active") ===
		true;

	const allowBack = this.getOption("features.allowBackNavigation");
	const allowForward = this.getOption("features.allowForwardNavigation");
	const showActions = this.getOption("features.showActions");
	const showPrevious =
		showActions && !finalVisible && allowBack === true && !atFirst;

	this[controlElementSymbol]?.toggleAttribute(
		"data-monster-show-actions",
		showActions,
	);

	if (this[previousButtonElementSymbol]) {
		this[previousButtonElementSymbol].hidden = !showPrevious;
		this[previousButtonElementSymbol].style.display = showPrevious
			? ""
			: "none";
		this[previousButtonElementSymbol].disabled =
			!hasPanels || atFirst || allowBack !== true;
	}

	if (this[nextButtonElementSymbol]) {
		this[nextButtonElementSymbol].hidden =
			!showActions || atLast || finalVisible;
		this[nextButtonElementSymbol].style.display =
			!showActions || atLast || finalVisible ? "none" : "";
		this[nextButtonElementSymbol].disabled =
			!hasPanels || atLast || allowForward !== true;
	}

	if (this[submitButtonElementSymbol]) {
		this[submitButtonElementSymbol].hidden =
			!showActions || !atLast || finalVisible;
		this[submitButtonElementSymbol].style.display =
			!showActions || !atLast || finalVisible ? "none" : "";
		this[submitButtonElementSymbol].disabled = !hasPanels || !atLast;
	}
}

function syncStatus() {
	const statusElement = this[statusElementSymbol];
	if (!(statusElement instanceof HTMLElement)) {
		return;
	}

	if (this.getOption("features.showStatus") !== true) {
		statusElement.hidden = true;
		statusElement.textContent = "";
		return;
	}

	statusElement.hidden = false;

	const total = this[panelRecordsSymbol]?.length || 0;
	const finalVisible =
		this[finalRecordSymbol]?.element?.hasAttribute?.("data-monster-active") ===
		true;
	if (total === 0 || this[currentPanelIndexSymbol] < 0 || finalVisible) {
		statusElement.textContent = "";
		return;
	}

	const template =
		this.getOption("labels.stepStatus") || "Step {current} of {total}";
	statusElement.textContent = template
		.replace("{current}", String(this[currentPanelIndexSymbol] + 1))
		.replace("{total}", String(total));
}

function ensureLabels() {
	const current = this.getOption("labels");
	const fallback = getTranslations();
	if (!isObject(current)) {
		this.setOption("labels", fallback);
		return;
	}

	this.setOption("labels", Object.assign({}, fallback, current));
}

function collectPanels() {
	const elements = Array.from(getSlottedElements.call(this) || []).filter(
		(element) => element instanceof HTMLElement,
	);
	const finalIndex = elements.length - 1;
	const hasImplicitFinal =
		finalIndex >= 0 &&
		elements.length > 1 &&
		elements[finalIndex] instanceof HTMLElement &&
		elements[finalIndex].hasAttribute(
			"data-monster-option-navigation-label",
		) === false;
	this[finalRecordSymbol] = null;

	const stepElements = elements.filter((element, index) => {
		if (hasImplicitFinal && index === finalIndex) {
			return false;
		}
		return element.hasAttribute("data-monster-option-navigation-label");
	});

	const records = stepElements.map((element, index) => {
		const mainLabel =
			element.getAttribute("data-monster-option-navigation-label")?.trim() ||
			`Step ${index + 1}`;
		const subLabel =
			element
				.getAttribute("data-monster-option-navigation-sub-label")
				?.trim() || null;
		const panelId =
			element.getAttribute("data-monster-option-navigation-id")?.trim() ||
			slugify(
				subLabel
					? `${mainLabel}-${subLabel}-${index + 1}`
					: `${mainLabel}-${index + 1}`,
			) ||
			`panel-${++this[idsSymbol].panel}`;
		const groupBase =
			element.getAttribute("data-monster-option-navigation-group")?.trim() ||
			mainLabel;
		const groupId = slugify(groupBase) || `group-${++this[idsSymbol].group}`;

		element.setAttribute("data-monster-wizard-panel-id", panelId);
		element.setAttribute("data-monster-wizard-group-id", groupId);
		element.setAttribute("data-monster-role", "panel");
		normalizePanelHeading(element);

		return {
			element,
			index,
			panelId,
			groupId,
			mainLabel,
			subLabel,
			mainIndex: -1,
			subIndex: -1,
		};
	});

	const finalElement = elements[finalIndex];
	if (hasImplicitFinal && finalElement instanceof HTMLElement) {
		finalElement.setAttribute("data-monster-role", "panel");
		normalizePanelHeading(finalElement);
		finalElement.hidden = true;
		finalElement.removeAttribute("data-monster-active");
		this[finalRecordSymbol] = {
			element: finalElement,
			panelId:
				finalElement
					.getAttribute("data-monster-option-navigation-id")
					?.trim() || "final",
		};
	}

	for (const element of elements) {
		applyEmbeddedFieldSetContext(element);
	}

	return records;
}

function applyEmbeddedFieldSetContext(element) {
	if (!(element instanceof HTMLElement)) {
		return;
	}

	for (const fieldSet of element.querySelectorAll("monster-field-set")) {
		fieldSet.setAttribute("data-monster-context", "wizard");
	}
}

function buildGroups(records) {
	const groups = [];
	const map = new Map();

	for (const record of records) {
		let group = map.get(record.groupId);
		if (!group) {
			group = {
				id: record.groupId,
				label: record.mainLabel,
				mainIndex: groups.length,
				panels: [],
			};
			map.set(record.groupId, group);
			groups.push(group);
		}

		record.mainIndex = group.mainIndex;
		record.subIndex = group.panels.length;
		group.panels.push(record);
	}

	return groups;
}

function renderNavigation() {
	const navigation = this[navigationElementSymbol];
	if (!(navigation instanceof HTMLElement)) {
		return;
	}

	navigation.innerHTML = "";

	const list = document.createElement("ol");
	list.className = "wizard-steps";

	for (const group of this[groupsSymbol] || []) {
		const step = document.createElement("li");
		step.className = "step";

		const label = document.createElement("span");
		label.setAttribute("data-monster-role", "step-label");
		label.textContent = group.label;
		step.appendChild(label);

		if (group.panels.length > 1 || group.panels[0]?.subLabel) {
			const subList = document.createElement("ul");
			for (const panel of group.panels) {
				const item = document.createElement("li");
				item.textContent = panel.subLabel || `Step ${panel.subIndex + 1}`;
				subList.appendChild(item);
			}
			step.appendChild(subList);
		}

		list.appendChild(step);
	}

	navigation.appendChild(list);

	navigation.setOption("actions.beforeStepChange", (fromIndex, toIndex) => {
		return handleNavigationRequest.call(this, fromIndex, toIndex);
	});

	queueMicrotask(() => {
		const currentIndex =
			this[currentPanelIndexSymbol] >= 0 ? this[currentPanelIndexSymbol] : 0;
		const record = this[panelRecordsSymbol]?.[currentIndex];
		if (record) {
			navigation.activate(record.mainIndex, record.subIndex, { force: true });
		}
	});
}

async function handleNavigationRequest(fromIndex, toIndex) {
	if (this.getOption("features.allowDirectNavigation") !== true) {
		return false;
	}

	const targetPanel = this[groupsSymbol]?.[toIndex]?.panels?.[0];
	if (!targetPanel) {
		return false;
	}

	const currentIndex = this[currentPanelIndexSymbol];
	if (currentIndex === -1) {
		return true;
	}

	const targetIndex = targetPanel.index;
	if (targetIndex === currentIndex) {
		return false;
	}

	const direction = targetIndex > currentIndex ? "forward" : "backward";
	const context = createTransitionContext.call(
		this,
		currentIndex,
		targetIndex,
		direction,
		"navigation",
	);

	return allowTransition.call(this, context);
}

function setActivePanel(targetIndex, options = {}) {
	const records = this[panelRecordsSymbol] || [];
	const previousIndex = this[currentPanelIndexSymbol];
	const record = records[targetIndex];
	if (!record) {
		return;
	}

	for (const panelRecord of records) {
		const active = panelRecord.index === targetIndex;
		panelRecord.element.hidden = !active;
		panelRecord.element.toggleAttribute("data-monster-active", active);
		panelRecord.element.toggleAttribute(
			"data-monster-completed",
			this.getOption("features.keepCompletedNavigationState") === true &&
				panelRecord.index < targetIndex,
		);
	}

	if (this[finalRecordSymbol]?.element) {
		this[finalRecordSymbol].element.hidden = true;
		this[finalRecordSymbol].element.removeAttribute("data-monster-active");
	}

	this[currentPanelIndexSymbol] = targetIndex;

	if (this.getOption("features.keepCompletedNavigationState") === true) {
		this[navigationElementSymbol]?.activate(record.mainIndex, record.subIndex, {
			force: true,
		});
	} else {
		this[navigationElementSymbol]?.activate(record.mainIndex, record.subIndex, {
			force: true,
		});
	}

	syncActionState.call(this);
	syncStatus.call(this);
	clearActionFeedback.call(this);

	if (this.getOption("features.autoFocus") === true) {
		queueMicrotask(() => {
			focusFirstControl(record.element);
		});
	}

	fireCustomEvent(this, "monster-wizard-panel-change", {
		panelIndex: targetIndex,
		previousPanelIndex: previousIndex,
		panelId: record.panelId,
		groupId: record.groupId,
		panel: record.element,
		options,
	});

	const callback = this.getOption("actions.afterStepChange");
	if (isFunction(callback)) {
		callback.call(
			this,
			createTransitionContext.call(
				this,
				previousIndex,
				targetIndex,
				targetIndex > previousIndex ? "forward" : "backward",
				options.reason || "internal",
			),
		);
	}
}

function showFinalPanel() {
	const finalRecord = this[finalRecordSymbol];
	if (!finalRecord?.element) {
		return;
	}

	for (const panelRecord of this[panelRecordsSymbol] || []) {
		panelRecord.element.hidden = true;
		panelRecord.element.removeAttribute("data-monster-active");
	}

	finalRecord.element.hidden = false;
	finalRecord.element.setAttribute("data-monster-active", "");
	this[currentPanelIndexSymbol] = -1;
	syncActionState.call(this);
	syncStatus.call(this);
	clearActionFeedback.call(this);
	fireCustomEvent(this, "monster-wizard-final-panel-show", {
		panelId: finalRecord.panelId,
		panel: finalRecord.element,
	});
}

async function allowTransition(context) {
	const currentIndex = this[currentPanelIndexSymbol];

	fireCustomEvent(this, "monster-wizard-change-request", context);

	if (context.direction === "backward") {
		if (this.getOption("features.allowBackNavigation") !== true) {
			return false;
		}
	} else if (context.direction === "forward") {
		if (this.getOption("features.allowForwardNavigation") !== true) {
			return false;
		}

		if (
			this.getOption("features.allowDirectNavigation") !== true &&
			context.toIndex > currentIndex + 1
		) {
			return false;
		}

		const valid = await runValidation.call(this, context);
		if (!valid) {
			return false;
		}
	}

	const beforeStepChange = this.getOption("actions.beforeStepChange");
	if (isFunction(beforeStepChange)) {
		const result = await Promise.resolve(beforeStepChange.call(this, context));
		if (result === false) {
			return false;
		}
	}

	return true;
}

async function runValidation(context) {
	let valid = true;
	let message = null;

	if (this.getOption("validation.native") === true) {
		valid = validatePanelElement.call(this, context.fromPanel) && valid;
	}

	const groupCallback = this.getOption(
		`validation.groups.${context.fromGroup?.id || ""}`,
	);
	if (isFunction(groupCallback)) {
		const result = await Promise.resolve(groupCallback.call(this, context));
		valid = normalizeValidationResult(result, valid);
		message = extractValidationMessage(result) || message;
	}

	const panelCallback = this.getOption(
		`validation.panels.${context.fromPanelId || ""}`,
	);
	if (isFunction(panelCallback)) {
		const result = await Promise.resolve(panelCallback.call(this, context));
		valid = normalizeValidationResult(result, valid);
		message = extractValidationMessage(result) || message;
	}

	const globalCallback = this.getOption("validation.callback");
	if (isFunction(globalCallback)) {
		const result = await Promise.resolve(globalCallback.call(this, context));
		valid = normalizeValidationResult(result, valid);
		message = extractValidationMessage(result) || message;
	}

	if (!valid) {
		const actionButton = getActionButtonForContext.call(this, context);
		const fallbackMessage = this.getOption("labels.validationFailed");
		showActionError.call(
			this,
			actionButton,
			message ||
				fallbackMessage ||
				"Please check the current step before continuing.",
		);

		fireCustomEvent(this, "monster-wizard-invalid", context);
		const invalid = this.getOption("actions.invalid");
		if (isFunction(invalid)) {
			invalid.call(this, Object.assign({}, context, { message }));
		}
	}

	fireCustomEvent(this, "monster-wizard-validation", {
		...context,
		valid,
		message,
	});

	return valid;
}

function validatePanelElement(panel) {
	if (!(panel instanceof HTMLElement)) {
		return true;
	}

	let valid = true;
	const formContainers = panel.querySelectorAll("form, monster-form");
	for (const element of formContainers) {
		if (typeof element.reportValidity === "function") {
			valid = element.reportValidity() !== false && valid;
		}
	}

	const selector = this.getOption("validation.selector");
	if (typeof selector !== "string" || selector.trim() === "") {
		return valid;
	}

	const controls = panel.querySelectorAll(selector);
	for (const element of controls) {
		if (element.closest("form, monster-form")) {
			continue;
		}

		if (typeof element.reportValidity === "function") {
			valid = element.reportValidity() !== false && valid;
		}
	}

	return valid;
}

function createTransitionContext(fromIndex, toIndex, direction, reason) {
	const records = this[panelRecordsSymbol] || [];
	const groups = this[groupsSymbol] || [];
	const fromRecord =
		fromIndex >= 0 && fromIndex < records.length ? records[fromIndex] : null;
	const toRecord =
		toIndex >= 0 && toIndex < records.length ? records[toIndex] : null;

	return {
		wizard: this,
		reason,
		direction,
		fromIndex,
		toIndex,
		fromPanelId: fromRecord?.panelId || null,
		toPanelId: toRecord?.panelId || null,
		fromPanel: fromRecord?.element || null,
		toPanel: toRecord?.element || null,
		fromGroup: fromRecord ? groups[fromRecord.mainIndex] : null,
		toGroup: toRecord ? groups[toRecord.mainIndex] : null,
		actionButton: getActionButtonForDirection.call(this, direction),
	};
}

function getActionButtonForContext(context) {
	if (context.direction === "submit") {
		return this[submitButtonElementSymbol] || null;
	}

	return this[nextButtonElementSymbol] || null;
}

function getActionButtonForDirection(direction) {
	if (direction === "submit") {
		return this[submitButtonElementSymbol] || null;
	}
	if (direction === "backward") {
		return this[previousButtonElementSymbol] || null;
	}
	return this[nextButtonElementSymbol] || null;
}

function clearActionFeedback() {
	for (const button of [
		this[previousButtonElementSymbol],
		this[nextButtonElementSymbol],
		this[submitButtonElementSymbol],
	]) {
		button?.removeState?.();
		button?.hideMessage?.();
		button?.clearMessage?.();
	}
}

function showActionError(button, message) {
	if (!button) {
		return;
	}

	button.removeState?.();
	button.hideMessage?.();
	button.clearMessage?.();
	button.setState?.("failed", 2500);
	button.setMessage?.(message);
	button.showMessage?.(3500);
}

function normalizeValidationResult(result, valid) {
	if (result === false) {
		return false;
	}

	if (typeof result === "string") {
		return false;
	}

	return valid;
}

function extractValidationMessage(result) {
	if (typeof result === "string" && result.trim() !== "") {
		return result.trim();
	}

	if (
		result &&
		typeof result === "object" &&
		typeof result.message === "string" &&
		result.message.trim() !== ""
	) {
		return result.message.trim();
	}

	return null;
}

function focusFirstControl(panel) {
	if (!(panel instanceof HTMLElement)) {
		return;
	}

	const target = panel.querySelector(
		'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])',
	);
	target?.focus?.();
}

function normalizePanelHeading(panel) {
	if (!(panel instanceof HTMLElement)) {
		return;
	}

	const heading = panel.querySelector("h1, h2, h3, h4, h5, h6");
	if (!(heading instanceof HTMLElement)) {
		return;
	}

	heading.style.marginTop = "0";
	heading.style.paddingTop = "0";
}

function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				previous: "Zurueck",
				next: "Weiter",
				submit: "Abschliessen",
				stepStatus: "Schritt {current} von {total}",
				validationFailed: "Bitte pruefen Sie den aktuellen Schritt.",
			};
		case "fr":
			return {
				previous: "Retour",
				next: "Suivant",
				submit: "Terminer",
				stepStatus: "Etape {current} sur {total}",
				validationFailed: "Veuillez verifier l'etape actuelle.",
			};
		case "es":
			return {
				previous: "Atras",
				next: "Siguiente",
				submit: "Finalizar",
				stepStatus: "Paso {current} de {total}",
				validationFailed: "Revise el paso actual.",
			};
		case "zh":
			return {
				previous: "返回",
				next: "下一步",
				submit: "完成",
				stepStatus: "第 {current} / {total} 步",
				validationFailed: "请检查当前步骤。",
			};
		case "hi":
			return {
				previous: "वापस",
				next: "आगे",
				submit: "पूरा करें",
				stepStatus: "चरण {current} / {total}",
				validationFailed: "कृपया वर्तमान चरण जांचें।",
			};
		case "bn":
			return {
				previous: "পেছনে",
				next: "পরবর্তী",
				submit: "সমাপ্ত",
				stepStatus: "ধাপ {current} / {total}",
				validationFailed: "অনুগ্রহ করে বর্তমান ধাপটি পরীক্ষা করুন।",
			};
		case "pt":
			return {
				previous: "Voltar",
				next: "Avancar",
				submit: "Concluir",
				stepStatus: "Passo {current} de {total}",
				validationFailed: "Verifique a etapa atual.",
			};
		case "ru":
			return {
				previous: "Назад",
				next: "Далее",
				submit: "Завершить",
				stepStatus: "Шаг {current} из {total}",
				validationFailed: "Проверьте текущий шаг.",
			};
		case "ja":
			return {
				previous: "戻る",
				next: "次へ",
				submit: "完了",
				stepStatus: "{total} 件中 {current} ステップ",
				validationFailed: "現在のステップを確認してください。",
			};
		case "pa":
			return {
				previous: "ਵਾਪਸ",
				next: "ਅੱਗੇ",
				submit: "ਪੂਰਾ ਕਰੋ",
				stepStatus: "ਕਦਮ {current} / {total}",
				validationFailed: "ਕਿਰਪਾ ਕਰਕੇ ਮੌਜੂਦਾ ਕਦਮ ਦੀ ਜਾਂਚ ਕਰੋ।",
			};
		case "mr":
			return {
				previous: "मागे",
				next: "पुढे",
				submit: "पूर्ण करा",
				stepStatus: "पायरी {current} / {total}",
				validationFailed: "कृपया सद्य पायरी तपासा.",
			};
		case "it":
			return {
				previous: "Indietro",
				next: "Avanti",
				submit: "Concludi",
				stepStatus: "Passo {current} di {total}",
				validationFailed: "Controlla il passaggio corrente.",
			};
		case "nl":
			return {
				previous: "Terug",
				next: "Volgende",
				submit: "Voltooien",
				stepStatus: "Stap {current} van {total}",
				validationFailed: "Controleer de huidige stap.",
			};
		case "sv":
			return {
				previous: "Tillbaka",
				next: "Nasta",
				submit: "Slutfor",
				stepStatus: "Steg {current} av {total}",
				validationFailed: "Kontrollera det aktuella steget.",
			};
		case "pl":
			return {
				previous: "Wstecz",
				next: "Dalej",
				submit: "Zakoncz",
				stepStatus: "Krok {current} z {total}",
				validationFailed: "Sprawdz biezacy krok.",
			};
		case "da":
			return {
				previous: "Tilbage",
				next: "Naeste",
				submit: "Afslut",
				stepStatus: "Trin {current} af {total}",
				validationFailed: "Kontroller det aktuelle trin.",
			};
		case "fi":
			return {
				previous: "Takaisin",
				next: "Seuraava",
				submit: "Valmis",
				stepStatus: "Vaihe {current} / {total}",
				validationFailed: "Tarkista nykyinen vaihe.",
			};
		case "no":
			return {
				previous: "Tilbake",
				next: "Neste",
				submit: "Fullfor",
				stepStatus: "Trinn {current} av {total}",
				validationFailed: "Kontroller gjeldende trinn.",
			};
		case "cs":
			return {
				previous: "Zpet",
				next: "Dalsi",
				submit: "Dokoncit",
				stepStatus: "Krok {current} z {total}",
				validationFailed: "Zkontrolujte aktualni krok.",
			};
		default:
			return {
				previous: "Back",
				next: "Next",
				submit: "Finish",
				stepStatus: "Step {current} of {total}",
				validationFailed: "Please check the current step before continuing.",
			};
	}
}

function slugify(value) {
	return String(value || "")
		.trim()
		.toLowerCase()
		.replace(/[^a-z0-9]+/g, "-")
		.replace(/^-+|-+$/g, "");
}

function getTemplate() {
	return `
		<div data-monster-role="control" part="control">
			<div data-monster-role="body" part="body">
				<monster-wizard-navigation data-monster-role="navigation" part="navigation"></monster-wizard-navigation>
				<div data-monster-role="content" part="content">
					<div data-monster-role="status" part="status" aria-live="polite"></div>
					<div data-monster-role="panel-slot" part="panel-slot">
						<slot></slot>
					</div>
					<monster-button-bar data-monster-role="actions" part="actions">
						<monster-message-state-button data-monster-role="previous" part="previous"></monster-message-state-button>
						<monster-message-state-button data-monster-role="next" part="next"></monster-message-state-button>
						<monster-message-state-button data-monster-role="submit" part="submit"></monster-message-state-button>
					</monster-button-bar>
				</div>
			</div>
		</div>`;
}

registerCustomElement(Wizard);
