import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import {
	ComboboxControl,
	FormTokenField,
	Spinner,
} from '@wordpress/components';

const getValueFromPath = ( item, path ) => {
	if ( ! path ) {
		return item;
	}

	return path.split( '.' ).reduce( ( accumulator, key ) => {
		if (
			accumulator &&
			typeof accumulator === 'object' &&
			key in accumulator
		) {
			return accumulator[ key ];
		}

		return undefined;
	}, item );
};

const mergeOptionSets = ( primary, secondary ) => {
	const map = new Map();

	primary.forEach( ( option ) => {
		if ( option?.value !== undefined ) {
			map.set( String( option.value ), option );
		}
	} );

	secondary.forEach( ( option ) => {
		if ( ! option || option.value === undefined ) {
			return;
		}

		const key = String( option.value );
		if ( ! map.has( key ) ) {
			map.set( key, option );
		}
	} );

	return Array.from( map.values() );
};

const normaliseOption = ( item, valueField, labelField ) => {
	const value = getValueFromPath( item, valueField ?? 'id' );
	const labelValue = getValueFromPath( item, labelField ?? 'name' );
	const label =
		typeof labelValue === 'string'
			? labelValue
			: labelValue?.rendered ?? labelValue?.title ?? value ?? '';

	return {
		value: value !== undefined && value !== null ? String( value ) : '',
		label: label !== undefined && label !== null ? String( label ) : '',
		item,
	};
};

const toArray = ( input ) => {
	if ( Array.isArray( input ) ) {
		return input;
	}

	if ( input === undefined || input === null || input === '' ) {
		return [];
	}

	return [ input ];
};

const toValueType = ( value, type = 'string' ) => {
	if ( type === 'number' ) {
		const parsed = Number( value );
		return Number.isNaN( parsed ) ? null : parsed;
	}

	if ( type === 'integer' ) {
		const parsed = parseInt( value, 10 );
		return Number.isNaN( parsed ) ? null : parsed;
	}

	return value;
};

