import { EOL } from 'node:os';

import { DEFAULT_SEVERITY, RULE_NAME_ALL } from './constants.mjs';
import { DEFAULT_CONFIGURATION_COMMENT } from './utils/configurationComment.mjs';
import assignDisabledRanges from './assignDisabledRanges.mjs';
import { emitDeprecationWarning } from './utils/emitWarning.mjs';
import { fork } from 'css-tree';
import getStylelintRule from './utils/getStylelintRule.mjs';
import reportUnknownRuleNames from './reportUnknownRuleNames.mjs';
import rules from './rules/index.mjs';
import timing from './timing.mjs';

/** @import {Config, LinterOptions, PostcssResult} from 'stylelint' */

/**
 * @param {LinterOptions} stylelintOptions
 * @param {PostcssResult} postcssResult
 * @param {Config} config
 * @returns {Promise<any>}
 */
export default async function lintPostcssResult(stylelintOptions, postcssResult, config) {
	postcssResult.stylelint.stylelintError = false;
	postcssResult.stylelint.stylelintWarning = false;
	postcssResult.stylelint.quiet = config.quiet;
	postcssResult.stylelint.quietDeprecationWarnings = stylelintOptions.quietDeprecationWarnings;
	postcssResult.stylelint.config = config;

	const postcssDoc = postcssResult.root;

	if (!('type' in postcssDoc)) {
		throw new Error('Unexpected Postcss root object!');
	}

	const newlineMatch = postcssDoc.source?.input.css.match(/\r?\n/);
	const newline = newlineMatch ? newlineMatch[0] : EOL;
	const configurationComment = config.configurationComment || DEFAULT_CONFIGURATION_COMMENT;
	const ctx = { configurationComment, newline };

	assignDisabledRanges(postcssDoc, postcssResult);

	const postcssRoots = /** @type {import('postcss').Root[]} */ (
		postcssDoc && postcssDoc.constructor.name === 'Document' ? postcssDoc.nodes : [postcssDoc]
	);

	// Promises for the rules. Although the rule code runs synchronously now,
	// the use of Promises makes it compatible with the possibility of async
	// rules down the line.
	/** @type {Array<Promise<any>>} */
	const performRules = [];

	const rulesOrder = Object.keys(rules);
	const ruleNames = config.rules
		? Object.keys(config.rules).sort((a, b) => rulesOrder.indexOf(a) - rulesOrder.indexOf(b))
		: [];

	for (const ruleName of ruleNames) {
		const ruleFunction = await getStylelintRule(ruleName, config);

		if (ruleFunction === undefined) {
			performRules.push(
				Promise.all(
					postcssRoots.map((postcssRoot) =>
						reportUnknownRuleNames(ruleName, postcssRoot, postcssResult),
					),
				),
			);

			continue;
		}

		const ruleSettings = config.rules?.[ruleName];

		if (ruleSettings === null || ruleSettings[0] === null) continue;

		if (ruleFunction.meta?.deprecated && !stylelintOptions.quietDeprecationWarnings) {
			warnDeprecatedRule(postcssResult, ruleName);
		}

		const primaryOption = ruleSettings[0];
		const secondaryOptions = ruleSettings[1];

		// Log the rule's severity in the PostCSS result
		const defaultSeverity = config.defaultSeverity || DEFAULT_SEVERITY;

		postcssResult.stylelint.ruleSeverities[ruleName] =
			(secondaryOptions && secondaryOptions.severity) || defaultSeverity;
		postcssResult.stylelint.customMessages[ruleName] = secondaryOptions && secondaryOptions.message;
		postcssResult.stylelint.customUrls[ruleName] = secondaryOptions && secondaryOptions.url;
		postcssResult.stylelint.ruleMetadata[ruleName] = ruleFunction.meta || {};

		const shouldWarn = ruleFunction.meta?.fixable && !stylelintOptions.quietDeprecationWarnings;
		const disableFix = secondaryOptions?.disableFix === true;
		const fix = !disableFix && config.fix && isFixCompatible(postcssResult, ruleName);
		const lexer = getCachedLexer(config);
		const context = {
			...ctx,
			lexer,
			// context.fix is unlikely to be removed in the foreseeable future
			// due to the sheer number of rules in the wild that rely on it
			get fix() {
				if (shouldWarn) {
					emitDeprecationWarning(
						'`context.fix` is being deprecated.',
						'CONTEXT_FIX',
						`Please pass a \`fix\` callback to the \`report\` utility of "${ruleName}" instead.`,
					);
				}

				return fix;
			},
		};

		const ruleFn = ruleFunction(primaryOption, secondaryOptions, context);

		/**
		 * @param {import('postcss').Root} postcssRoot
		 */
		async function runRule(postcssRoot) {
			if (timing.enabled) {
				return timing.time(ruleName, () => ruleFn(postcssRoot, postcssResult))();
			}

			return ruleFn(postcssRoot, postcssResult);
		}

		performRules.push(Promise.all(postcssRoots.map(runRule)));
	}

	return Promise.all(performRules);
}

/**
 * using context.fix instead of the fix callback has the drawback
 * of not honouring the configuration comments in subtle ways
 * @see file://./../docs/user-guide/options.md#fix for details
 * @param {PostcssResult} postcssResult
 * @param {string} name
 * @returns {boolean}
 */
function isFixCompatible({ stylelint: { disabledRanges } }, name) {
	return !disabledRanges[RULE_NAME_ALL]?.length && !disabledRanges[name];
}

/**
 * @param {PostcssResult} result
 * @param {string} ruleName
 * @returns {void}
 */
function warnDeprecatedRule(result, ruleName) {
	const message = `The "${ruleName}" rule is deprecated.`;

	emitDeprecationWarning(
		message,
		'RULE',
		`Please be aware that the "${ruleName}" rule will soon be either removed or renamed.`,
	);

	result.warn(message, { stylelintType: 'deprecation' });
}

const lexerCache = new Map();

/**
 * @param {Config} config
 * @returns {import('css-tree').Lexer}
 * */
function getCachedLexer(config) {
	const cacheKey = JSON.stringify(config.languageOptions?.syntax || {});

	if (lexerCache.has(cacheKey)) {
		return lexerCache.get(cacheKey);
	}

	const newLexer = fork({
		atrules: config.languageOptions?.syntax?.atRules || {},
		properties: config.languageOptions?.syntax?.properties || {},
		types: config.languageOptions?.syntax?.types || {},
		cssWideKeywords: config.languageOptions?.syntax?.cssWideKeywords || [],
	}).lexer;

	lexerCache.set(cacheKey, newLexer);

	return newLexer;
}
