import { styleText } from 'node:util';
import type { Validate } from '@clack/core';
import { AutocompletePrompt, settings } from '@clack/core';
import {
	type CommonOptions,
	S_BAR,
	S_BAR_END,
	S_CHECKBOX_INACTIVE,
	S_CHECKBOX_SELECTED,
	S_RADIO_ACTIVE,
	S_RADIO_INACTIVE,
	symbol,
} from './common.js';
import { limitOptions } from './limit-options.js';
import type { Option } from './select.js';

function getLabel<T>(option: Option<T>) {
	return option.label ?? String(option.value ?? '');
}

function getFilteredOption<T>(searchText: string, option: Option<T>): boolean {
	if (!searchText) {
		return true;
	}
	const label = (option.label ?? String(option.value ?? '')).toLowerCase();
	const hint = (option.hint ?? '').toLowerCase();
	const value = String(option.value).toLowerCase();
	const term = searchText.toLowerCase();

	return label.includes(term) || hint.includes(term) || value.includes(term);
}

function getSelectedOptions<T>(values: T[], options: Option<T>[]): Option<T>[] {
	const results: Option<T>[] = [];

	for (const option of options) {
		if (values.includes(option.value)) {
			results.push(option);
		}
	}

	return results;
}

/**
 * Options for the {@link autocomplete} prompt.
 */
interface AutocompleteSharedOptions<Value> extends CommonOptions {
	/**
	 * The message or question shown to the user above the input.
	 */
	message: string;

	/**
	 * The options to present, or a function that returns the options to present
	 * allowing for custom search/filtering.
	 *
	 * @see https://bomb.sh/docs/clack/packages/prompts/#dynamic-options-getter
	 */
	options: Option<Value>[] | ((this: AutocompletePrompt<Option<Value>>) => Option<Value>[]);

	/**
	 * The maximum number of items/options to display in the autocomplete list at once.
	 */
	maxItems?: number;

	/**
	 * Placeholder text displayed when the search field is empty. When set, pressing
	 * tab copies the placeholder into the input.
	 */
	placeholder?: string;

	/**
	 * A function or a [Standard Schema](https://github.com/standard-schema/standard-schema)
	 * that validates user input. If a custom function is given, you should return a `string` or `Error`
	 * to show as a validation error, or `undefined` to accept the result.
	 */
	validate?: Validate<Value | Value[]>;

	/**
	 * Custom filter function to match options against the search input.
	 */
	filter?: (search: string, option: Option<Value>) => boolean;
}

export interface AutocompleteOptions<Value> extends AutocompleteSharedOptions<Value> {
	/**
	 * The initially selected option from the list.
	 */
	initialValue?: Value;

	/**
	 * The starting value shown in the users input box.
	 */
	initialUserInput?: string;
}

/**
 * The `autocomplete` prompt combines a text input with a searchable list of options.
 * It's perfect for when you have a large list of options and want to help users
 * find what they're looking for quickly.
 *
 * @see https://bomb.sh/docs/clack/packages/prompts/#autocomplete
 *
 * @example
 * ```ts
 * import { autocomplete } from '@clack/prompts';
 *
 * const framework = await autocomplete({
 *   message: 'Search for a framework',
 *   options: [
 *     { value: 'next', label: 'Next.js', hint: 'React framework' },
 *     { value: 'astro', label: 'Astro', hint: 'Content-focused' },
 *     { value: 'svelte', label: 'SvelteKit', hint: 'Compile-time framework' },
 *     { value: 'remix', label: 'Remix', hint: 'Full stack framework' },
 *     { value: 'nuxt', label: 'Nuxt', hint: 'Vue framework' },
 *   ],
 *   placeholder: 'Type to search...',
 *   maxItems: 5,
 * });
 * ```
 */
