/**
 * Navigation and search handling with debouncing for the Spectra Pro Loop Builder.
 *
 * This file contains various utility functions and generator actions related to navigation, search, and filter operations
 * in the Spectra Pro Loop Builder block. It includes functions for:
 * - Validating links and events for navigation.
 * - Debouncing input events to reduce unnecessary operations.
 * - Updating URL parameters based on search, sort, and filter inputs.
 * - Managing navigation and prefetching actions using the Interactivity API.
 *
 * Dependencies:
 * - WordPress Interactivity API (`@wordpress/interactivity`)
 * - WordPress Interactivity Router (`@wordpress/interactivity-router`)
 *
 * Actions:
 * - Handles search, sorting, and filtering inputs with debouncing and URL updates.
 * - Navigates to new URLs with loading flags and context updates.
 * - Prefetches content for links on hover or focus.
 * - Clears search and filter settings, resetting the context and URL.
 *
 * Context:
 * - The context is used for managing state related to search, sorting, filtering, and URL parameters.
 * - Actions use context data to update the page dynamically without full reloads.
 *
 * @since x.x.x
 */

/**
 * External dependencies.
 */
import { store, getContext, getElement, withSyncEvent } from '@wordpress/interactivity';
/**
 * Validates if a link is suitable for navigation.
 *
 * Ensures the reference is an anchor element with a valid href, targets the same window,
 * and is on the same origin as the current page.
 *
 * @param {HTMLElement} ref The DOM element to validate.
 * @return {boolean} True if the link is valid for navigation, false otherwise.
 */
const isValidLink = ( ref ) =>
	ref &&
	ref instanceof window.HTMLAnchorElement &&
	ref.href &&
	( ! ref.target || ref.target === '_self' ) &&
	ref.origin === window.location.origin;

/**
 * Validates if an event is suitable for navigation.
 *
 * Ensures the event is a left-click without modifier keys that would alter default behavior.
 *
 * @param {MouseEvent} event The event to validate.
 * @return {boolean} True if the event is valid for navigation, false otherwise.
 */
const isValidEvent = ( event ) =>
	event.button === 0 && // Left clicks only.
	! event.metaKey && // Open in new tab (Mac).
	! event.ctrlKey && // Open in new tab (Windows).
	! event.altKey && // Download.
	! event.shiftKey &&
	! event.defaultPrevented;

// Debounce helper to delay execution with cancellation support.
let supersedePreviousSearch = null;
let supersedePreviousFilter = null;

/**
 * Creates a debounced promise with cancellation capability.
 *
 * Delays execution by the specified time and allows cancellation of previous debounces.
 *
 * @param {number} [delay=500]     The delay in milliseconds before resolving.
 * @param {string} [type='search'] The type of debounce (search or filter).
 * @return {Promise} A promise that resolves after the delay or rejects on cancellation.
 */
const debounce = ( delay = 500, type = 'search' ) => {
	if ( type === 'filter' ) {
		supersedePreviousFilter?.(); // Cancel any previous pending filter debounce.
		return new Promise( ( resolve, reject ) => {
			const timer = setTimeout( resolve, delay ); // Set timeout to resolve after delay.
			supersedePreviousFilter = () => {
				clearTimeout( timer ); // Clear timeout if superseded.
				reject(); // Reject the promise.
			};
		} );
	}

	supersedePreviousSearch?.(); // Cancel any previous pending debounce.
	return new Promise( ( resolve, reject ) => {
		const timer = setTimeout( resolve, delay ); // Set timeout to resolve after delay.
		supersedePreviousSearch = () => {
			clearTimeout( timer ); // Clear timeout if superseded.
			reject(); // Reject the promise.
		};
	} );
};

/**
 * Updates a URL parameter with a given value.
 *
 * Sets the parameter if a value is provided, or deletes it if the value is empty.
 *
 * @param {URL}    url   The URL object to modify.
 * @param {string} param The parameter name to update.
 * @param {string} value The value to set, or empty to delete.
 * @return {void}
 */
const updateUrlParam = ( url, param, value ) => {
	if ( value ) {
		url.searchParams.set( param, value ); // Set the parameter with the value.
	} else {
		url.searchParams.delete( param ); // Remove the parameter if no value.
	}
};

