import chalk from 'chalk';

import type {InputSetting, Prompter, Result, RuleEntry} from './types';

import format from './format';
import getForcedCaseFn from './get-forced-case-fn';
import getForcedLeadingFn from './get-forced-leading-fn';
import meta from './meta';
import {
	enumRuleIsActive,
	ruleIsNotApplicable,
	ruleIsApplicable,
	ruleIsActive,
	getHasName,
	getMaxLength,
} from './utils';

/**
 * Get a cli prompt based on rule configuration
 * @param type type of the data to gather
 * @param context rules to parse
 * @return prompt instance
 */
export default function getPrompt(
	type: string,
	context: {
		rules?: RuleEntry[];
		settings?: InputSetting;
		results?: Result;
		prompter?: () => Prompter;
	} = {}
): Promise<string | undefined> {
	const {rules = [], settings = {}, results = {}, prompter} = context;

	if (typeof prompter !== 'function') {
		throw new TypeError('Missing prompter function in getPrompt context');
	}

	const prompt = prompter();

	if (typeof prompt.removeAllListeners !== 'function') {
		throw new TypeError(
			'getPrompt: prompt.removeAllListeners is not a function'
		);
	}

	if (typeof prompt.command !== 'function') {
		throw new TypeError('getPrompt: prompt.command is not a function');
	}

	if (typeof prompt.catch !== 'function') {
		throw new TypeError('getPrompt: prompt.catch is not a function');
	}

	if (typeof prompt.addListener !== 'function') {
		throw new TypeError('getPrompt: prompt.addListener is not a function');
	}

	if (typeof prompt.log !== 'function') {
		throw new TypeError('getPrompt: prompt.log is not a function');
	}

	if (typeof prompt.delimiter !== 'function') {
		throw new TypeError('getPrompt: prompt.delimiter is not a function');
	}

	if (typeof prompt.show !== 'function') {
		throw new TypeError('getPrompt: prompt.show is not a function');
	}

	const enumRule = rules.filter(getHasName('enum')).find(enumRuleIsActive);

	const emptyRule = rules.find(getHasName('empty'));

	const mustBeEmpty =
		emptyRule && ruleIsActive(emptyRule) && ruleIsApplicable(emptyRule);

	const mayNotBeEmpty =
		emptyRule && ruleIsActive(emptyRule) && ruleIsNotApplicable(emptyRule);

	const mayBeEmpty = !mayNotBeEmpty;

	if (mustBeEmpty) {
		prompt.removeAllListeners('keypress');
		prompt.removeAllListeners('client_prompt_submit');
		prompt.ui.redraw.done();
		return Promise.resolve(undefined);
	}

	const caseRule = rules.find(getHasName('case'));

	const forceCaseFn = getForcedCaseFn(caseRule);

	const leadingBlankRule = rules.find(getHasName('leading-blank'));

	const forceLeadingBlankFn = getForcedLeadingFn(leadingBlankRule);

	const maxLengthRule = rules.find(getHasName('max-length'));
	const inputMaxLength = getMaxLength(maxLengthRule);

	const headerLength = settings.header ? settings.header.length : Infinity;

	const remainingHeaderLength = headerLength
		? headerLength -
		  [
				results.type,
				results.scope,
				results.scope ? '()' : '',
				results.type && results.scope ? ':' : '',
				results.subject,
		  ].join('').length
		: Infinity;

	const maxLength = Math.min(inputMaxLength, remainingHeaderLength);

	return new Promise((resolve) => {
		// Add the defined enums as sub commands if applicable
		if (enumRule) {
			const [, [, , enums]] = enumRule;

			enums.forEach((enumerable) => {
				const enumSettings = (settings.enumerables || {})[enumerable] || {};
				prompt
					.command(enumerable)
					.description(enumSettings.description || '')
					.action(() => {
						prompt.removeAllListeners();
						prompt.ui.redraw.done();
						return resolve(forceLeadingBlankFn(forceCaseFn(enumerable)));
					});
			});
		} else {
			prompt.catch('[text...]').action((parameters) => {
				const {text = ''} = parameters;
				prompt.removeAllListeners();
				prompt.ui.redraw.done();
				return resolve(forceLeadingBlankFn(forceCaseFn(text.join(' '))));
			});
		}

		if (mayBeEmpty) {
			// Add an easy exit command
			prompt
				.command(':skip')
				.description('Skip the input if possible.')
				.action(() => {
					prompt.removeAllListeners();
					prompt.ui.redraw.done();
					resolve('');
				});
		}

		// Handle empty input
		const onSubmit = (input: string) => {
			if (input.length > 0) {
				return;
			}

			// Show help if enum is defined and input may not be empty
			if (mayNotBeEmpty) {
				prompt.ui.log(chalk.yellow(`⚠ ${chalk.bold(type)} may not be empty.`));
			}

			if (mayBeEmpty) {
				prompt.ui.log(
					chalk.blue(
						`ℹ Enter ${chalk.bold(':skip')} to omit ${chalk.bold(type)}.`
					)
				);
			}

			if (enumRule) {
				prompt.exec('help');
			}
		};

		const drawRemaining = (length: number) => {
			if (length < Infinity) {
				const colors = [
					{
						threshold: 5,
						color: chalk.red,
					},
					{
						threshold: 10,
						color: chalk.yellow,
					},
					{
						threshold: Infinity,
						color: chalk.grey,
					},
				];

				const el = colors.find((item) => item.threshold >= length);
				const color = el ? el.color : chalk.grey;
				prompt.ui.redraw(color(`${length} characters left`));
			}
		};

		const onKey = (event: {value: string}) => {
			const sanitized = forceCaseFn(event.value);
			const cropped = sanitized.slice(0, maxLength);

			// We **could** do live editing, but there are some quirks to solve
			/* const live = merge({}, results, {
				[type]: cropped
			});
			prompt.ui.redraw(`\n\n${format(live, true)}\n\n`); */

			if (maxLength) {
				drawRemaining(maxLength - cropped.length);
			}
			prompt.ui.input(cropped);
		};

		prompt.addListener('keypress', onKey);
		prompt.addListener('client_prompt_submit', onSubmit);

		prompt.log(
			`\n\nPlease enter a ${chalk.bold(type)}: ${meta({
				optional: !mayNotBeEmpty,
				required: mayNotBeEmpty,
				'tab-completion': typeof enumRule !== 'undefined',
				header: typeof settings.header !== 'undefined',
				'multi-line': settings.multiline,
			})}`
		);

		if (settings.description) {
			prompt.log(chalk.grey(`${settings.description}\n`));
		}

		prompt.log(`\n\n${format(results, true)}\n\n`);

		drawRemaining(maxLength);

		prompt.delimiter(`❯ ${type}:`).show();
	});
}