export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
	const prompt = new AutocompletePrompt({
		options: opts.options,
		initialValue: opts.initialValue ? [opts.initialValue] : undefined,
		initialUserInput: opts.initialUserInput,
		placeholder: opts.placeholder,
		filter:
			opts.filter ??
			((search: string, opt: Option<Value>) => {
				return getFilteredOption(search, opt);
			}),
		signal: opts.signal,
		input: opts.input,
		output: opts.output,
		validate: opts.validate,
		render() {
			const hasGuide = opts.withGuide ?? settings.withGuide;
			// Title and message display
			const headings = hasGuide
				? [`${styleText('gray', S_BAR)}`, `${symbol(this.state)}  ${opts.message}`]
				: [`${symbol(this.state)}  ${opts.message}`];
			const userInput = this.userInput;
			const options = this.options;
			const placeholder = opts.placeholder;
			const showPlaceholder = userInput === '' && placeholder !== undefined;
			const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'disabled') => {
				const label = getLabel(option);
				const hint =
					option.hint && option.value === this.focusedValue
						? styleText('dim', ` (${option.hint})`)
						: '';
				switch (state) {
					case 'active':
						return `${styleText('green', S_RADIO_ACTIVE)} ${label}${hint}`;
					case 'inactive':
						return `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}`;
					case 'disabled':
						return `${styleText('gray', S_RADIO_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`;
				}
			};

			// Handle different states
			switch (this.state) {
				case 'submit': {
					// Show selected value
					const selected = getSelectedOptions(this.selectedValues, options);
					const label =
						selected.length > 0 ? `  ${styleText('dim', selected.map(getLabel).join(', '))}` : '';
					const submitPrefix = hasGuide ? styleText('gray', S_BAR) : '';
					return `${headings.join('\n')}\n${submitPrefix}${label}`;
				}

				case 'cancel': {
					const userInputText = userInput
						? `  ${styleText(['strikethrough', 'dim'], userInput)}`
						: '';
					const cancelPrefix = hasGuide ? styleText('gray', S_BAR) : '';
					return `${headings.join('\n')}\n${cancelPrefix}${userInputText}`;
				}

				default: {
					const barStyle = this.state === 'error' ? 'yellow' : 'cyan';
					const guidePrefix = hasGuide ? `${styleText(barStyle, S_BAR)}  ` : '';
					const guidePrefixEnd = hasGuide ? styleText(barStyle, S_BAR_END) : '';
					// Display cursor position - show plain text in navigation mode
					let searchText = '';
					if (this.isNavigating || showPlaceholder) {
						const searchTextValue = showPlaceholder ? placeholder : userInput;
						searchText = searchTextValue !== '' ? ` ${styleText('dim', searchTextValue)}` : '';
					} else {
						searchText = ` ${this.userInputWithCursor}`;
					}

					// Show match count if filtered
					const matches =
						this.filteredOptions.length !== options.length
							? styleText(
									'dim',
									` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`
								)
							: '';

					// No matches message
					const noResults =
						this.filteredOptions.length === 0 && userInput
							? [`${guidePrefix}${styleText('yellow', 'No matches found')}`]
							: [];

					const validationError =
						this.state === 'error' ? [`${guidePrefix}${styleText('yellow', this.error)}`] : [];

					if (hasGuide) {
						headings.push(`${guidePrefix.trimEnd()}`);
					}
					headings.push(
						`${guidePrefix}${styleText('dim', 'Search:')}${searchText}${matches}`,
						...noResults,
						...validationError
					);

					// Show instructions
					const instructions = [
						`${styleText('dim', '↑/↓')} to select`,
						`${styleText('dim', 'Enter:')} confirm`,
						`${styleText('dim', 'Type:')} to search`,
					];

					const footers = [`${guidePrefix}${instructions.join(' • ')}`, guidePrefixEnd];

					// Render options with selection
					const displayOptions =
						this.filteredOptions.length === 0
							? []
							: limitOptions({
									cursor: this.cursor,
									options: this.filteredOptions,
									columnPadding: hasGuide ? 3 : 0, // for `|  ` when guide is shown
									rowPadding: headings.length + footers.length,
									style: (option, active) => {
										return opt(
											option,
											option.disabled ? 'disabled' : active ? 'active' : 'inactive'
										);
									},
									maxItems: opts.maxItems,
									output: opts.output,
								});

					// Return the formatted prompt
					return [
						...headings,
						...displayOptions.map((option) => `${guidePrefix}${option}`),
						...footers,
					].join('\n');
				}
			}
		},
	});

	// Return the result or cancel symbol
	return prompt.prompt() as Promise<Value | symbol>;
};

/**
 * Options for the {@link autocompleteMultiselect} prompt
 */
export interface AutocompleteMultiSelectOptions<Value> extends AutocompleteSharedOptions<Value> {
	/**
	 * The initially selected option(s) from the list.
	 */
	initialValues?: Value[];

	/**
	 * When `true` at least one option must be selected.
	 * @default false
	 */
	required?: boolean;
}

/**
 * The `autocompleteMultiselect` prompt combines the search functionality of autocomplete
 * with the ability to select multiple options.
 *
 * @see https://bomb.sh/docs/clack/packages/prompts/#autocomplete-multiselect
 *
 * @example
 * ```ts
 * import { autocompleteMultiselect } from '@clack/prompts';
 *
 * const frameworks = await autocompleteMultiselect({
 *   message: 'Select frameworks',
 *   options: [
 *     { value: 'next', label: 'Next.js', hint: 'React framework' },
 *     { value: 'astro', label: 'Astro', hint: 'Content-focused' },
 *     { value: 'svelte', label: 'SvelteKit', hint: 'Compile-time framework' },
 *     { value: 'remix', label: 'Remix', hint: 'Full stack framework' },
 *     { value: 'nuxt', label: 'Nuxt', hint: 'Vue framework' },
 *   ],
 *   placeholder: 'Type to search...',
 *   maxItems: 5,
 * });
 * ```
 */
