/**
 * External dependencies
 */
import { FormTokenField } from '@wordpress/components';
import { useDebounce } from '@wordpress/compose';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { useState, useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import {
	getEntitiesInfo,
	mapToIHasNameAndId,
} from '@spectra-pro-helpers/loop-builder';

/**
 * An empty array constant used as a default value.
 *
 * @type {Array}
 */
const EMPTY_ARRAY = [];

/**
 * Base query parameters for fetching entities.
 *
 * Defines default settings for ordering and fields to retrieve.
 *
 * @type {Object}
 */
const BASE_QUERY = {
	order: 'asc',
	_fields: 'id,title',
	context: 'view',
};

/**
 * Renders a token field for selecting parents.
 *
 * This component is used to select parent entities for the loop block.
 * It uses the `FormTokenField` component from `@wordpress/components`.
 *
 * @param {Object} props The component props.
 *
 * @return {Element} The component.
 */
const ParentControl = ( props ) => {
	const { parents, postType, onChange } = props;
	// State for the current search term entered by the user.
	const [ search, setSearch ] = useState( '' );
	// State for the current value displayed in the token field (array of { id, value } objects).
	const [ value, setValue ] = useState( EMPTY_ARRAY );
	// State for the list of suggestions displayed in the token field.
	const [ suggestions, setSuggestions ] = useState( EMPTY_ARRAY );
	// Debounced version of setSearch to limit the frequency of search updates.
	const debouncedSearch = useDebounce( setSearch, 250 );
	// Fetch search results and resolution status for parent suggestions based on the search term.
	const { searchResults, searchHasResolved } = useSelect(
		( select ) => {
			// If no search term is provided, return empty results and mark as resolved.
			if ( ! search ) {
				return { searchResults: EMPTY_ARRAY, searchHasResolved: true };
			}

			const { getEntityRecords, hasFinishedResolution } =
				select( coreStore );

			// Query arguments for fetching search results, excluding current parents.
			const selectorArgs = [
				'postType',
				postType,
				{
					...BASE_QUERY,
					search,
					orderby: 'relevance',
					exclude: parents,
					per_page: 20,
				},
			];

			return {
				// Fetch entities matching the search term.
				searchResults: getEntityRecords( ...selectorArgs ),
				// Check if the search query has fully resolved.
				searchHasResolved: hasFinishedResolution(
					'getEntityRecords',
					selectorArgs
				),
			};
		},
		[ search, parents ] // Dependencies for re-running the selector.
	);

	// Fetch details of the currently selected parent entities.
	const currentParents = useSelect(
		( select ) => {
			// If no parents are selected, return an empty array.
			if ( ! parents?.length ) {
				return EMPTY_ARRAY;
			}

			const { getEntityRecords } = select( coreStore );

			// Fetch details for the currently selected parent IDs.
			return getEntityRecords( 'postType', postType, {
				...BASE_QUERY,
				include: parents,
				per_page: parents.length,
			} );
		},
		[ parents ] // Dependency for re-running when parents change.
	);
	// Update the `value` state only after the selectors are resolved
	// to avoid emptying the input when we're changing parents.
	useEffect( () => {
		// If no parents are selected, reset the value to an empty array.
		if ( ! parents?.length ) {
			setValue( EMPTY_ARRAY );
		}

		// Wait until current parent data is available before proceeding.
		if ( ! currentParents?.length ) {
			return;
		}

		// Process the fetched parent entities into a usable format.
		const currentParentsInfo = getEntitiesInfo(
			mapToIHasNameAndId( currentParents, 'title.rendered' )
		);

		// Returns only the existing entity ids. This prevents the component
		// from crashing in the editor, when non existing ids are provided.
		const sanitizedValue = parents.reduce( ( accumulator, id ) => {
			const entity = currentParentsInfo.mapById[ id ];

			if ( entity ) {
				accumulator.push( {
					id,
					value: entity.name,
				} );
			}

			return accumulator;
		}, [] );
		// Update the token field with the sanitized value.
		setValue( sanitizedValue );
	}, [ parents, currentParents ] );

	// Memoize entity info for search results to optimize performance.
	const entitiesInfo = useMemo( () => {
		// If no search results are available, return an empty array.
		if ( ! searchResults?.length ) {
			return EMPTY_ARRAY;
		}

		// Transform search results into a format with IDs and names.
		return getEntitiesInfo(
			mapToIHasNameAndId( searchResults, 'title.rendered' )
		);
	}, [ searchResults ] );
	// Update suggestions when the search query resolves.
	useEffect( () => {
		// Wait until the search has resolved before updating suggestions.
		if ( ! searchHasResolved ) {
			return;
		}

		// Set the suggestions to the names of the resolved entities.
		setSuggestions( entitiesInfo.names );
	}, [ entitiesInfo.names, searchHasResolved ] );

	// Retrieves the ID of an entity based on its value (name).
	const getIdByValue = ( entitiesMappedByName, entity ) => {
		const id = entity?.id || entitiesMappedByName?.[ entity ]?.id;

		if ( id ) {
			return id;
		}
	};

	/**
	 * Handles the change event of the token field.
	 *
	 * Converts the new token field value into an array of valid entity IDs,
	 * clears suggestions, and calls the `onChange` callback with the updated IDs.
	 *
	 * @param {Array} newValue - The new array of entity names or objects from the token field.
	 */
	const onParentChange = ( newValue ) => {
		// Convert the new value into a Set of valid entity IDs, removing duplicates.
		const ids = Array.from(
			newValue.reduce( ( accumulator, entity ) => {
				// Verify that each new value corresponds to an existing entity.
				const id = getIdByValue( entitiesInfo.mapByName, entity );

				if ( id ) {
					accumulator.add( id );
				}

				return accumulator;
			}, new Set() )
		);

		// Clear suggestions after a change to reset the dropdown.
		setSuggestions( EMPTY_ARRAY );

		// Update the query with the new array of parent IDs.
		onChange( { parents: ids } );
	};
	return (
		<FormTokenField
			__next40pxDefaultSize
			label={ __( 'Parents', 'spectra-pro' ) }
			value={ value }
			onInputChange={ debouncedSearch }
			suggestions={ suggestions }
			onChange={ onParentChange }
			__experimentalShowHowTo={ false }
			__nextHasNoMarginBottom
		/>
	);
};

export default ParentControl;
