import path from 'node:path';
import coreJsCompat from 'core-js-compat';
import {camelCase} from 'change-case';
import isStaticRequire from './ast/is-static-require.js';
import {readPackageJson} from './shared/package-json.js';

const {data: compatData, entries: coreJsEntries} = coreJsCompat;

const MESSAGE_ID_POLYFILL = 'unnecessaryPolyfill';
const MESSAGE_ID_CORE_JS = 'unnecessaryCoreJsModule';
const messages = {
	[MESSAGE_ID_POLYFILL]: 'Use built-in instead.',
	[MESSAGE_ID_CORE_JS]:
		'All polyfilled features imported from `{{coreJsModule}}` are available as built-ins. Use the built-ins instead.',
};

const additionalPolyfillModules = {
	'es.promise.finally': ['p-finally'],
	'es.object.set-prototype-of': ['setprototypeof'],
	'es.string.code-point-at': ['code-point-at'],
};
const additionalPolyfillPatterns = Object.fromEntries(Object.entries(additionalPolyfillModules).map(([feature, modules]) => [feature, `|(${modules.join('|')})`]));

const prefixes = '(mdn-polyfills/|polyfill-)';
const suffixes = '(-polyfill)';
const delimiter = String.raw`(\.|-|\.prototype\.|/)?`;
const moduleDelimiter = /[./-]/u;

const getFirstSegment = value => {
	const [firstSegment = ''] = value.split(moduleDelimiter);
	return firstSegment;
};

const stripPolyfillPrefix = value => {
	if (value.startsWith('polyfill-')) {
		return value.slice('polyfill-'.length);
	}

	if (value.startsWith('mdn-polyfills/')) {
		return value.slice('mdn-polyfills/'.length);
	}

	return value;
};

function addPolyfillToken(tokens, value) {
	if (!value) {
		return;
	}

	const lowercaseValue = value.toLowerCase();
	tokens.add(lowercaseValue);
	tokens.add(getFirstSegment(lowercaseValue));

	const camelCasedValue = camelCase(value).toLowerCase();
	tokens.add(camelCasedValue);
	tokens.add(getFirstSegment(camelCasedValue));
}

const polyfills = Object.keys(compatData).map(feature => {
	const [rawEcmaVersion, rawConstructorName, rawMethodName = ''] = feature.split('.');
	let ecmaVersion = rawEcmaVersion;
	let constructorName = rawConstructorName;
	let methodName = rawMethodName;

	if (ecmaVersion === 'es') {
		ecmaVersion = String.raw`(es\d*)`;
	}

	constructorName = `(${constructorName}|${camelCase(constructorName)})`;
	methodName &&= `(${methodName}|${camelCase(methodName)})`;

	const methodOrConstructor = methodName || constructorName;

	const patterns = [
		`^((${prefixes}?(`,
		methodName && `(${ecmaVersion}${delimiter}${constructorName}${delimiter}${methodName})|`, // Ex: es6-array-copy-within
		methodName && `(${constructorName}${delimiter}${methodName})|`, // Ex: array-copy-within
		`(${ecmaVersion}${delimiter}${constructorName}))`, // Ex: es6-array
		`${suffixes}?)|`,
		`(${prefixes}${methodOrConstructor}|${methodOrConstructor}${suffixes})`, // Ex: polyfill-copy-within / polyfill-promise
		`${additionalPolyfillPatterns[feature] || ''})$`,
	];

	return {
		feature,
		pattern: new RegExp(patterns.join(''), 'i'),
		tokens: (() => {
			const tokens = new Set();

			if (rawEcmaVersion === 'es') {
				tokens.add('es');
			} else {
				addPolyfillToken(tokens, rawEcmaVersion);
			}

			addPolyfillToken(tokens, rawConstructorName);
			addPolyfillToken(tokens, rawMethodName);

			for (const module of additionalPolyfillModules[feature] || []) {
				addPolyfillToken(tokens, module);
			}

			return tokens;
		})(),
	};
});
const polyfillsByToken = new Map();
const polyfillTokensByFirstCharacter = new Map();
const esConstructorTokens = new Set();

for (const polyfill of polyfills) {
	const [ecmaVersion, constructorName] = polyfill.feature.split('.');
	if (ecmaVersion === 'es') {
		esConstructorTokens.add(constructorName.toLowerCase());
		esConstructorTokens.add(camelCase(constructorName).toLowerCase());
	}

	for (const token of polyfill.tokens) {
		if (!token) {
			continue;
		}

		if (polyfillsByToken.has(token)) {
			polyfillsByToken.get(token).push(polyfill);
		} else {
			polyfillsByToken.set(token, [polyfill]);
		}

		const firstCharacter = token[0];
		if (polyfillTokensByFirstCharacter.has(firstCharacter)) {
			polyfillTokensByFirstCharacter.get(firstCharacter).add(token);
		} else {
			polyfillTokensByFirstCharacter.set(firstCharacter, new Set([token]));
		}
	}
}

const hasEsConstructorPrefix = value => {
	for (const token of esConstructorTokens) {
		if (value.startsWith(token)) {
			return true;
		}
	}

	return false;
};

