import util from 'util';
import isIgnored from '@commitlint/is-ignored';
import parse from '@commitlint/parse';
import defaultRules from '@commitlint/rules';
import {buildCommitMesage} from './commit-message';
import {
	LintOptions,
	LintOutcome,
	LintRuleOutcome,
	Rule,
	RuleConfigSeverity,
	BaseRule,
	RuleType,
	QualifiedRules,
} from '@commitlint/types';

export default async function lint(
	message: string,
	rawRulesConfig?: QualifiedRules,
	rawOpts?: LintOptions
): Promise<LintOutcome> {
	const opts = rawOpts
		? rawOpts
		: {defaultIgnores: undefined, ignores: undefined};
	const rulesConfig = rawRulesConfig || {};

	// Found a wildcard match, skip
	if (
		isIgnored(message, {defaults: opts.defaultIgnores, ignores: opts.ignores})
	) {
		return {
			valid: true,
			errors: [],
			warnings: [],
			input: message,
		};
	}

	// Parse the commit message
	const parsed =
		message === ''
			? {header: null, body: null, footer: null}
			: await parse(message, undefined, opts.parserOpts);

	if (
		parsed.header === null &&
		parsed.body === null &&
		parsed.footer === null
	) {
		// Commit is empty, skip
		return {
			valid: true,
			errors: [],
			warnings: [],
			input: message,
		};
	}

	const allRules: Map<string, BaseRule<never, RuleType>> = new Map(
		Object.entries(defaultRules)
	);

	if (opts.plugins) {
		Object.values(opts.plugins).forEach((plugin) => {
			if (plugin.rules) {
				Object.keys(plugin.rules).forEach((ruleKey) =>
					allRules.set(ruleKey, plugin.rules[ruleKey])
				);
			}
		});
	}

	// Find invalid rules configs
	const missing = Object.keys(rulesConfig).filter(
		(name) => typeof allRules.get(name) !== 'function'
	);

	if (missing.length > 0) {
		const names = [...allRules.keys()];
		throw new RangeError(
			`Found invalid rule names: ${missing.join(
				', '
			)}. Supported rule names are: ${names.join(', ')}`
		);
	}

	const invalid = Object.entries(rulesConfig)
		.map(([name, config]) => {
			if (!Array.isArray(config)) {
				return new Error(
					`config for rule ${name} must be array, received ${util.inspect(
						config
					)} of type ${typeof config}`
				);
			}

			const [level] = config;

			if (level === RuleConfigSeverity.Disabled && config.length === 1) {
				return null;
			}

			const [, when] = config;

			if (typeof level !== 'number' || isNaN(level)) {
				return new Error(
					`level for rule ${name} must be number, received ${util.inspect(
						level
					)} of type ${typeof level}`
				);
			}

			if (config.length !== 2 && config.length !== 3) {
				return new Error(
					`config for rule ${name} must be 2 or 3 items long, received ${util.inspect(
						config
					)} of length ${config.length}`
				);
			}

			if (level < 0 || level > 2) {
				return new RangeError(
					`level for rule ${name} must be between 0 and 2, received ${util.inspect(
						level
					)}`
				);
			}

			if (typeof when !== 'string') {
				return new Error(
					`condition for rule ${name} must be string, received ${util.inspect(
						when
					)} of type ${typeof when}`
				);
			}

			if (when !== 'never' && when !== 'always') {
				return new Error(
					`condition for rule ${name} must be "always" or "never", received ${util.inspect(
						when
					)}`
				);
			}

			return null;
		})
		.filter((item): item is Error => item instanceof Error);

	if (invalid.length > 0) {
		throw new Error(invalid.map((i) => i.message).join('\n'));
	}

	// Validate against all rules
	const pendingResults = Object.entries(rulesConfig)
		// Level 0 rules are ignored
		.filter(([, config]) => !!config && config.length && config[0] > 0)
		.map(async (entry) => {
			const [name, config] = entry;
			const [level, when, value] = config!; //

			const rule = allRules.get(name);

			if (!rule) {
				throw new Error(`Could not find rule implementation for ${name}`);
			}

			const executableRule = rule as Rule<unknown>;
			const [valid, message] = await executableRule(parsed, when, value);

			return {
				level,
				valid,
				name,
				message,
			};
		});

	const results = (await Promise.all(pendingResults)).filter(
		(result): result is LintRuleOutcome => result !== null
	);

	const errors = results.filter(
		(result) => result.level === 2 && !result.valid
	);
	const warnings = results.filter(
		(result) => result.level === 1 && !result.valid
	);

	const valid = errors.length === 0;

	return {
		valid,
		errors,
		warnings,
		input: buildCommitMesage(parsed),
	};
}