import {isRegExp} from 'node:util/types';
import {findVariable} from '@eslint-community/eslint-utils';
import {getAvailableVariableName, upperFirst} from './utils/index.js';
import {renameVariable} from './fix/index.js';
import {isMethodCall} from './ast/index.js';

const MESSAGE_ID = 'catch-error-name';
const messages = {
	[MESSAGE_ID]: 'The catch parameter `{{originalName}}` should be named `{{fixedName}}`.',
};

// - `promise.then(…, foo => {})`
// - `promise.then(…, function(foo) {})`
// - `promise.catch(foo => {})`
// - `promise.catch(function(foo) {})`
const isPromiseCatchParameter = node =>
	(node.parent.type === 'FunctionExpression' || node.parent.type === 'ArrowFunctionExpression')
	&& node.parent.params[0] === node
	&& (
		isMethodCall(node.parent.parent, {
			method: 'then',
			argumentsLength: 2,
			optionalCall: false,
		})
		|| isMethodCall(node.parent.parent, {
			method: 'catch',
			argumentsLength: 1,
			optionalCall: false,
		})
	)
	&& node.parent.parent.arguments.at(-1) === node.parent;

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
	const options = context.options[0];
	const {name: expectedName} = options;
	const ignore = options.ignore.map(
		pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'),
	);
	const isNameAllowed = name =>
		name === expectedName
		|| ignore.some(regexp => regexp.test(name))
		|| name.endsWith(expectedName)
		|| name.endsWith(upperFirst(expectedName));

	context.on('Identifier', node => {
		if (
			!(node.parent.type === 'CatchClause' && node.parent.param === node)
			&& !isPromiseCatchParameter(node)
		) {
			return;
		}

		const originalName = node.name;

		if (
			isNameAllowed(originalName)
			|| isNameAllowed(originalName.replaceAll(/_+$/g, ''))
		) {
			return;
		}

		const scope = context.sourceCode.getScope(node);
		const variable = findVariable(scope, node);

		// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768072967
		// But can't reproduce, just ignore this case
		/* c8 ignore next 3 */
		if (!variable) {
			return;
		}

		if (originalName === '_' && variable.references.length === 0) {
			return;
		}

		const scopes = [
			variable.scope,
			...variable.references.map(({from}) => from),
		];
		const fixedName = getAvailableVariableName(expectedName, scopes);

		const problem = {
			node,
			messageId: MESSAGE_ID,
			data: {
				originalName,
				fixedName: fixedName || expectedName,
			},
		};

		if (fixedName) {
			problem.fix = fixer => renameVariable(variable, fixedName, context, fixer);
		}

		return problem;
	});
};

const schema = [
	{
		type: 'object',
		additionalProperties: false,
		properties: {
			name: {
				type: 'string',
			},
			ignore: {
				type: 'array',
				uniqueItems: true,
			},
		},
	},
];

/** @type {import('eslint').Rule.RuleModule} */
const config = {
	create,
	meta: {
		type: 'suggestion',
		docs: {
			description: 'Enforce a specific parameter name in catch clauses.',
			recommended: true,
		},
		fixable: 'code',
		schema,
		defaultOptions: [{name: 'error', ignore: []}],
		messages,
	},
};

export default config;
