import { Destination } from 'types';

/**
 * Map of common address abbreviations to their full names.
 * We only use this to ensure that leveinstein distance is not affected by abbreviations.
 */
export const addresAbbreviationMap: Record< string, string > = {
	STREET: 'ST',
	AVENUE: 'AVE',
	BOULEVARD: 'BLVD',
	ROAD: 'RD',
	DRIVE: 'DR',
	LANE: 'LN',
	COURT: 'CT',
	PARKWAY: 'PKWY',
	PLACE: 'PL',
	TERRACE: 'TER',
	CIRCLE: 'CIR',
	HIGHWAY: 'HWY',
	MOUNT: 'MT',
	MOUNTAIN: 'MTN',
	SQUARE: 'SQ',
	SUITE: 'STE',
	BUILDING: 'BLDG',
	FLOOR: 'FL',
	ROOM: 'RM',
	APARTMENT: 'APT',
	UNIT: 'UNIT',
	HARBOR: 'HBR',
	ISLAND: 'IS',
	CREEK: 'CRK',
	HEIGHTS: 'HTS',
	SPRING: 'SPG',
	VALLEY: 'VLY',
	CROSSING: 'XING',
	NORTH: 'N',
	SOUTH: 'S',
	EAST: 'E',
	WEST: 'W',
	// Add other common abbreviations as needed
};

/**
 * Normalise an address by removing periods, commas, extra spaces, and converting to uppercase.
 *
 * @param address The address to normalise
 *
 * @return The normalised address
 */
export const normaliseAddress = ( address: string ): string => {
	let normalised = address
		.toUpperCase() // Convert to uppercase
		.replace( /\./g, '' ) // Remove periods
		.replace( /,/g, '' ) // Remove commas
		.replace( /\s+/g, ' ' ); // Remove extra spaces
	// Replace full words with abbreviations.
	Object.keys( addresAbbreviationMap ).forEach( ( abbr ) => {
		const regex = new RegExp( `\\b${ abbr }\\b`, 'g' ); // Match the abbreviation as a whole word
		normalised = normalised.replace( regex, addresAbbreviationMap[ abbr ] );
	} );

	return normalised;
};

/**
 * Compare two addresses and return the Levenshtein distance between them to determine similarity.
 *
 * @param address1 The first address to compare
 * @param address2 The second address to compare
 *
 * @return number
 */
export const levenshteinDistance = (
	address1: string,
	address2: string
): number => {
	const matrix: number[][] = [];

	// Increment along the first column of each row
	for ( let i = 0; i <= address2.length; i++ ) {
		matrix[ i ] = [ i ];
	}

	// Increment each column in the first row
	for ( let j = 0; j <= address1.length; j++ ) {
		matrix[ 0 ][ j ] = j;
	}

	// Fill the matrix
	for ( let i = 1; i <= address2.length; i++ ) {
		for ( let j = 1; j <= address1.length; j++ ) {
			if ( address2.charAt( i - 1 ) === address1.charAt( j - 1 ) ) {
				matrix[ i ][ j ] = matrix[ i - 1 ][ j - 1 ];
			} else {
				matrix[ i ][ j ] = Math.min(
					matrix[ i - 1 ][ j - 1 ] + 1,
					Math.min(
						matrix[ i ][ j - 1 ] + 1,
						matrix[ i - 1 ][ j ] + 1
					)
				);
			}
		}
	}

	return matrix[ address2.length ][ address1.length ];
};

/**
 * Compare two addresses to determine if they are similar.
 * @param address1 The first address to compare
 * @param address2 The second address to compare
 * @return boolean True if the addresses are similar, false otherwise
 */
export const areAddressesClose = (
	address1: Destination,
	address2: Destination
): boolean => {
	const normStreet1Address1 = address1.address1
		? normaliseAddress( address1.address1 )
		: '';
	const normStreet1Address2 = address2.address1
		? normaliseAddress( address2.address1 )
		: '';
	const normStreet2Address1 = address1.address2
		? normaliseAddress( address1.address2 )
		: '';
	const normStreet2Address2 = address2.address2
		? normaliseAddress( address2.address2 )
		: '';
	const normCityAddress1 = address1.city?.toUpperCase() ?? '';
	const normCityAddress2 = address2.city?.toUpperCase() ?? '';
	const normStateAddress1 = address1.state?.toUpperCase() ?? '';
	const normStateAddress2 = address2.state?.toUpperCase() ?? '';
	// Extract the base 5 digits of the ZIP code to account for ZIP+4 codes
	const normPostalCodeAddress1 = address1.postcode.slice( 0, 5 ); // First 5 digits
	const normPostalCodeAddress2 = address2.postcode.slice( 0, 5 ); // First 5 digits
	const normCountryAddress1 = address1.country.toUpperCase();
	const normCountryAddress2 = address2.country.toUpperCase();

	// Set thresholds (adjust as necessary)
	const STREET_THRESHOLD = 5; // Maximum number of character changes allowed for street

	// Compare each aspect of the addresses
	const isStreetSimilar =
		levenshteinDistance( normStreet1Address1, normStreet1Address2 ) <=
		STREET_THRESHOLD;
	const isStreet2Similar =
		levenshteinDistance( normStreet2Address1, normStreet2Address2 ) <=
		STREET_THRESHOLD;
	const isCitySimilar = normCityAddress1 === normCityAddress2;
	const isStateSimilar = normStateAddress1 === normStateAddress2;
	const isPostalCodeSimilar =
		normPostalCodeAddress1 === normPostalCodeAddress2; // Compare only the first 5 digits
	const isCountrySimilar = normCountryAddress1 === normCountryAddress2;

	// Return true if all key address parts are considered "similar"
	return (
		isStreetSimilar &&
		isStreet2Similar &&
		isCitySimilar &&
		isStateSimilar &&
		isPostalCodeSimilar &&
		isCountrySimilar
	);
};

/**
 * Check if only checkbox fields (defaultAddress, defaultReturnAddress) have changed
 * and no actual address data fields have been modified.
 *
 * @param originalAddress The original address object
 * @param newAddress      The new address object with potentially changed values
 * @return boolean True if only checkbox fields changed, false otherwise
 */
export const hasOnlyCheckboxChanges = < T extends Record< string, unknown > >(
	originalAddress: T,
	newAddress: T
): boolean => {
	const addressFields = [
		'name',
		'company',
		'address',
		'city',
		'state',
		'postcode',
		'country',
		'email',
		'phone',
		'firstName',
		'lastName',
	];

	// Check if any actual address fields have changed
	const hasAddressFieldChanges = addressFields.some( ( field ) => {
		const originalValue = originalAddress[ field as keyof T ];
		const newValue = newAddress[ field as keyof T ];

		// Type-agnostic comparison to handle cases like 1 vs '1'
		// eslint-disable-next-line eqeqeq
		return originalValue != newValue;
	} );

	return ! hasAddressFieldChanges;
};
