import type { Result } from '@n8n/utils/result';
import type { DateTime } from 'luxon';

import { UserError } from '../errors/base/user.error';
import type {
	FilterConditionValue,
	FilterOperatorType,
	FilterOptionsValue,
	FilterValue,
	INodeProperties,
	ValidationResult,
} from '../interfaces';
import * as LoggerProxy from '../logger-proxy';
import { parseRegexLiteral, safeRegex } from '../safe-regex';
import { validateFieldType } from '../type-validation';

type FilterConditionMetadata = {
	index: number;
	unresolvedExpressions: boolean;
	itemIndex: number;
	errorFormat: 'full' | 'inline';
};

type RegexTest = (pattern: string, input: string, flags?: string) => boolean;
type RegexTestAsync = (pattern: string, input: string, flags?: string) => Promise<boolean>;

export class FilterError extends UserError {
	constructor(
		message: string,
		override readonly description: string,
	) {
		super(message, { level: 'warning', description });
	}
}

function parseSingleFilterValue(
	value: unknown,
	type: FilterOperatorType,
	strict = false,
	version: FilterOptionsValue['version'] = 1,
): ValidationResult {
	if (type === 'any' || value === null || value === undefined) {
		return { valid: true, newValue: value } as ValidationResult;
	}

	if (type === 'boolean' && !strict) {
		if (version >= 2) {
			const result = validateFieldType('filter', value, type);
			if (result.valid) return result;
		}

		return { valid: true, newValue: Boolean(value) };
	}

	if (type === 'number') {
		if (Number.isNaN(value)) {
			return { valid: true, newValue: value };
		}
		const isEmptyString = typeof value === 'string' && value.trim() === '';
		const isEmptyArray = Array.isArray(value) && value.length === 0;
		// Number('') and Number([]) convert to 0 in validateFieldType, which is not intuitive, consider them empty values
		if ((isEmptyString || isEmptyArray) && version >= 3) {
			return { valid: true, newValue: null };
		}
	}

	return validateFieldType('filter', value, type, { strict, parseStrings: true });
}

const withIndefiniteArticle = (noun: string): string => {
	const article = 'aeiou'.includes(noun.charAt(0)) ? 'an' : 'a';
	return `${article} ${noun}`;
};

