/**
 * External dependencies
 */
import { debounce } from 'lodash';
import { select, dispatch } from '@wordpress/data';
import {
	formatStoreApiErrorMessage,
	pluckAddress,
	pluckEmail,
} from '@woocommerce/base-utils';
import {
	CartResponseBillingAddress,
	CartResponseShippingAddress,
} from '@woocommerce/type-defs/cart-response';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { BillingAddressShippingAddress } from '@woocommerce/type-defs/cart';

/**
 * Internal dependencies
 */
import { STORE_KEY } from './constants';

declare type CustomerData = {
	billingAddress: CartResponseBillingAddress;
	shippingAddress: CartResponseShippingAddress;
};

/**
 * Checks if a cart response contains an email property.
 */
const isCartResponseBillingAddress = (
	address: CartResponseBillingAddress | CartResponseShippingAddress
): address is CartResponseBillingAddress => {
	return 'email' in address;
};

/**
 * Does a shallow compare of important address data to determine if the cart needs updating on the server. This takes
 * the current and previous address into account, as well as the billing email field.
 */
const isAddressDirty = <
	T extends CartResponseBillingAddress | CartResponseShippingAddress
>(
	// An object containing all previous address information
	previousAddress: T,
	// An object containing all address information.
	address: T
): boolean => {
	if (
		isCartResponseBillingAddress( address ) &&
		pluckEmail( address ) !==
			pluckEmail( previousAddress as CartResponseBillingAddress )
	) {
		return true;
	}

	return (
		!! address.country &&
		! isShallowEqual(
			pluckAddress( previousAddress ),
			pluckAddress( address )
		)
	);
};

/**
 * Local cache of customerData used for comparisons.
 */
let customerData = <CustomerData>{
	billingAddress: {},
	shippingAddress: {},
};
// Tracks if customerData has been populated.
let customerDataIsInitialized = false;

/**
 * Tracks which props have changed so the correct data gets pushed to the server.
 */
const dirtyProps = {
	billingAddress: false,
	shippingAddress: false,
};

/**
 * Function to dispatch an update to the server. This is debounced.
 */
const updateCustomerData = debounce( (): void => {
	const { billingAddress, shippingAddress } = customerData;
	const customerDataToUpdate = {} as Partial< BillingAddressShippingAddress >;

	if ( dirtyProps.billingAddress ) {
		customerDataToUpdate.billing_address = billingAddress;
		dirtyProps.billingAddress = false;
	}

	if ( dirtyProps.shippingAddress ) {
		customerDataToUpdate.shipping_address = shippingAddress;
		dirtyProps.shippingAddress = false;
	}

	if ( Object.keys( customerDataToUpdate ).length ) {
		dispatch( STORE_KEY )
			.updateCustomerData( customerDataToUpdate )
			.then( () => {
				dispatch( 'core/notices' ).removeNotice(
					'checkout',
					'wc/checkout'
				);
			} )
			.catch( ( response ) => {
				dispatch( 'core/notices' ).createNotice(
					'error',
					formatStoreApiErrorMessage( response ),
					{
						id: 'checkout',
						context: 'wc/checkout',
					}
				);
			} );
	}
}, 1000 );

/**
 * After cart has fully initialized, pushes changes to the server when data in the store is changed. Updates to the
 * server are debounced to prevent excessive requests.
 */
export const pushChanges = (): void => {
	const store = select( STORE_KEY );
	const isInitialized = store.hasFinishedResolution( 'getCartData' );

	if ( ! isInitialized ) {
		return;
	}

	const newCustomerData = store.getCustomerData();

	if ( ! customerDataIsInitialized ) {
		customerData = newCustomerData;
		customerDataIsInitialized = true;
		return;
	}

	// An address is dirty and needs pushing to the server if the email, country, state, city, or postcode have changed.
	if (
		isAddressDirty(
			customerData.billingAddress,
			newCustomerData.billingAddress
		)
	) {
		dirtyProps.billingAddress = true;
	}

	if (
		isAddressDirty(
			customerData.shippingAddress,
			newCustomerData.shippingAddress
		)
	) {
		dirtyProps.shippingAddress = true;
	}

	customerData = newCustomerData;

	if ( dirtyProps.billingAddress || dirtyProps.shippingAddress ) {
		updateCustomerData();
	}
};