const RemoteSelectField = ( { field, value, onChange } ) => {
	const namespace = field.namespace ?? 'wp/v2';
	const fieldType = field.type ?? '';
	let inferredResource = 'post-type';

	if ( fieldType === 'wp-taxonomy' ) {
		inferredResource = 'taxonomy';
	} else if ( fieldType === 'query-select' ) {
		inferredResource = 'custom';
	} else if ( fieldType === 'remote-select' ) {
		inferredResource = 'rest';
	}

	const resourceType = field.resource ?? inferredResource;
	const postType = field.postType ?? 'posts';
	const taxonomy = field.taxonomy ?? 'categories';
	const endpoint = useMemo( () => {
		if ( resourceType === 'taxonomy' || field.type === 'wp-taxonomy' ) {
			return `/${ namespace.replace( /^\//, '' ) }/${ taxonomy.replace(
				/^\//,
				''
			) }`;
		}

		if ( resourceType === 'custom' ) {
			return `/${ namespace.replace( /^\//, '' ) }/${
				field.endpoint?.replace( /^\//, '' ) ?? ''
			}`.replace( /\/+$/, '' );
		}

		if ( resourceType === 'rest' ) {
			return field.endpoint ?? '';
		}

		return `/${ namespace.replace( /^\//, '' ) }/${ (
			field.postType ?? postType
		).replace( /^\//, '' ) }`;
	}, [
		namespace,
		resourceType,
		taxonomy,
		field.endpoint,
		field.postType,
		postType,
		field.type,
	] );

	const isMultiple = field.multiple ?? false;
	const perPage = field.perPage ?? 20;
	const searchParam = field.searchParam ?? 'search';
	const valueType = field.valueType ?? 'string';
	const valueField = field.valueField ?? 'id';
	const labelField =
		field.labelField ??
		( resourceType === 'taxonomy' || field.type === 'wp-taxonomy'
			? 'name'
			: 'title.rendered' );

	const [ search, setSearch ] = useState( '' );
	const [ options, setOptions ] = useState( [] );
	const [ selectedOptions, setSelectedOptions ] = useState( [] );
	const [ isLoading, setIsLoading ] = useState( false );
	const [ loadError, setLoadError ] = useState( '' );

	const selectedIds = useMemo(
		() =>
			toArray( value )
				.map( ( item ) =>
					item !== undefined && item !== null ? String( item ) : ''
				)
				.filter( ( item ) => item !== '' ),
		[ value ]
	);

	const fetchItems = useCallback(
		async ( params = {} ) => {
			if ( ! endpoint ) {
				return [];
			}

			const query = new URLSearchParams();
			const include = params.include ?? [];

			if ( include.length > 0 ) {
				query.set( 'include', include.join( ',' ) );
				query.set( 'per_page', String( include.length ) );
			} else {
				query.set( 'per_page', String( perPage ) );
			}

			if ( params.search ) {
				query.set( searchParam, params.search );
			}

			const additionalFields =
				field.fields ??
				( resourceType === 'taxonomy' || field.type === 'wp-taxonomy'
					? 'id,name'
					: 'id,title.rendered' );
			query.set( '_fields', additionalFields );

			if ( field.queryArgs ) {
				Object.entries( field.queryArgs ).forEach( ( [ key, val ] ) => {
					if ( Array.isArray( val ) ) {
						val.forEach( ( item ) => query.append( key, item ) );
						return;
					}

					if ( val !== undefined && val !== null ) {
						query.set( key, val );
					}
				} );
			}

			const path = query.toString()
				? `${ endpoint }?${ query.toString() }`
				: endpoint;
			const response = await apiFetch( { path } );

			if ( ! Array.isArray( response ) ) {
				if ( Array.isArray( response?.items ) ) {
					return response.items;
				}

				return [];
			}

			return response;
		},
		[
			endpoint,
			field.fields,
			field.queryArgs,
			field.type,
			perPage,
			resourceType,
			searchParam,
		]
	);

	const mapItemsToOptions = useCallback(
		( items ) =>
			items
				.map( ( item ) =>
					normaliseOption( item, valueField, labelField )
				)
				.filter(
					( option ) => option.value !== '' && option.label !== ''
				),
		[ labelField, valueField ]
	);

	useEffect( () => {
		let isCancelled = false;

		const run = async () => {
			setIsLoading( true );
			try {
				const items = await fetchItems( { search } );
				if ( isCancelled ) {
					return;
				}

				const mapped = mapItemsToOptions( items );
				setOptions( mapped );
				setLoadError( '' );
			} catch ( error ) {
				if ( isCancelled ) {
					return;
				}

				setLoadError(
					error?.message ??
						__( 'Unable to load options.', 'cliq-store-locator' )
				);
				setOptions( [] );
			} finally {
				if ( ! isCancelled ) {
					setIsLoading( false );
				}
			}
		};

		run();

		return () => {
			isCancelled = true;
		};
	}, [ fetchItems, mapItemsToOptions, search ] );

	useEffect( () => {
		if ( selectedIds.length === 0 ) {
			setSelectedOptions( [] );
			return;
		}

		let isCancelled = false;

		const run = async () => {
			try {
				const items = await fetchItems( { include: selectedIds } );
				if ( isCancelled ) {
					return;
				}

				const mapped = mapItemsToOptions( items );
				setSelectedOptions( mapped );
			} catch ( error ) {
				if ( isCancelled ) {
					return;
				}

				setLoadError(
					error?.message ??
						__(
							'Unable to load selected options.',
							'cliq-store-locator'
						)
				);
			}
		};

		run();

		return () => {
			isCancelled = true;
		};
	}, [ fetchItems, mapItemsToOptions, selectedIds ] );

	const combinedOptions = useMemo(
		() => mergeOptionSets( selectedOptions, options ),
		[ options, selectedOptions ]
	);

	const handleSingleChange = useCallback(
		( newValue ) => {
			if (
				newValue === undefined ||
				newValue === null ||
				newValue === ''
			) {
				onChange( '' );
				return;
			}

			const converted = toValueType( newValue, valueType );
			onChange( converted ?? '' );
		},
		[ onChange, valueType ]
	);

	const labelToValueMap = useMemo(
		() =>
			combinedOptions.reduce( ( accumulator, option ) => {
				accumulator.set( option.label, option.value );
				return accumulator;
			}, new Map() ),
		[ combinedOptions ]
	);

	const handleMultipleChange = useCallback(
		( tokens ) => {
			const values = tokens
				.map( ( token ) => labelToValueMap.get( token ) ?? token )
				.map( ( token ) => toValueType( token, valueType ) )
				.filter(
					( token ) =>
						token !== null && token !== '' && token !== undefined
				);

			onChange( values );
		},
		[ labelToValueMap, onChange, valueType ]
	);

	const selectedValue = selectedIds[ 0 ] ?? '';
	const selectedTokenLabels = useMemo(
		() =>
			selectedIds.map( ( id ) => {
				const match = combinedOptions.find(
					( option ) => option.value === id
				);
				return match?.label ?? id;
			} ),
		[ combinedOptions, selectedIds ]
	);

	if ( ! endpoint ) {
		return (
			<div className="cliqthemes-ui-remote-select cliqthemes-ui-remote-select--error">
				<p className="cliqthemes-ui-field-help">
					{ field.help ??
						__(
							'No endpoint configured for this field.',
							'cliq-store-locator'
						) }
				</p>
			</div>
		);
	}

	return (
		<div
			className={ `cliqthemes-ui-remote-select${
				isLoading ? ' is-loading' : ''
			}` }
		>
			{ isMultiple ? (
				<FormTokenField
					label={ field.label }
					value={ selectedTokenLabels }
					suggestions={ combinedOptions.map(
						( option ) => option.label
					) }
					onChange={ handleMultipleChange }
					onInputChange={ setSearch }
					placeholder={ field.placeholder }
					__nextHasNoMarginBottom
				/>
			) : (
				<ComboboxControl
					label={ field.label }
					value={ selectedValue }
					options={ combinedOptions }
					onChange={ handleSingleChange }
					onFilterValueChange={ setSearch }
					placeholder={ field.placeholder }
				/>
			) }
			{ isLoading && (
				<div className="cliqthemes-ui-remote-select__spinner">
					<Spinner />
				</div>
			) }
			{ ( loadError || field.help ) && (
				<p className="cliqthemes-ui-field-help">
					{ loadError || field.help }
				</p>
			) }
		</div>
	);
};

export default RemoteSelectField;