/**
 * Generator function to perform navigation with loading flags.
 *
 * Updates context flags during navigation and uses the Interactivity Router to navigate.
 *
 * @param {URL}      url        The URL to navigate to.
 * @param {Object}   context    The Interactivity context object.
 * @param {string[]} [flags=[]] Array of flag names to toggle during navigation.
 * @return {void}
 */
function* navigateWithFlags( url, context, flags = [] ) {
	context.loading = true; // Set loading state to true.
	flags.forEach( ( flag ) => ( context[ flag ] = true ) ); // Activate specified flags.
	const { actions } = yield import( '@wordpress/interactivity-router' ); // Yield the imported actions.
	yield actions.navigate( url.href ); // Yield the navigation action.
	context.loading = false; // Reset loading state.
	flags.forEach( ( flag ) => ( context[ flag ] = false ) ); // Deactivate specified flags.

	// Handle animations after navigation.
	setTimeout( () => {
		retrigerLoopBuilderAnimations();
	}, 200 ); // Wait for content to be loaded and visible.
}

/**
 * Resets pagination in the context and URL.
 *
 * @param {Object} context The Interactivity context object.
 * @param {URL}    url     The URL object to modify.
 * @return {void}
 */
const resetPagination = ( context, url ) => {
	context.page = 1;
	url.searchParams.delete( `${ context.urlPrefix }-page` );
};