function parseFilterConditionValues(
	condition: FilterConditionValue,
	options: FilterOptionsValue,
	metadata: Partial<FilterConditionMetadata>,
): Result<{ left: unknown; right: unknown }, FilterError> {
	const index = metadata.index ?? 0;
	const itemIndex = metadata.itemIndex ?? 0;
	const errorFormat = metadata.errorFormat ?? 'full';
	const strict = options.typeValidation === 'strict';
	const version = options.version ?? 1;
	const { operator } = condition;
	const rightType = operator.rightType ?? operator.type;
	const parsedLeftValue = parseSingleFilterValue(
		condition.leftValue,
		operator.type,
		strict,
		version,
	);
	const parsedRightValue = parseSingleFilterValue(condition.rightValue, rightType, strict, version);
	const leftValid =
		parsedLeftValue.valid ||
		(metadata.unresolvedExpressions &&
			typeof condition.leftValue === 'string' &&
			condition.leftValue.startsWith('='));
	const rightValid =
		parsedRightValue.valid ||
		!!operator.singleValue ||
		(metadata.unresolvedExpressions &&
			typeof condition.rightValue === 'string' &&
			condition.rightValue.startsWith('='));
	const leftValueString = String(condition.leftValue);
	const rightValueString = String(condition.rightValue);
	const suffix =
		errorFormat === 'full' ? `[condition ${index}, item ${itemIndex}]` : `[item ${itemIndex}]`;

	const composeInvalidTypeMessage = (type: string, fromType: string, value: string) => {
		fromType = fromType.toLocaleLowerCase();
		if (strict) {
			return `Wrong type: '${value}' is ${withIndefiniteArticle(
				fromType,
			)} but was expecting ${withIndefiniteArticle(type)} ${suffix}`;
		}
		return `Conversion error: the ${fromType} '${value}' can't be converted to ${withIndefiniteArticle(
			type,
		)} ${suffix}`;
	};

	const getTypeDescription = (isStrict: boolean) => {
		if (isStrict)
			return "Try changing the type of comparison. Alternatively you can enable 'Convert types where required'.";
		return 'Try changing the type of the comparison.';
	};

	const composeInvalidTypeDescription = (
		type: string,
		fromType: string,
		valuePosition: 'first' | 'second',
	) => {
		fromType = fromType.toLocaleLowerCase();
		const expectedType = withIndefiniteArticle(type);

		let convertionFunction = '';
		if (type === 'string') {
			convertionFunction = '.toString()';
		} else if (type === 'number') {
			convertionFunction = '.toNumber()';
		} else if (type === 'boolean') {
			convertionFunction = '.toBoolean()';
		}

		if (strict && convertionFunction) {
			const suggestFunction = ` by adding <code>${convertionFunction}</code>`;
			return `
<p>Try either:</p>
<ol>
  <li>Enabling 'Convert types where required'</li>
  <li>Converting the ${valuePosition} field to ${expectedType}${suggestFunction}</li>
</ol>
			`;
		}

		return getTypeDescription(strict);
	};

	if (!leftValid && !rightValid && typeof condition.leftValue === typeof condition.rightValue) {
		return {
			ok: false,
			error: new FilterError(
				`Comparison type expects ${withIndefiniteArticle(operator.type)} but both fields are ${withIndefiniteArticle(
					typeof condition.leftValue,
				)}`,
				getTypeDescription(strict),
			),
		};
	}

	if (!leftValid) {
		return {
			ok: false,
			error: new FilterError(
				composeInvalidTypeMessage(operator.type, typeof condition.leftValue, leftValueString),
				composeInvalidTypeDescription(operator.type, typeof condition.leftValue, 'first'),
			),
		};
	}

	if (!rightValid) {
		return {
			ok: false,
			error: new FilterError(
				composeInvalidTypeMessage(rightType, typeof condition.rightValue, rightValueString),
				composeInvalidTypeDescription(rightType, typeof condition.rightValue, 'second'),
			),
		};
	}

	return {
		ok: true,
		result: {
			left: parsedLeftValue.valid ? parsedLeftValue.newValue : undefined,
			right: parsedRightValue.valid ? parsedRightValue.newValue : undefined,
		},
	};
}

export function arrayContainsValue(array: unknown[], value: unknown, ignoreCase: boolean): boolean {
	if (ignoreCase && typeof value === 'string') {
		return array.some((item) => {
			if (typeof item !== 'string') {
				return false;
			}
			return item.toString().toLocaleLowerCase() === value.toLocaleLowerCase();
		});
	}
	return array.includes(value);
}

type PreparedFilterCondition = {
	exists: boolean;
	ignoreCase: boolean;
	leftValue: unknown;
	rightValue: unknown;
};

function prepareFilterCondition(
	condition: FilterConditionValue,
	filterOptions: FilterOptionsValue,
	metadata: Partial<FilterConditionMetadata> = {},
): PreparedFilterCondition {
	const ignoreCase = !filterOptions.caseSensitive;
	const parsedValues = parseFilterConditionValues(condition, filterOptions, metadata);

	if (!parsedValues.ok) {
		throw parsedValues.error;
	}

	const { left: leftValue, right: rightValue } = parsedValues.result;
	const exists = leftValue !== undefined && leftValue !== null && !Number.isNaN(leftValue);

	return { exists, ignoreCase, leftValue, rightValue };
}

function evaluateExistsOperation(
	condition: FilterConditionValue,
	exists: boolean,
): boolean | undefined {
	if (condition.operator.operation === 'exists') {
		return exists;
	} else if (condition.operator.operation === 'notExists') {
		return !exists;
	}

	return undefined;
}