export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOptions<Value>) => {
	const formatOption = (
		option: Option<Value>,
		active: boolean,
		selectedValues: Value[],
		focusedValue: Value | undefined
	) => {
		const isSelected = selectedValues.includes(option.value);
		const label = option.label ?? String(option.value ?? '');
		const hint =
			option.hint && focusedValue !== undefined && option.value === focusedValue
				? styleText('dim', ` (${option.hint})`)
				: '';
		const checkbox = isSelected
			? styleText('green', S_CHECKBOX_SELECTED)
			: styleText('dim', S_CHECKBOX_INACTIVE);

		if (option.disabled) {
			return `${styleText('gray', S_CHECKBOX_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`;
		}
		if (active) {
			return `${checkbox} ${label}${hint}`;
		}
		return `${checkbox} ${styleText('dim', label)}`;
	};

	// Create text prompt which we'll use as foundation
	const prompt = new AutocompletePrompt<Option<Value>>({
		options: opts.options,
		multiple: true,
		placeholder: opts.placeholder,
		filter:
			opts.filter ??
			((search, opt) => {
				return getFilteredOption(search, opt);
			}),
		validate: () => {
			if (opts.required && prompt.selectedValues.length === 0) {
				return 'Please select at least one item';
			}
			return undefined;
		},
		initialValue: opts.initialValues,
		signal: opts.signal,
		input: opts.input,
		output: opts.output,
		render() {
			const hasGuide = opts.withGuide ?? settings.withGuide;
			// Title and symbol
			const title = `${hasGuide ? `${styleText('gray', S_BAR)}\n` : ''}${symbol(this.state)}  ${
				opts.message
			}\n`;

			// Selection counter
			const userInput = this.userInput;
			const placeholder = opts.placeholder;
			const showPlaceholder = userInput === '' && placeholder !== undefined;

			// Search input display
			const searchText =
				this.isNavigating || showPlaceholder
					? styleText('dim', showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode
					: this.userInputWithCursor;

			const options = this.options;

			const matches =
				this.filteredOptions.length !== options.length
					? styleText(
							'dim',
							` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})`
						)
					: '';

			// Render prompt state
			switch (this.state) {
				case 'submit': {
					return `${title}${hasGuide ? `${styleText('gray', S_BAR)}  ` : ''}${styleText(
						'dim',
						`${this.selectedValues.length} items selected`
					)}`;
				}
				case 'cancel': {
					return `${title}${hasGuide ? `${styleText('gray', S_BAR)}  ` : ''}${styleText(
						['strikethrough', 'dim'],
						userInput
					)}`;
				}
				default: {
					const barStyle = this.state === 'error' ? 'yellow' : 'cyan';
					const guidePrefix = hasGuide ? `${styleText(barStyle, S_BAR)}  ` : '';
					const guidePrefixEnd = hasGuide ? styleText(barStyle, S_BAR_END) : '';
					// Instructions
					const instructions = [
						`${styleText('dim', '↑/↓')} to navigate`,
						`${styleText('dim', this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`,
						`${styleText('dim', 'Enter:')} confirm`,
						`${styleText('dim', 'Type:')} to search`,
					];

					// No results message
					const noResults =
						this.filteredOptions.length === 0 && userInput
							? [`${guidePrefix}${styleText('yellow', 'No matches found')}`]
							: [];

					const errorMessage =
						this.state === 'error' ? [`${guidePrefix}${styleText('yellow', this.error)}`] : [];

					// Calculate header and footer line counts for rowPadding
					const headerLines = [
						...`${title}${hasGuide ? styleText(barStyle, S_BAR) : ''}`.split('\n'),
						`${guidePrefix}${styleText('dim', 'Search:')} ${searchText}${matches}`,
						...noResults,
						...errorMessage,
					];
					const footerLines = [`${guidePrefix}${instructions.join(' • ')}`, guidePrefixEnd];

					// Get limited options for display
					const displayOptions = limitOptions({
						cursor: this.cursor,
						options: this.filteredOptions,
						style: (option, active) =>
							formatOption(option, active, this.selectedValues, this.focusedValue),
						maxItems: opts.maxItems,
						output: opts.output,
						rowPadding: headerLines.length + footerLines.length,
					});

					// Build the prompt display
					return [
						...headerLines,
						...displayOptions.map((option) => `${guidePrefix}${option}`),
						...footerLines,
					].join('\n');
				}
			}
		},
	});

	// Return the result or cancel symbol
	return prompt.prompt() as Promise<Value[] | symbol>;
};
