/**
 * External dependencies
 */
import {
	createContext,
	useContext,
	useReducer,
	useRef,
	useEffect,
} from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import {
	CHECKOUT_STORE_KEY,
	PAYMENT_STORE_KEY,
	VALIDATION_STORE_KEY,
} from '@woocommerce/block-data';

/**
 * Internal dependencies
 */
import { useEventEmitters, reducer as emitReducer } from './event-emit';
import { emitterCallback } from '../../../event-emit';

type PaymentEventsContextType = {
	// Event registration callback for registering observers for the payment processing event.
	onPaymentProcessing: ReturnType< typeof emitterCallback >;
};

const PaymentEventsContext = createContext< PaymentEventsContextType >( {
	onPaymentProcessing: () => () => () => void null,
} );

export const usePaymentEventsContext = () => {
	return useContext( PaymentEventsContext );
};

/**
 * PaymentEventsProvider is automatically included in the CheckoutProvider.
 *
 * This provides the api interface (via the context hook) for payment status and data.
 *
 * @param {Object} props          Incoming props for provider
 * @param {Object} props.children The wrapped components in this provider.
 */
export const PaymentEventsProvider = ( {
	children,
}: {
	children: React.ReactNode;
} ): JSX.Element => {
	const {
		isProcessing: checkoutIsProcessing,
		isIdle: checkoutIsIdle,
		isCalculating: checkoutIsCalculating,
		hasError: checkoutHasError,
	} = useSelect( ( select ) => {
		const store = select( CHECKOUT_STORE_KEY );
		return {
			isProcessing: store.isProcessing(),
			isIdle: store.isIdle(),
			hasError: store.hasError(),
			isCalculating: store.isCalculating(),
		};
	} );
	const { isPaymentReady } = useSelect( ( select ) => {
		const store = select( PAYMENT_STORE_KEY );

		return {
			// The PROCESSING status represents befor the checkout runs the observers
			// registered for the payment_setup event.
			isPaymentProcessing: store.isPaymentProcessing(),
			// the READY status represents when the observers have finished processing and payment data
			// synced with the payment store, ready to be sent to the StoreApi
			isPaymentReady: store.isPaymentReady(),
		};
	} );

	const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
	const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
	const { onPaymentProcessing } = useEventEmitters( observerDispatch );
	const currentObservers = useRef( observers );

	// ensure observers are always current.
	useEffect( () => {
		currentObservers.current = observers;
	}, [ observers ] );

	const {
		__internalSetPaymentProcessing,
		__internalSetPaymentIdle,
		__internalEmitPaymentProcessingEvent,
	} = useDispatch( PAYMENT_STORE_KEY );

	// flip payment to processing if checkout processing is complete and there are no errors
	useEffect( () => {
		if (
			checkoutIsProcessing &&
			! checkoutHasError &&
			! checkoutIsCalculating
		) {
			__internalSetPaymentProcessing();

			// Note: the nature of this event emitter is that it will bail on any
			// observer that returns a response that !== true. However, this still
			// allows for other observers that return true for continuing through
			// to the next observer (or bailing if there's a problem).
			__internalEmitPaymentProcessingEvent(
				currentObservers.current,
				setValidationErrors
			);
		}
	}, [
		checkoutIsProcessing,
		checkoutHasError,
		checkoutIsCalculating,
		__internalSetPaymentProcessing,
		__internalEmitPaymentProcessingEvent,
		setValidationErrors,
	] );

	// When checkout is returned to idle, and the payment setup has not completed, set payment status to idle
	useEffect( () => {
		if ( checkoutIsIdle && ! isPaymentReady ) {
			__internalSetPaymentIdle();
		}
	}, [ checkoutIsIdle, isPaymentReady, __internalSetPaymentIdle ] );

	// if checkout has an error sync payment status back to idle.
	useEffect( () => {
		if ( checkoutHasError && isPaymentReady ) {
			__internalSetPaymentIdle();
		}
	}, [ checkoutHasError, isPaymentReady, __internalSetPaymentIdle ] );

	const paymentContextData = {
		onPaymentProcessing,
	};

	return (
		<PaymentEventsContext.Provider value={ paymentContextData }>
			{ children }
		</PaymentEventsContext.Provider>
	);
};
