import uFuzzy from '@leeoniya/ufuzzy';
import type {
	PrepareImportFileResponse,
	GetImportFileResponse,
} from '../../typings/types';
import { getAdditionalConditionalLogicFieldOptions } from '../../utilities';

/**
 * Given a string and a list of fields to search, return the field whose
 * label most clesely matches the search string.
 */
function fuzzyMatchFieldByLabel(searchString: string, fields: GFField[]) {
	const fieldLabels = fields.map((field) => field.label);

	const opts = {};
	const uf = new uFuzzy(opts);
	const [haystackIdxs] = uf.search(fieldLabels, searchString);

	if (haystackIdxs && fields[haystackIdxs[0]]) {
		return fields[haystackIdxs[0]];
	}

	return null;
}

/**
 * Given a string and a list of fields to search, return the field whose
 * label most clesely matches the search string.
 */
function fuzzyMatchFieldByAdminLabel(searchString: string, fields: GFField[]) {
	const fieldsWithAdminLabels = fields.filter((field) => field.adminLabel);
	const fieldAdminLabels: string[] = fieldsWithAdminLabels.map(
		(field) => field.adminLabel
	) as string[]; // force this as a string array because TS cannot infer this correctly (extremely annoying)

	const opts = {};
	const uf = new uFuzzy(opts);
	const [haystackIdxs] = uf.search(fieldAdminLabels, searchString);

	if (haystackIdxs && fieldsWithAdminLabels[haystackIdxs[0]]) {
		return fieldsWithAdminLabels[haystackIdxs[0]];
	}

	return null;
}

/**
 * Given a list of spreadsheet column values and a list of fields to search,
 * returns a field whose options closely match the column values.
 */
function fuzzyMatchFieldByColumnContents(
	columnContents: string[],
	fields: GFField[]
) {
	const opts = {};
	const uf = new uFuzzy(opts);

	for (const field of fields) {
		// only supporting fuzzy matching on these field types for now.
		if (!['select', 'checkbox', 'radio'].includes(field.type)) {
			continue;
		}

		for (const choiceText of (field.choices ?? []).map(
			(choice) => choice.text
		)) {
			const [haystackIdxs] = uf.search(
				// field.choices.map((choice) => choice.text),
				columnContents,
				choiceText
			);

			if (
				haystackIdxs &&
				haystackIdxs[0] &&
				columnContents[haystackIdxs[0]]
			) {
				return field;
			}
		}

		const choices = (field.choices ?? []).map((choice) => choice.text);

		for (const data of columnContents) {
			const [haystackIdxs] = uf.search(choices, data);

			if (haystackIdxs && haystackIdxs[0] && fields[haystackIdxs[0]]) {
				return fields[haystackIdxs[0]];
			}
		}
	}

	return null;
}

/**
 * Determines if a spreadsheet row is a quantity condition row.
 */
function isRowQuantityCondtionRow(
	row: PrepareImportFileResponse['data']['disambiguation']['rows'][number]
) {
	return Object.values(row.values).every(
		(value) => value.startsWith('>') || value.includes('<')
	);
}

/**
 * Given a list of "stragegy" functions, returns the first value
 * returned by a strategy that is not null. If no strategy returns
 * a non-null value, returns the default field.
 */
function findFieldWithStrategies(strategies: Array<() => GFField | null>) {
	let matchedField = null;
	let i = 0;
	while (!matchedField && i < strategies.length) {
		matchedField = strategies[i]();
		i++;
	}

	return matchedField;
}

export function initialProductMappingsState(
	disambiguation: PrepareImportFileResponse['data']['disambiguation'] | null
) {
	if (!disambiguation) {
		return {};
	}

	return disambiguation.products.reduce((acc, product) => {
		const productFields = window.form.fields.filter(
			// @ts-ignore
			(field) => field.type === 'product'
		);

		const matchedField = findFieldWithStrategies([
			() => fuzzyMatchFieldByLabel(product.name, productFields),
			() => fuzzyMatchFieldByAdminLabel(product.name, productFields),
		]);

		if (matchedField) {
			acc[product.slug] = String(matchedField.id);
		}

		return acc;
	}, {} as { [key: string]: string });
}

export function initialColumnMappingsState(
	disambiguation: PrepareImportFileResponse['data']['disambiguation'] | null,
	fileContents: GetImportFileResponse['data']['file_contents'] | null
) {
	if (!disambiguation) {
		return {};
	}

	const rowConditionIndexes = disambiguation.rows.map((row) => row.index);
	const additionalOptions = getAdditionalConditionalLogicFieldOptions();

	return disambiguation.columns.reduce((acc, column) => {
		const columnContents = (fileContents || [])
			.filter((row, i) => !rowConditionIndexes.includes(i))
			.map((row) => row[column.index]);

		const matchedField = findFieldWithStrategies([
			() => fuzzyMatchFieldByLabel(column.name, window.form.fields),
			() =>
				fuzzyMatchFieldByColumnContents(
					columnContents,
					window.form.fields
				),
		]);

		if (matchedField) {
			acc[String(column.index)] = String(matchedField.id);
			return acc;
		}

		const matchedAdditionalOption = additionalOptions.find((option) => {
			return option.value === column.name || option.label === column.name;
		});

		if (matchedAdditionalOption) {
			acc[String(column.index)] = matchedAdditionalOption.value;
		}

		return acc;
	}, {} as { [key: string]: string });
}

export function initialRowMappingsState(
	disambiguation: PrepareImportFileResponse['data']['disambiguation'] | null,
	fileContents: GetImportFileResponse['data']['file_contents'] | null
) {
	if (!disambiguation) {
		return {};
	}

	return disambiguation.rows.reduce((acc, row) => {
		const productFields = window.form.fields.filter(
			// @ts-ignore
			(field) => field.type === 'product'
		);

		let matchedField = null;

		// find the corresponding product name from the CSV file and then attempt
		// to fuzzy match it to a product field.
		const productLine = (fileContents || []).find(
			(line, i) => i > row.index && line[0]
		);
		const product = productLine?.[0];
		if (product) {
			matchedField = fuzzyMatchFieldByLabel(product, productFields);
		}

		if (matchedField) {
			let matchedFieldId = String(matchedField.id);
			if (isRowQuantityCondtionRow(row)) {
				matchedFieldId = `quantity_${matchedFieldId}`;
			}

			acc[String(row.index)] = matchedFieldId;
		}

		return acc;
	}, {} as { [key: string]: string });
}