const isPotentialEsPrefix = importedModule => {
	if (!importedModule.startsWith('es')) {
		return false;
	}

	let constructorIndex = 2;
	while (
		constructorIndex < importedModule.length
		&& importedModule[constructorIndex] >= '0'
		&& importedModule[constructorIndex] <= '9'
	) {
		constructorIndex++;
	}

	if (importedModule.startsWith('.prototype.', constructorIndex)) {
		constructorIndex += '.prototype.'.length;
	} else if (['.', '-', '/'].includes(importedModule[constructorIndex])) {
		constructorIndex++;
	}

	return hasEsConstructorPrefix(importedModule.slice(constructorIndex));
};

const getPolyfillCandidates = importedModule => {
	const normalizedImportedModule = stripPolyfillPrefix(importedModule);
	if (!normalizedImportedModule) {
		return;
	}

	const firstCharacter = normalizedImportedModule[0];
	const tokens = polyfillTokensByFirstCharacter.get(firstCharacter);
	if (!tokens) {
		return;
	}

	const candidates = new Set();
	const firstSegment = getFirstSegment(normalizedImportedModule);
	if (firstSegment === normalizedImportedModule) {
		for (const token of tokens) {
			if (token === 'es') {
				if (!isPotentialEsPrefix(normalizedImportedModule)) {
					continue;
				}
			} else if (!normalizedImportedModule.startsWith(token)) {
				continue;
			}

			for (const polyfill of polyfillsByToken.get(token)) {
				candidates.add(polyfill);
			}
		}
	} else {
		for (const token of tokens) {
			if (
				token === 'es'
				|| !firstSegment.startsWith(token)
			) {
				continue;
			}

			for (const polyfill of polyfillsByToken.get(token)) {
				candidates.add(polyfill);
			}
		}
	}

	if (isPotentialEsPrefix(normalizedImportedModule)) {
		for (const polyfill of polyfillsByToken.get('es') || []) {
			candidates.add(polyfill);
		}
	}

	if (candidates.size === 0) {
		return;
	}

	return [...candidates];
};

function getTargets(options, dirname) {
	if (options?.targets) {
		return options.targets;
	}

	const packageJsonResult = readPackageJson(dirname);

	if (!packageJsonResult) {
		return;
	}

	const {browserslist, engines} = packageJsonResult.packageJson;
	return browserslist ?? engines;
}

function create(context) {
	const targets = getTargets(context.options[0], path.dirname(context.filename));
	if (!targets) {
		return;
	}

	let unavailableFeatures;
	try {
		unavailableFeatures = coreJsCompat({targets}).list;
	} catch {
		// This can happen if the targets are invalid or use unsupported syntax like `{node:'*'}`.
		return;
	}

	const unavailableFeatureSet = new Set(unavailableFeatures);

	// When core-js graduates a feature from `esnext` to `es`, the entries list both (e.g. `['es.regexp.escape', 'esnext.regexp.escape']`),
	// but `coreJsCompat` only includes the `es` version in its unavailable list, making the `esnext` version appear "available".
	// To avoid false positives, treat `esnext.*` features as unavailable when their `es.*` counterpart is already in the list.
	const checkFeatures = features => !features.every(feature =>
		unavailableFeatureSet.has(feature)
		|| (feature.startsWith('esnext.') && features.includes(feature.replace('esnext.', 'es.'))));

	context.on('Literal', node => {
		if (
			!(
				(['ImportDeclaration', 'ImportExpression'].includes(node.parent.type) && node.parent.source === node)
				|| (isStaticRequire(node.parent) && node.parent.arguments[0] === node)
			)
		) {
			return;
		}

		const importedModule = node.value;
		if (typeof importedModule !== 'string' || ['.', '/'].includes(importedModule[0])) {
			return;
		}

		const coreJsModuleFeatures = coreJsEntries[importedModule.replace('core-js-pure', 'core-js')];

		if (coreJsModuleFeatures) {
			if (coreJsModuleFeatures.length > 1) {
				if (checkFeatures(coreJsModuleFeatures)) {
					return {
						node,
						messageId: MESSAGE_ID_CORE_JS,
						data: {
							coreJsModule: importedModule,
						},
					};
				}
			} else if (!unavailableFeatureSet.has(coreJsModuleFeatures[0])) {
				return {node, messageId: MESSAGE_ID_POLYFILL};
			}

			return;
		}

		const polyfillCandidates = getPolyfillCandidates(importedModule.toLowerCase());
		if (!polyfillCandidates) {
			return;
		}

		const polyfill = polyfillCandidates.find(({pattern}) => pattern.test(importedModule));
		if (polyfill) {
			const [, namespace, method = ''] = polyfill.feature.split('.');
			const features = coreJsEntries[`core-js/full/${namespace}${method && '/'}${method}`];

			if (features && checkFeatures(features)) {
				return {node, messageId: MESSAGE_ID_POLYFILL};
			}
		}
	});
}

const schema = [
	{
		type: 'object',
		additionalProperties: false,
		required: ['targets'],
		properties: {
			targets: {
				oneOf: [
					{
						type: 'string',
						description: 'A browserslist query string.',
					},
					{
						type: 'array',
						description: 'An array of browserslist query strings.',
					},
					{
						type: 'object',
						description: 'A browserslist targets object.',
					},
				],
				description: 'The target environments.',
			},
		},
	},
];

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Enforce the use of built-in methods instead of unnecessary polyfills.',
			recommended: 'unopinionated',
		},
		schema,
		// eslint-disable-next-line eslint-plugin/require-meta-default-options
		defaultOptions: [],
		messages,
	},
};

export default config;
