type Options<T> = {
	fetchUrl: (query: string) => Promise<T[]>;
	onSelect?: (item: T) => void;
	debounceMs?: number;
};

export function createTypeahead<T>(options: Options<T>) {
	const { fetchUrl, onSelect, debounceMs = 300 } = options;

	let query = $state('');
	let results: T[] = $state([]);
	let isOpen = $state(false);
	let loading = $state(false);
	let error = $state(false);
	let activeIndex = $state(-1);

	let debounceTimer: ReturnType<typeof setTimeout> | undefined;

	function setQuery(q: string) {
		query = q;
		isOpen = true;
		// Set loading before the debounce fires so callers can distinguish
		// "still searching" from "searched, zero hits" during the debounce window.
		loading = true;
		clearTimeout(debounceTimer);
		debounceTimer = setTimeout(async () => {
			error = false;
			try {
				results = await fetchUrl(q);
			} catch (e) {
				console.error('typeahead fetch error', e);
				results = [];
				error = true;
			} finally {
				loading = false;
			}
		}, debounceMs);
	}

	function close() {
		isOpen = false;
		activeIndex = -1;
		// Cancel the pending debounce — an abandoned query must not fire a stale
		// fetch after the dropdown closed, nor leave loading stuck on true.
		clearTimeout(debounceTimer);
		loading = false;
	}

	function setActiveIndex(idx: number) {
		activeIndex = idx;
	}

	function select(item: T) {
		onSelect?.(item);
		close();
	}

	/** Directly populate results without going through the debounce (e.g. on-focus preload). */
	function openWith(items: T[]) {
		results = items;
		isOpen = true;
	}

	return {
		get query() {
			return query;
		},
		get results() {
			return results;
		},
		get isOpen() {
			return isOpen;
		},
		get loading() {
			return loading;
		},
		get error() {
			return error;
		},
		get activeIndex() {
			return activeIndex;
		},
		setQuery,
		setActiveIndex,
		close,
		select,
		openWith
	};
}