function prepareStringFilterValues(
	condition: FilterConditionValue,
	prepared: PreparedFilterCondition,
): { left: string; right: string } {
	let { leftValue, rightValue } = prepared;

	if (prepared.ignoreCase) {
		if (typeof leftValue === 'string') {
			leftValue = leftValue.toLocaleLowerCase();
		}

		if (
			typeof rightValue === 'string' &&
			!(condition.operator.operation === 'regex' || condition.operator.operation === 'notRegex')
		) {
			rightValue = rightValue.toLocaleLowerCase();
		}
	}

	return {
		left: (leftValue ?? '') as string,
		right: (rightValue ?? '') as string,
	};
}

// eslint-disable-next-line complexity
function executeFilterConditionWithRegexTest(
	condition: FilterConditionValue,
	filterOptions: FilterOptionsValue,
	metadata: Partial<FilterConditionMetadata>,
	regexTest: RegexTest,
): boolean {
	const { operator } = condition;
	const prepared = prepareFilterCondition(condition, filterOptions, metadata);
	const { leftValue, rightValue, exists, ignoreCase } = prepared;

	const existsResult = evaluateExistsOperation(condition, exists);
	if (existsResult !== undefined) return existsResult;

	switch (operator.type) {
		case 'string': {
			const { left, right } = prepareStringFilterValues(condition, prepared);

			switch (condition.operator.operation) {
				case 'empty':
					return left.length === 0;
				case 'notEmpty':
					return left.length !== 0;
				case 'equals':
					return left === right;
				case 'notEquals':
					return left !== right;
				case 'contains':
					return left.includes(right);
				case 'notContains':
					return !left.includes(right);
				case 'startsWith':
					return left.startsWith(right);
				case 'notStartsWith':
					return !left.startsWith(right);
				case 'endsWith':
					return left.endsWith(right);
				case 'notEndsWith':
					return !left.endsWith(right);
				case 'regex': {
					const { source, flags } = parseRegexLiteral(right);
					return regexTest(source, left, flags);
				}
				case 'notRegex': {
					const { source, flags } = parseRegexLiteral(right);
					return !regexTest(source, left, flags);
				}
			}

			break;
		}
		case 'number': {
			const left = leftValue as number;
			const right = rightValue as number;

			switch (condition.operator.operation) {
				case 'empty':
					return !exists;
				case 'notEmpty':
					return exists;
				case 'equals':
					return left === right;
				case 'notEquals':
					return left !== right;
				case 'gt':
					return left > right;
				case 'lt':
					return left < right;
				case 'gte':
					return left >= right;
				case 'lte':
					return left <= right;
			}

			break;
		}
		case 'dateTime': {
			const left = leftValue as DateTime;
			const right = rightValue as DateTime;

			if (condition.operator.operation === 'empty') {
				return !exists;
			} else if (condition.operator.operation === 'notEmpty') {
				return exists;
			}

			if (!left || !right) {
				return false;
			}

			switch (condition.operator.operation) {
				case 'equals':
					return left.toMillis() === right.toMillis();
				case 'notEquals':
					return left.toMillis() !== right.toMillis();
				case 'after':
					return left.toMillis() > right.toMillis();
				case 'before':
					return left.toMillis() < right.toMillis();
				case 'afterOrEquals':
					return left.toMillis() >= right.toMillis();
				case 'beforeOrEquals':
					return left.toMillis() <= right.toMillis();
			}

			break;
		}
		case 'boolean': {
			const left = leftValue as boolean;
			const right = rightValue as boolean;

			switch (condition.operator.operation) {
				case 'empty':
					return !exists;
				case 'notEmpty':
					return exists;
				case 'true':
					return left;
				case 'false':
					return !left;
				case 'equals':
					return left === right;
				case 'notEquals':
					return left !== right;
			}

			break;
		}
		case 'array': {
			const left = (leftValue ?? []) as unknown[];
			const rightNumber = rightValue as number;

			switch (condition.operator.operation) {
				case 'contains':
					return arrayContainsValue(left, rightValue, ignoreCase);
				case 'notContains':
					return !arrayContainsValue(left, rightValue, ignoreCase);
				case 'lengthEquals':
					return left.length === rightNumber;
				case 'lengthNotEquals':
					return left.length !== rightNumber;
				case 'lengthGt':
					return left.length > rightNumber;
				case 'lengthLt':
					return left.length < rightNumber;
				case 'lengthGte':
					return left.length >= rightNumber;
				case 'lengthLte':
					return left.length <= rightNumber;
				case 'empty':
					return left.length === 0;
				case 'notEmpty':
					return left.length !== 0;
			}

			break;
		}
		case 'object': {
			const left = leftValue;

			switch (condition.operator.operation) {
				case 'empty':
					return !left || Object.keys(left).length === 0;
				case 'notEmpty':
					return !!left && Object.keys(left).length !== 0;
			}

			break;
		}
	}

	LoggerProxy.warn(`Unknown filter parameter operator "${operator.type}:${operator.operation}"`);

	return false;
}

