/**
 * Load-once-per-key data fetching, deliberately small.
 *
 * With a `key`, what was loaded is kept for the life of this wp-admin page
 * (screenCache.ts): going back to a screen shows it at once, without loading
 * it again. Saves and syncs empty that memory.
 *
 * `reload` exists because several screens act and then need the truth again —
 * creating a page, saving settings — and re-running the same loader is a
 * more honest refresh than patching local state to match what we hope the
 * server did.
 */

import { useCallback, useEffect, useRef, useState } from 'react';
import { ApiError } from './api';
import { cacheScreen, cachedScreen } from './screenCache';
import { onSynced } from './sync';

export interface Async< T > {
	data: T | null;
	error: ApiError | Error | null;
	loading: boolean;
	reload: () => void;
	/** Replace the loaded value without a round trip, after a write that returned it. */
	set: ( value: T ) => void;
}

/**
 * @param loader The read.
 * @param deps   Re-read when these change.
 * @param key    Remember the result under this key (the request path) while
 *               the page is open; a later mount shows it without loading.
 */
export function useAsync< T >( loader: () => Promise< T >, deps: unknown[] = [], key?: string ): Async< T > {
	const remembered = key === undefined ? undefined : cachedScreen< T >( key );
	const [ data, setData ] = useState< T | null >( remembered ?? null );
	const [ error, setError ] = useState< ApiError | Error | null >( null );
	const [ loading, setLoading ] = useState( remembered === undefined );
	const [ tick, setTick ] = useState( 0 );

	// The first run shows what was remembered instead of loading it again.
	const skipFirst = useRef( remembered !== undefined );

	// A screen the merchant navigated away from must not overwrite the next
	// screen's state when its request finally lands.
	const live = useRef( true );

	useEffect( () => {
		live.current = true;

		return () => {
			live.current = false;
		};
	}, [] );

	useEffect( () => {
		if ( skipFirst.current ) {
			skipFirst.current = false;

			return;
		}

		let current = true;

		setLoading( true );
		setError( null );

		loader()
			.then( ( value ) => {
				if ( key !== undefined ) {
					cacheScreen( key, value );
				}

				if ( current && live.current ) {
					setData( value );
				}
			} )
			.catch( ( caught: Error ) => {
				if ( current && live.current ) {
					setError( caught );
				}
			} )
			.finally( () => {
				if ( current && live.current ) {
					setLoading( false );
				}
			} );

		return () => {
			current = false;
		};
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [ tick, ...deps ] );

	const reload = useCallback( () => setTick( ( n ) => n + 1 ), [] );

	useEffect( () => onSynced( reload ), [ reload ] );

	const set = useCallback(
		( value: T ) => {
			if ( key !== undefined ) {
				cacheScreen( key, value );
			}

			setData( value );
		},
		[ key ]
	);

	return { data, error, loading, reload, set };
}
