import resolvedNestedSelector from 'postcss-resolve-nested-selector';
import selectorParser from 'postcss-selector-parser';

import findAtRuleContext from '../../utils/findAtRuleContext.mjs';
import { isBoolean } from '../../utils/validateTypes.mjs';
import isKeyframeRule from '../../utils/isKeyframeRule.mjs';
import isStandardSyntaxSelector from '../../utils/isStandardSyntaxSelector.mjs';
import nodeContextLookup from '../../utils/nodeContextLookup.mjs';
import parseSelector from '../../utils/parseSelector.mjs';
import report from '../../utils/report.mjs';
import ruleMessages from '../../utils/ruleMessages.mjs';
import validateOptions from '../../utils/validateOptions.mjs';

const ruleName = 'no-duplicate-selectors';

const messages = ruleMessages(ruleName, {
	rejected: (selector, firstDuplicateLine) =>
		`Unexpected duplicate selector "${selector}", first used at line ${firstDuplicateLine}`,
});

const meta = {
	url: 'https://stylelint.io/user-guide/rules/no-duplicate-selectors',
};

/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
	return (root, result) => {
		const validOptions = validateOptions(
			result,
			ruleName,
			{ actual: primary },
			{
				actual: secondaryOptions,
				possible: {
					disallowInList: [isBoolean],
				},
				optional: true,
			},
		);

		if (!validOptions) {
			return;
		}

		const shouldDisallowDuplicateInList = secondaryOptions && secondaryOptions.disallowInList;

		// The top level of this map will be rule sources.
		// Each source maps to another map, which maps rule parents to a set of selectors.
		// This ensures that selectors are only checked against selectors
		// from other rules that share the same parent and the same source.
		const selectorContextLookup = nodeContextLookup();

		root.walkRules((ruleNode) => {
			if (isKeyframeRule(ruleNode)) {
				return;
			}

			const contextSelectorSet = selectorContextLookup.getContext(
				ruleNode,
				findAtRuleContext(ruleNode),
			);
			const resolvedSelectorList = [
				...new Set(
					ruleNode.selectors.flatMap((selector) => resolvedNestedSelector(selector, ruleNode)),
				),
			];

			const normalizedSelectorList = resolvedSelectorList.map(normalize);

			// Sort the selectors list so that the order of the constituents
			// doesn't matter
			const sortedSelectorList = [...normalizedSelectorList].sort().join(',');

			if (!ruleNode.source) throw new Error('The rule node must have a source');

			if (!ruleNode.source.start) throw new Error('The rule source must have a start position');

			const selectorLine = ruleNode.source.start.line;

			// Complain if the same selector list occurs twice

			let previousDuplicatePosition;
			// When `disallowInList` is true, we must parse `sortedSelectorList` into
			// list items.
			/** @type {string[]} */
			const selectorListParsed = [];

			if (shouldDisallowDuplicateInList) {
				parseSelector(sortedSelectorList, result, ruleNode)?.each((s) => {
					const selector = String(s);

					selectorListParsed.push(selector);

					if (contextSelectorSet.get(selector)) {
						previousDuplicatePosition = contextSelectorSet.get(selector);
					}
				});
			} else {
				previousDuplicatePosition = contextSelectorSet.get(sortedSelectorList);
			}

			if (previousDuplicatePosition) {
				// If the selector isn't nested we can use its raw value; otherwise,
				// we have to approximate something for the message -- which is close enough
				const isNestedSelector = resolvedSelectorList.join(',') !== ruleNode.selectors.join(',');
				const selectorForMessage = isNestedSelector
					? resolvedSelectorList.join(', ')
					: ruleNode.selector;

				return report({
					result,
					ruleName,
					node: ruleNode,
					message: messages.rejected,
					messageArgs: [selectorForMessage, previousDuplicatePosition],
					word: selectorForMessage,
				});
			}

			const presentedSelectors = new Set();
			const reportedSelectors = new Set();

			// Or complain if one selector list contains the same selector more than once
			for (const selector of ruleNode.selectors) {
				const normalized = normalize(selector);

				if (presentedSelectors.has(normalized)) {
					if (reportedSelectors.has(normalized)) {
						continue;
					}

					report({
						result,
						ruleName,
						node: ruleNode,
						message: messages.rejected,
						messageArgs: [selector, selectorLine],
						word: selector,
					});
					reportedSelectors.add(normalized);
				} else {
					presentedSelectors.add(normalized);
				}
			}

			if (shouldDisallowDuplicateInList) {
				for (const selector of selectorListParsed) {
					// [selectorLine] will not really be accurate for multi-line
					// selectors, such as "bar" in "foo,\nbar {}".
					contextSelectorSet.set(selector, selectorLine);
				}
			} else {
				contextSelectorSet.set(sortedSelectorList, selectorLine);
			}
		});
	};
};

/**
 * @param {string} selector
 * @returns {string}
 */
function normalize(selector) {
	if (!isStandardSyntaxSelector(selector)) {
		return selector;
	}

	return selectorParser().processSync(selector, { lossless: false });
}

rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
export default rule;