export function executeFilterCondition(
	condition: FilterConditionValue,
	filterOptions: FilterOptionsValue,
	metadata: Partial<FilterConditionMetadata> = {},
): boolean {
	return executeFilterConditionWithRegexTest(
		condition,
		filterOptions,
		metadata,
		(pattern, input, flags) => safeRegex.test(pattern, input, flags),
	);
}

/**
 * Identical to executeFilterCondition, but accepts custom regex test function.
 * This method should be used on frontend where safe regex test is async only, while on backend executeFilterCondition can be used with default sync regex test.
 */
export async function executeFilterConditionAsync(
	condition: FilterConditionValue,
	filterOptions: FilterOptionsValue,
	regexTest: RegexTestAsync,
	metadata: Partial<FilterConditionMetadata> = {},
): Promise<boolean> {
	const { operator } = condition;
	const prepared = prepareFilterCondition(condition, filterOptions, metadata);

	const existsResult = evaluateExistsOperation(condition, prepared.exists);
	if (existsResult !== undefined) return existsResult;

	// This function is provided as a placeholder, where regex operations are unreachable and handled by outer code.
	const disabledRegexTest = () => {
		throw new Error('Not implemented');
	};

	if (operator.type !== 'string') {
		return executeFilterConditionWithRegexTest(
			condition,
			filterOptions,
			metadata,
			// string operations are intercepted, so a placeholder regex test function is provided
			disabledRegexTest,
		);
	}

	const { left, right } = prepareStringFilterValues(condition, prepared);

	switch (condition.operator.operation) {
		case 'regex': {
			const { source, flags } = parseRegexLiteral(right);
			return await regexTest(source, left, flags);
		}
		case 'notRegex': {
			const { source, flags } = parseRegexLiteral(right);
			return !(await regexTest(source, left, flags));
		}
		default:
			return executeFilterConditionWithRegexTest(
				condition,
				filterOptions,
				metadata,
				// regex operations are intercepted, so a placeholder regex test function is provided
				disabledRegexTest,
			);
	}
}

type ExecuteFilterOptions = {
	itemIndex?: number;
};
export function executeFilter(
	value: FilterValue,
	{ itemIndex }: ExecuteFilterOptions = {},
): boolean {
	const conditionPass = (condition: FilterConditionValue, index: number) =>
		executeFilterCondition(condition, value.options, { index, itemIndex });

	if (value.combinator === 'and') {
		return value.conditions.every(conditionPass);
	} else if (value.combinator === 'or') {
		return value.conditions.some(conditionPass);
	}

	LoggerProxy.warn(`Unknown filter combinator "${value.combinator as string}"`);

	return false;
}

export const validateFilterParameter = (
	nodeProperties: INodeProperties,
	value: FilterValue,
): Record<string, string[]> => {
	return value.conditions.reduce(
		(issues, condition, index) => {
			const key = `${nodeProperties.name}.${index}`;

			try {
				parseFilterConditionValues(condition, value.options, {
					index,
					unresolvedExpressions: true,
					errorFormat: 'inline',
				});
			} catch (error) {
				if (error instanceof FilterError) {
					issues[key].push(error.message);
				}
			}

			return issues;
		},
		{} as Record<string, string[]>,
	);
};
