import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
import type { ReactNode } from 'react';

import {
	type AdminData,
	type NavigationItem,
	safeArray,
	safeRecord,
	textValue,
} from './admin-data';
import { appNavigationTarget } from './admin-navigation';

interface Props {
	data: AdminData;
	renderScreen: ( props: AdminShellRenderProps ) => ReactNode;
}

export interface ProductDetailRoute {
	type: 'change' | 'recording';
	uuid: string;
}

export interface AdminShellRenderProps {
	data: AdminData;
	currentScreen: string;
	detailRoute: ProductDetailRoute | null;
	onOpenChangeDetail: ( uuid: string ) => void;
	onOpenRecordingDetail: ( uuid: string ) => void;
	onCloseDetail: () => void;
}

const fetchScreenData = async (
	screen: string,
	ajaxUrl: string,
	nonce: string
): Promise< AdminData > => {
	const requestUrl = new URL( ajaxUrl, window.location.href );
	requestUrl.searchParams.set(
		'action',
		'scheduled_changes_admin_screen_data'
	);
	requestUrl.searchParams.set( 'screen', screen );
	requestUrl.searchParams.set( 'nonce', nonce );

	const response = await window.fetch( requestUrl.toString(), {
		credentials: 'same-origin',
		headers: { Accept: 'application/json' },
	} );
	const payload = safeRecord( await response.json() );

	if ( ! response.ok || true !== payload.success ) {
		throw new Error(
			textValue(
				safeRecord( payload.data ).message,
				"That JJ's Post and Product Scheduler screen could not load."
			)
		);
	}

	return safeRecord( payload.data ) as AdminData;
};

const screenCacheFromData = (
	data: AdminData,
	fallbackScreen: string
): Record< string, AdminData > => {
	const cache: Record< string, AdminData > = {};
	const screen = textValue( data.currentScreen, fallbackScreen );

	cache[ screen ] = data;

	for ( const [ key, value ] of Object.entries(
		safeRecord( data.preloadedScreens )
	) ) {
		const preloaded = safeRecord( value ) as AdminData;
		const preloadedScreen = textValue( preloaded.currentScreen, key );

		if ( preloadedScreen ) {
			cache[ preloadedScreen ] = preloaded;
		}
	}

	return cache;
};

export default function AdminShell( { data, renderScreen }: Props ) {
	const initialScreen = textValue( data.currentScreen, 'schedule-center' );
	const [ screenData, setScreenData ] = useState< AdminData >( data );
	const [ currentScreen, setCurrentScreen ] = useState( initialScreen );
	const [ detailRoute, setDetailRoute ] =
		useState< ProductDetailRoute | null >( detailRouteFromLocation );
	const [ screenError, setScreenError ] = useState( '' );
	const [ loadingScreen, setLoadingScreen ] = useState( '' );
	const appRef = useRef< HTMLDivElement | null >( null );
	const screenCacheRef = useRef< Record< string, AdminData > >(
		screenCacheFromData( data, initialScreen )
	);
	const preloadingScreensRef = useRef< Set< string > >( new Set() );
	const navigation = useMemo(
		() => safeArray< NavigationItem >( screenData.navigation ),
		[ screenData.navigation ]
	);
	const effectiveData = useMemo(
		() => ( { ...screenData, currentScreen } ),
		[ currentScreen, screenData ]
	);

	useEffect( () => {
		const screen = textValue( data.currentScreen, 'schedule-center' );
		screenCacheRef.current = screenCacheFromData( data, screen );
		setScreenData( data );
		setCurrentScreen( screen );
	}, [ data ] );

	useEffect( () => {
		const ajaxUrl = textValue( screenData.ajaxUrl );
		const nonce = textValue( screenData.nonce );

		if ( ! ajaxUrl || ! nonce || 'function' !== typeof window.fetch ) {
			return;
		}

		for ( const item of navigation ) {
			const screen = textValue( item.id );
			if (
				! screen ||
				screenCacheRef.current[ screen ] ||
				preloadingScreensRef.current.has( screen )
			) {
				continue;
			}

			preloadingScreensRef.current.add( screen );
			void fetchScreenData( screen, ajaxUrl, nonce )
				.then( ( nextData ) => {
					const nextScreen = textValue(
						nextData.currentScreen,
						screen
					);
					screenCacheRef.current[ nextScreen ] = nextData;
				} )
				.catch( () => {
					// Navigation still falls back to an on-demand fetch.
				} )
				.finally( () => {
					preloadingScreensRef.current.delete( screen );
				} );
		}
	}, [ navigation, screenData.ajaxUrl, screenData.nonce ] );

	useEffect( () => {
		const syncDetailRoute = () =>
			setDetailRoute( detailRouteFromLocation() );

		window.addEventListener( 'popstate', syncDetailRoute );

		return () => window.removeEventListener( 'popstate', syncDetailRoute );
	}, [] );

	useEffect( () => {
		const app = appRef.current;

		if ( ! app ) {
			return undefined;
		}

		const navigate = async ( screen: string, href: string ) => {
			const ajaxUrl = textValue( screenData.ajaxUrl );
			const nonce = textValue( screenData.nonce );

			if ( ! ajaxUrl || ! nonce || 'function' !== typeof window.fetch ) {
				window.location.href = href;
				return;
			}

			setCurrentScreen( screen );
			setDetailRoute( null );
			setScreenError( '' );
			pushAppRoute( href );

			const cachedData = screenCacheRef.current[ screen ];
			if ( cachedData ) {
				setScreenData( cachedData );
				setCurrentScreen(
					textValue( cachedData.currentScreen, screen )
				);
				setLoadingScreen( '' );
				return;
			}

			setLoadingScreen( screen );

			try {
				const nextData = await fetchScreenData(
					screen,
					ajaxUrl,
					nonce
				);
				screenCacheRef.current[
					textValue( nextData.currentScreen, screen )
				] = nextData;
				setScreenData( nextData );
				setCurrentScreen( textValue( nextData.currentScreen, screen ) );
			} catch ( error ) {
				setScreenError(
					error instanceof Error && error.message
						? error.message
						: "That JJ's Post and Product Scheduler screen could not load."
				);
			} finally {
				setLoadingScreen( '' );
			}
		};

		const handleClick = ( event: MouseEvent ) => {
			const target = appNavigationTarget( event, app, navigation );

			if ( ! target ) {
				return;
			}

			event.preventDefault();
			void navigate( target.item.id, target.href );
		};

		app.addEventListener( 'click', handleClick );

		return () => app.removeEventListener( 'click', handleClick );
	}, [ navigation, screenData ] );

	const notice = screenError
		? { type: 'error', message: screenError }
		: effectiveData.notice;
	const renderData = {
		...effectiveData,
		notice: loadingScreen
			? {
					type: 'info',
					message: "Loading JJ's Post and Product Scheduler screen.",
			  }
			: notice,
	};

	return (
		<div data-scheduled-changes-shell ref={ appRef }>
			{ renderScreen( {
				data: renderData,
				currentScreen,
				detailRoute,
				onOpenChangeDetail: ( uuid ) => {
					setDetailRoute( { type: 'change', uuid } );
					pushProductDetailRoute( 'change', uuid );
				},
				onOpenRecordingDetail: ( uuid ) => {
					setDetailRoute( { type: 'recording', uuid } );
					pushProductDetailRoute( 'recording', uuid );
				},
				onCloseDetail: () => {
					setDetailRoute( null );
					clearProductDetailRoute();
				},
			} ) }
		</div>
	);
}