store(
	'spectra-pro/loop-builder',
	{
		actions: {
			/**
			 * Handles navigation when a link is clicked.
			 *
			 * Validates the link and event, prevents default behavior, navigates to the href,
			 * and focuses the first anchor in the query block.
			 *
			 * @param {MouseEvent} e The click event triggered by the link.
			 */
			navigate: withSyncEvent( function* ( e ) {
				const context = getContext();
				const { ref } = getElement();
				const queryRef = ref.closest( '.wp-block-spectra-pro-loop-builder[data-wp-router-region]' );

				if ( isValidLink( ref ) && isValidEvent( e ) ) {
					e.preventDefault();

					// Check if this is a filter button by looking for the parent filter button block.
					const isFilterButton = ref.closest( '.wp-block-spectra-pro-loop-builder-child-filter-button' );

					if ( isFilterButton ) {
						// For filter buttons, update the URL and trigger filtering without full navigation.
						const url = new URL( ref.href );
						const filterParam = url.searchParams.get( `${ context.urlPrefix }-filter` );

						// Update context with new filter value.
						if ( filterParam ) {
							context.filter = filterParam;
						} else {
							context.filter = '';
						}

						// Reset pagination when filtering.
						resetPagination( context, url );

						// Navigate with filtering flag.
						yield* navigateWithFlags( url, context, [ 'filtering' ] );
					} else {
						// For other navigation (pagination, etc.), use the normal navigation
						const { actions } = yield import( '@wordpress/interactivity-router' );
						yield actions.navigate( ref.href );
						context.url = ref.href;

						// Focus the first anchor of the Query block.
						const firstAnchor = `.wp-block-spectra-pro-loop-builder-child-template a[href]`;
						queryRef.querySelector( firstAnchor )?.focus();
					}
				}
			} ),
			/**
			 * Prefetches a link's content on hover or focus.
			 *
			 * Preloads the linked content if the link is valid.
			 */
			*prefetch() {
				const { ref } = getElement();
				if ( isValidLink( ref ) ) {
					const { actions } = yield import( '@wordpress/interactivity-router' );
					yield actions.prefetch( ref.href );
				}
			},
			/**
			 * Handles search input changes with debouncing.
			 *
			 * Updates the search context, debounces the input, and navigates with the new search term.
			 *
			 * @param {Event} e The input event from the search field.
			 */
			onSearchInput: withSyncEvent( function* ( e ) {
				e.preventDefault();
				const { value } = e.currentTarget;
				const context = getContext();

				// Avoid navigation if nothing has changed.
				if ( value === context.search ) {
					return;
				}

				context.search = value;
				context.searching = true;

				try {
					yield debounce( 500 );
				} catch {
					return;
				}

				// Update URL parameter.
				const url = new URL( window.location.href );

				resetPagination( context, url ); // Reset pagination.

				updateUrlParam( url, `${ context.urlPrefix }-search`, value );

				yield* navigateWithFlags( url, context, [ 'searching' ] );
			} ),
			/**
			 * Clears the search input and navigates.
			 *
			 * Resets the search term and navigates to the updated URL on Enter or Space keypress.
			 *
			 * @param {Event} e The keydown or click event.
			 */
			clearSearch: withSyncEvent( function* ( e ) {
				// Only handle Enter or Space keydown events.
				if ( e?.type === 'keydown' && ! [ 'Enter', 'Space' ].includes( e.key ) ) {
					return true; // Allow other key events to proceed.
				}

				e.preventDefault(); // Prevent default behavior.
				const context = getContext();
				context.search = '';

				const url = new URL( window.location.href ); // Create URL object from current location.
				url.searchParams.delete( `${ context.urlPrefix }-page` ); // Clear pagination param.
				url.searchParams.delete( `${ context.urlPrefix }-search` ); // Clear search param.

				yield* navigateWithFlags( url, context, [ 'searching' ] ); // Delegate to navigation helper.
			} ),
			/**
			 * Handles sort input changes with debouncing.
			 *
			 * Updates the sort context, debounces the input, and navigates with the new sort value.
			 *
			 * @param {Event} e The input event from the sort field.
			 */
			onSortInput: withSyncEvent( function* ( e ) {
				e.preventDefault();
				const { value } = e.currentTarget;
				const context = getContext();

				// Only navigate if the sort value has changed.
				if ( value === context.sort ) {
					return;
				}

				context.sort = value;
				context.sorting = true;

				try {
					yield debounce( 500 );
				} catch {
					return;
				}

				const url = new URL( window.location.href );

				resetPagination( context, url ); // Reset pagination.

				updateUrlParam( url, `${ context.urlPrefix }-sort`, value );

				yield* navigateWithFlags( url, context, [ 'sorting' ] );
			} ),
			/**
			 * Handles single filter input changes with debouncing.
			 *
			 * Updates the filter context, debounces the input, and navigates with the new filter value.
			 *
			 * @param {Event} e The input event from the filter field.
			 */
			onFilterInput: withSyncEvent( function* ( e ) {
				e.preventDefault();
				const { value } = e.currentTarget;
				const context = getContext();

				// Only navigate if the filter value has changed.
				if ( value === context.filter ) {
					return;
				}

				context.filter = value;
				context.filtering = true;

				try {
					yield debounce( 500 ); // Yield a 500ms debounce promise.
				} catch {
					return;
				}

				const url = new URL( window.location.href );

				resetPagination( context, url ); // Reset pagination.

				updateUrlParam( url, `${ context.urlPrefix }-filter`, value );

				yield* navigateWithFlags( url, context, [ 'filtering' ] );
			} ),
			/**
			 * Handles multiple filter input changes (checkboxes) with debouncing.
			 *
			 * Updates the filters array in context based on checkbox state and navigates.
			 *
			 * @param {Event} e The change event from a checkbox.
			 */
			onFiltersInput: withSyncEvent( function* ( e ) {
				// Don't prevent default for checkboxes - let them update immediately.
				const { value, checked } = e.currentTarget;
				const context = getContext();

				// Parse taxonomy and filter value from the input (e.g., "category|<id>").
				const [ taxonomyType = '', cleanValue = '' ] = value.split( '|' );
				const filterParamKey = `${ context.urlPrefix }-filter`;

				// Initialize pending filters if not exists.
				if ( ! context.pendingFilters ) {
					context.pendingFilters = new Map();
				}

				// Store the latest state for this filter.
				context.pendingFilters.set( cleanValue, checked );

				try {
					yield debounce( 200, 'filter' ); // Use filter-specific debounce.
				} catch {
					return;
				}

				// Process all pending filter changes.
				const url = new URL( window.location.href );

				// Get current filter values from URL.
				let currentValues = [];
				const currentFilterParam = url.searchParams.get( filterParamKey );
				if ( currentFilterParam ) {
					const [ existingTaxonomy, values ] = currentFilterParam.split( '|' );
					if ( existingTaxonomy === taxonomyType && values ) {
						currentValues = values.split( ',' );
					}
				}

				// Apply all pending changes.
				context.pendingFilters.forEach( ( isChecked, filterValue ) => {
					if ( isChecked && ! currentValues.includes( filterValue ) ) {
						currentValues.push( filterValue );
					} else if ( ! isChecked ) {
						currentValues = currentValues.filter( ( val ) => val !== filterValue );
					}
				} );

				// Update context with final filter state.
				context.filters = currentValues;

				// Clear pending filters.
				context.pendingFilters.clear();

				// Rebuild the filter parameter.
				url.searchParams.delete( filterParamKey );
				if ( context.filters.length ) {
					const filterParamValue = `${ taxonomyType }|${ context.filters.join( ',' ) }`;
					url.searchParams.set( filterParamKey, filterParamValue );
				}

				resetPagination( context, url ); // Reset pagination.

				context.filtering = true;
				yield* navigateWithFlags( url, context, [ 'filtering' ] );
			} ),
			/**
			 * Clears all filters, search, and sort settings.
			 *
			 * Resets context and navigates to a clean URL, unchecking all filter checkboxes.
			 *
			 * @param {Event} e The click event.
			 */
			clearFilters: withSyncEvent( function* ( e ) {
				e.preventDefault();
				const context = getContext();

				// Reset all context values.
				context.search = '';
				context.filter = '';
				context.sort = '';
				context.filters = [];
				context.page = 1;

				const url = new URL( window.location.href ); // Create URL object from current location.
				url.searchParams.delete( `${ context.urlPrefix }-page` ); // Clear pagination param.
				url.searchParams.delete( `${ context.urlPrefix }-search` ); // Clear search param.
				url.searchParams.delete( `${ context.urlPrefix }-filter` ); // Clear filter param.
				url.searchParams.delete( `${ context.urlPrefix }-sort` ); // Clear sort param.

				yield* navigateWithFlags( url, context, [] ); // Delegate to navigation helper without flags.

				// Uncheck all filter checkboxes in the DOM.
				document
					.querySelectorAll(
						'.wp-block-spectra-pro-loop-builder-child-filter-checkbox input[name="spectra-term-filter[]"]'
					)
					.forEach( ( checkbox ) => {
						checkbox.checked = false;
					} );
			} ),
		},
		callbacks: {
			/**
			 * Prefetches content based on context URL on initialization or update.
			 */
			*prefetch() {
				const { url } = getContext(); // Get the current URL from context.
				const { ref } = getElement(); // Get the DOM element reference.

				// Check if prefetching is valid.
				if ( url && isValidLink( ref ) ) {
					const { actions } = yield import( '@wordpress/interactivity-router' );
					yield actions.prefetch( ref.href ); // Prefetch the content.
				}
			},
			/**
			 * Initializes filter checkbox state based on context.
			 *
			 * Sets the checked state of a checkbox based on whether its value is in the filters array.
			 */
			initFiltersInput() {
				const context = getContext(); // Get the current context.
				const { ref } = getElement(); // Get the DOM element reference (checkbox).
				const { value } = ref; // Get the checkbox value.

				// Sync checkbox state with context.
				ref.checked = context.filters.includes( value );
			},
		},
	},
	{ lock: true }
);

/**
 * Retrigger AOS animations for loop builder content after navigation.
 * This handles the case where new content is loaded via AJAX and animations need to be reinitialized.
 */
function retrigerLoopBuilderAnimations() {
	// Find all loop builder containers
	const loopBuilderContainers = document.querySelectorAll( '.wp-block-spectra-pro-loop-builder' );

	loopBuilderContainers.forEach( ( container ) => {
		// Find all animated elements in the container
		const animatedElements = container.querySelectorAll( '[data-aos]' );

		if ( animatedElements.length > 0 ) {
			// Hide animated elements immediately
			animatedElements.forEach( ( element ) => {
				element.style.visibility = 'hidden';
				element.classList.remove( 'aos-init', 'aos-animate' );
			} );

			// Show animated elements and reinitialize AOS
			setTimeout( () => {
				// Show all animated elements
				animatedElements.forEach( ( element ) => {
					element.style.visibility = 'visible';
				} );

				// Reinitialize AOS to trigger animations
				if ( typeof window.AOS !== 'undefined' ) {
					window.AOS.init();
				}
			}, 100 ); // Small delay to ensure elements are ready
		}
	} );
}
