import get from 'lodash/get';
import {
	UnexpectedError,
	UserError,
	LoggerProxy,
	NodeHelpers,
	NodeOperationError,
	WorkflowOperationError,
	executeFilter,
	isFilterValue,
	safeRegex,
	type INode,
	type INodeParameters,
	type INodeProperties,
	type INodePropertyCollection,
	type INodePropertyOptions,
	type INodeType,
	type NodeParameterValueType,
} from 'n8n-workflow';

function findPropertyFromParameterName(
	parameterName: string,
	nodeType: INodeType,
	node: INode,
	nodeParameters: INodeParameters,
): INodePropertyOptions | INodeProperties | INodePropertyCollection {
	const property = NodeHelpers.findDisplayedProperty(
		parameterName,
		nodeType.description.properties,
		nodeParameters,
		node,
		nodeType.description,
	);
	if (!property) {
		throw new UserError('Could not find property', { extra: { parameterName } });
	}
	return property;
}

function executeRegexExtractValue(
	value: string,
	regex: string,
	flags: string | undefined,
	parameterName: string,
	parameterDisplayName: string,
): NodeParameterValueType | object {
	const extracted = safeRegex.exec(regex, value, flags);
	if (!extracted) {
		throw new WorkflowOperationError(
			`ERROR: ${parameterDisplayName} parameter's value is invalid. This is likely because the URL entered is incorrect`,
		);
	}
	if (extracted.length < 2 || extracted.length > 2) {
		throw new WorkflowOperationError(
			`Property "${parameterName}" has an invalid extractValue regex "${regex}". extractValue expects exactly one group to be returned.`,
		);
	}
	return extracted[1];
}

function regexPatternAndFlags(regex: string | RegExp): {
	pattern: string;
	flags: string | undefined;
} {
	if (typeof regex === 'string') return { pattern: regex, flags: undefined };
	return { pattern: regex.source, flags: regex.flags };
}

function extractValueRLC(
	value: NodeParameterValueType | object,
	property: INodeProperties,
	parameterName: string,
): NodeParameterValueType | object {
	// Not an RLC value
	if (typeof value !== 'object' || !value || !('mode' in value) || !('value' in value)) {
		return value;
	}
	const modeProp = (property.modes ?? []).find((i) => i.name === value.mode);
	if (!modeProp) {
		return value.value;
	}
	if (!('extractValue' in modeProp) || !modeProp.extractValue) {
		return value.value;
	}

	if (typeof value.value !== 'string') {
		let typeName: string | undefined = value.value?.constructor.name;
		if (value.value === null) {
			typeName = 'null';
		} else if (typeName === undefined) {
			typeName = 'undefined';
		}
		LoggerProxy.error(
			`Only strings can be passed to extractValue. Parameter "${parameterName}" passed "${typeName}"`,
		);
		throw new UserError("ERROR: This parameter's value is invalid. Please enter a valid mode.", {
			extra: { parameter: property.displayName, modeProp: modeProp.displayName },
		});
	}

	if (modeProp.extractValue.type !== 'regex') {
		throw new UnexpectedError('Property with unknown `extractValue`', {
			extra: { parameter: parameterName, extractValueType: modeProp.extractValue.type },
		});
	}

	const { pattern, flags } = regexPatternAndFlags(modeProp.extractValue.regex);
	return executeRegexExtractValue(value.value, pattern, flags, parameterName, property.displayName);
}

function extractValueFilter(
	value: NodeParameterValueType | object,
	property: INodeProperties,
	parameterName: string,
	itemIndex: number,
): NodeParameterValueType | object {
	if (!isFilterValue(value)) {
		return value;
	}

	if (property.extractValue?.type) {
		throw new UserError(
			`Property "${parameterName}" has an invalid extractValue type. Filter parameters only support extractValue: true`,
			{ extra: { parameter: parameterName } },
		);
	}

	return executeFilter(value, { itemIndex });
}

function extractValueOther(
	value: NodeParameterValueType | object,
	property: INodeProperties | INodePropertyCollection,
	parameterName: string,
): NodeParameterValueType | object {
	if (!('extractValue' in property) || !property.extractValue) {
		return value;
	}

	if (typeof value !== 'string') {
		let typeName: string | undefined = value?.constructor.name;
		if (value === null) {
			typeName = 'null';
		} else if (typeName === undefined) {
			typeName = 'undefined';
		}
		LoggerProxy.error(
			`Only strings can be passed to extractValue. Parameter "${parameterName}" passed "${typeName}"`,
		);
		throw new UserError("This parameter's value is invalid", {
			extra: { parameter: property.displayName },
		});
	}

	if (property.extractValue.type !== 'regex') {
		throw new UnexpectedError('Property with unknown `extractValue`', {
			extra: { parameter: parameterName, extractValueType: property.extractValue.type },
		});
	}

	const { pattern, flags } = regexPatternAndFlags(property.extractValue.regex);
	return executeRegexExtractValue(value, pattern, flags, parameterName, property.displayName);
}

export function extractValue(
	value: NodeParameterValueType | object,
	parameterName: string,
	node: INode,
	nodeType: INodeType,
	itemIndex = 0,
): NodeParameterValueType | object {
	let property: INodePropertyOptions | INodeProperties | INodePropertyCollection;
	try {
		property = findPropertyFromParameterName(parameterName, nodeType, node, node.parameters);

		// Definitely doesn't have value extractor
		if (!('type' in property)) {
			return value;
		}

		if (property.type === 'resourceLocator') {
			return extractValueRLC(value, property, parameterName);
		} else if (property.type === 'filter') {
			return extractValueFilter(value, property, parameterName, itemIndex);
		}
		return extractValueOther(value, property, parameterName);
	} catch (error) {
		// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment
		throw new NodeOperationError(node, error, { description: get(error, 'description') });
	}
}