const detailRouteFromLocation = (): ProductDetailRoute | null => {
	if ( 'undefined' === typeof window ) {
		return null;
	}

	const params = new URLSearchParams( window.location.search );
	if ( 'change' === params.get( 'detail' ) && params.get( 'launch' ) ) {
		return { type: 'change', uuid: params.get( 'launch' ) || '' };
	}
	if ( 'recording' === params.get( 'detail' ) && params.get( 'recording' ) ) {
		return {
			type: 'recording',
			uuid: params.get( 'recording' ) || '',
		};
	}

	return null;
};

const pushProductDetailRoute = (
	type: ProductDetailRoute[ 'type' ],
	uuid: string
): void => {
	if ( 'undefined' === typeof window || ! window.history || ! uuid ) {
		return;
	}

	const url = new URL( window.location.href );
	url.hash = '';
	url.searchParams.set( 'detail', type );
	if ( 'change' === type ) {
		url.searchParams.set( 'launch', uuid );
		url.searchParams.delete( 'recording' );
	} else {
		url.searchParams.set( 'recording', uuid );
		url.searchParams.delete( 'launch' );
	}
	window.history.pushState(
		{
			...( 'object' === typeof window.history.state &&
			null !== window.history.state
				? window.history.state
				: {} ),
			scheduledChangesDetail: type,
			scheduledChangesDetailUuid: uuid,
		},
		'',
		url.toString()
	);
};

const clearProductDetailRoute = (): void => {
	if ( 'undefined' === typeof window || ! window.history ) {
		return;
	}

	const url = new URL( window.location.href );
	url.searchParams.delete( 'detail' );
	url.searchParams.delete( 'launch' );
	url.searchParams.delete( 'recording' );
	window.history.pushState(
		{
			...( 'object' === typeof window.history.state &&
			null !== window.history.state
				? window.history.state
				: {} ),
			scheduledChangesDetail: '',
			scheduledChangesDetailUuid: '',
		},
		'',
		url.toString()
	);
};

const pushAppRoute = ( href: string ): void => {
	if ( 'undefined' === typeof window || ! window.history ) {
		return;
	}

	window.history.pushState(
		{
			...( 'object' === typeof window.history.state &&
			null !== window.history.state
				? window.history.state
				: {} ),
			scheduledChangesScreen: href,
		},
		'',
		href
	);
};
