import { styleText } from 'node:util';
import { SelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
import {
	type CommonOptions,
	formatInstructionFooter,
	S_BAR,
	S_BAR_END,
	S_RADIO_ACTIVE,
	S_RADIO_INACTIVE,
	symbol,
	symbolBar,
} from './common.js';
import { limitOptions } from './limit-options.js';

export const SELECT_INSTRUCTIONS = [
	`${styleText('dim', '↑/↓')} to navigate`,
	`${styleText('dim', 'Enter:')} confirm`,
];

type Primitive = Readonly<string | boolean | number>;

export type Option<Value> = Value extends Primitive
	? {
			/**
			 * Internal data for this option.
			 */
			value: Value;
			/**
			 * The optional, user-facing text for this option.
			 *
			 * By default, the `value` is converted to a string.
			 */
			label?: string;
			/**
			 * An optional hint to display to the user when
			 * this option might be selected.
			 *
			 * By default, no `hint` is displayed.
			 */
			hint?: string;
			/**
			 * Whether this option is disabled.
			 * Disabled options are visible but cannot be selected.
			 *
			 * By default, options are not disabled.
			 */
			disabled?: boolean;
		}
	: {
			/**
			 * Internal data for this option.
			 */
			value: Value;
			/**
			 * Required. The user-facing text for this option.
			 */
			label: string;
			/**
			 * An optional hint to display to the user when
			 * this option might be selected.
			 *
			 * By default, no `hint` is displayed.
			 */
			hint?: string;
			/**
			 * Whether this option is disabled.
			 * Disabled options are visible but cannot be selected.
			 *
			 * By default, options are not disabled.
			 */
			disabled?: boolean;
		};

export interface SelectOptions<Value> extends CommonOptions {
	message: string;
	options: Option<Value>[];
	initialValue?: Value;
	maxItems?: number;
	/**
	 * Show keyboard instructions below the option list.
	 * @default true
	 */
	showInstructions?: boolean;
}

const computeLabel = (label: string, format: (text: string) => string) => {
	if (!label.includes('\n')) {
		return format(label);
	}
	return label
		.split('\n')
		.map((line) => format(line))
		.join('\n');
};

export const select = <Value>(opts: SelectOptions<Value>) => {
	const opt = (
		option: Option<Value>,
		state: 'inactive' | 'active' | 'selected' | 'cancelled' | 'disabled'
	) => {
		const label = option.label ?? String(option.value);
		switch (state) {
			case 'disabled':
				return `${styleText('gray', S_RADIO_INACTIVE)} ${computeLabel(label, (text) => styleText('gray', text))}${
					option.hint ? ` ${styleText('dim', `(${option.hint ?? 'disabled'})`)}` : ''
				}`;
			case 'selected':
				return `${computeLabel(label, (text) => styleText('dim', text))}`;
			case 'active':
				return `${styleText('green', S_RADIO_ACTIVE)} ${label}${
					option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : ''
				}`;
			case 'cancelled':
				return `${computeLabel(label, (str) => styleText(['strikethrough', 'dim'], str))}`;
			default:
				return `${styleText('dim', S_RADIO_INACTIVE)} ${computeLabel(label, (text) => styleText('dim', text))}`;
		}
	};

	const showInstructions = opts.showInstructions ?? true;

	return new SelectPrompt({
		options: opts.options,
		signal: opts.signal,
		input: opts.input,
		output: opts.output,
		initialValue: opts.initialValue,
		render() {
			const hasGuide = opts.withGuide ?? settings.withGuide;
			const titlePrefix = `${symbol(this.state)}  `;
			const titlePrefixBar = `${symbolBar(this.state)}  `;
			const messageLines = wrapTextWithPrefix(
				opts.output,
				opts.message,
				titlePrefixBar,
				titlePrefix
			);
			const title = `${hasGuide ? `${styleText('gray', S_BAR)}\n` : ''}${messageLines}\n`;

			switch (this.state) {
				case 'submit': {
					const submitPrefix = hasGuide ? `${styleText('gray', S_BAR)}  ` : '';
					const wrappedLines = wrapTextWithPrefix(
						opts.output,
						opt(this.options[this.cursor], 'selected'),
						submitPrefix
					);
					return `${title}${wrappedLines}`;
				}
				case 'cancel': {
					const cancelPrefix = hasGuide ? `${styleText('gray', S_BAR)}  ` : '';
					const wrappedLines = wrapTextWithPrefix(
						opts.output,
						opt(this.options[this.cursor], 'cancelled'),
						cancelPrefix
					);
					return `${title}${wrappedLines}${hasGuide ? `\n${styleText('gray', S_BAR)}` : ''}`;
				}
				default: {
					const prefix = hasGuide ? `${styleText('cyan', S_BAR)}  ` : '';
					const titleLineCount = title.split('\n').length;
					const footerLines = showInstructions
						? formatInstructionFooter(SELECT_INSTRUCTIONS, hasGuide)
						: hasGuide
							? [styleText('cyan', S_BAR_END)]
							: [];
					const footerText = footerLines.join('\n');
					const footerLineCount = footerLines.length + 1;
					return `${title}${prefix}${limitOptions({
						output: opts.output,
						cursor: this.cursor,
						options: this.options,
						maxItems: opts.maxItems,
						columnPadding: prefix.length,
						rowPadding: titleLineCount + footerLineCount,
						style: (item, active) =>
							opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'),
					}).join(`\n${prefix}`)}\n${footerText}\n`;
				}
			}
		},
	}).prompt() as Promise<Value | symbol>;
};
