/**
 * AsyncTokenSelect — a multi-value, REST-backed token field.
 *
 * The Gutenberg equivalent of the Elementor `arraysubs_ajax_select` control:
 * a searchable multi-select that queries the ArraySubs select-options REST
 * endpoint. FormTokenField works in LABEL space, so we keep an id→label map,
 * store IDs in the attribute, and convert both ways. Saved IDs are preloaded
 * via the endpoint's `include` param so their chips render with real labels.
 *
 * Response items are { value, label } (NOT { id, text } — confirmed against the
 * Elementor control view and the REST controller).
 */
import { useState, useEffect, useRef, useMemo } from '@wordpress/element';
import { FormTokenField } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';

const ROUTE =
	( window.ArraySubsGutenberg && window.ArraySubsGutenberg.restPath ) ||
	'/arraysubs/v1/members-access/select-options';

/**
 * Pull the option rows out of the REST response (supports `data` or `options`).
 *
 * @param {Object} response REST response.
 * @return {Array} Array of { value, label } rows.
 */
function rowsOf( response ) {
	return ( response && ( response.data || response.options ) ) || [];
}

export default function AsyncTokenSelect( {
	label,
	type,
	query = {},
	ids = [],
	onChangeIds,
} ) {
	// id (string) -> label.
	const [ map, setMap ] = useState( {} );
	const [ suggestions, setSuggestions ] = useState( [] );
	const debounce = useRef();

	const ingest = ( rows ) =>
		setMap( ( prev ) => {
			const next = { ...prev };
			rows.forEach( ( r ) => {
				next[ String( r.value ) ] = r.label;
			} );
			return next;
		} );

	// On mount: (1) preload labels for saved IDs so their chips render, and
	// (2) seed the suggestion list with the latest items (empty search) so the
	// dropdown isn't empty before the user types — the endpoint returns the 10
	// most-recently-created items for an empty search.
	useEffect( () => {
		if ( ids.length ) {
			apiFetch( {
				path: addQueryArgs( ROUTE, {
					type,
					include: ids.join( ',' ),
					...query,
				} ),
			} )
				.then( ( res ) => ingest( rowsOf( res ) ) )
				.catch( () => {} );
		}

		apiFetch( {
			path: addQueryArgs( ROUTE, { type, search: '', ...query } ),
		} )
			.then( ( res ) => {
				const rows = rowsOf( res );
				ingest( rows );
				setSuggestions(
					rows.map( ( r ) => `${ r.label } (#${ r.value })` )
				);
			} )
			.catch( () => {} );
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [] );

	// Tokens are displayed as "Label (#id)" so two items that resolve to the same
	// label (e.g. duplicate product names, or variations with no attribute
	// summary) stay distinct. Without the id suffix FormTokenField — which keys
	// tokens by their displayed string — would merge them and silently drop one
	// of the saved IDs on the next edit.
	const display = ( id ) => {
		const label = map[ String( id ) ];
		return label ? `${ label } (#${ id })` : String( id );
	};

	const onInputChange = ( term ) => {
		clearTimeout( debounce.current );
		debounce.current = setTimeout( () => {
			apiFetch( {
				path: addQueryArgs( ROUTE, {
					type,
					search: term || '',
					...query,
				} ),
			} )
				.then( ( res ) => {
					const rows = rowsOf( res );
					ingest( rows );
					setSuggestions(
						rows.map( ( r ) => `${ r.label } (#${ r.value })` )
					);
				} )
				.catch( () => {} );
		}, 300 );
	};

	// Attribute IDs -> visible "Label (#id)" tokens.
	const value = useMemo( () => ids.map( display ), [ ids, map ] );

	// Disambiguated "Label (#id)" string -> id, used as a fallback when a token
	// has no parseable suffix (defensive; the suffix is the primary id carrier).
	const labelToId = useMemo( () => {
		const out = {};
		Object.keys( map ).forEach( ( id ) => {
			out[ `${ map[ id ] } (#${ id })` ] = id;
		} );
		return out;
	}, [ map ] );

	const onChange = ( tokens ) => {
		const nextIds = tokens
			.map( ( token ) => {
				// FormTokenField may hand back the raw string or a { value } object.
				const raw =
					token && typeof token === 'object' ? token.value : token;
				const str = String( raw );
				// Primary: pull the id out of the trailing "(#id)" suffix.
				const match = str.match( /\(#(\d+)\)\s*$/ );
				if ( match ) {
					return parseInt( match[ 1 ], 10 );
				}
				// Fallbacks: known disambiguated label, then a bare numeric id.
				const resolved =
					labelToId[ str ] !== undefined ? labelToId[ str ] : str;
				return parseInt( resolved, 10 );
			} )
			.filter( ( n ) => ! Number.isNaN( n ) && n > 0 );

		// De-dupe while preserving order (a user can't meaningfully pick the same id twice).
		onChangeIds( [ ...new Set( nextIds ) ] );
	};

	return (
		<FormTokenField
			__next40pxDefaultSize
			__experimentalExpandOnFocus
			label={ label }
			value={ value }
			suggestions={ suggestions }
			onInputChange={ onInputChange }
			onChange={ onChange }
			maxSuggestions={ 20 }
		/>
	);
}
