import { __, _x } from '@wordpress/i18n';
import { useState, useEffect, useCallback } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs, isURL } from '@wordpress/url';
import { Spinner } from '@wordpress/components';

import InnerHTML from 'dangerously-set-html-content';
import { useAtom } from 'jotai';

import globalStore from './global-store';

const API_PATH = '/wp/v2/block-renderer';
const DEBOUNCE_FETCH = 500; // Used to debounce fetch request so that it only happens when the block's attributes haven't changed in 500ms.

/**
 * Check if a string is a URL (for stylesheets or scripts).
 *
 * Uses WordPress's isURL() utility from @wordpress/url for robust URL detection.
 *
 * @param {*} value The value to check.
 * @return {boolean} True if the value is a URL.
 */
const isUrl = ( value ) => {
	if ( ! value || typeof value !== 'string' ) {
		return false;
	}
	if ( isURL( value ) ) {
		return true;
	}
	// Relative paths (/, ./, ../) are URLs too.
	if ( value.charAt( 0 ) === '/' || value.indexOf( './' ) === 0 || value.indexOf( '../' ) === 0 ) {
		return true;
	}
	return false;
};

/**
 * Generate a simple hash code for a string.
 *
 * @param {string} str The string to hash.
 * @return {number} The hash code.
 */
const hashCode = ( str ) => {
	let hash = 0;
	for ( let i = 0; i < str.length; i++ ) {
		const char = str.charCodeAt( i );
		hash = ( ( hash << 5 ) - hash ) + char;
		hash = hash & hash; // Convert to 32-bit integer.
	}
	return Math.abs( hash );
};

/**
 * Inject a single inline CSS string into the document head, deduped by hash.
 *
 * @param {string} css Raw CSS text.
 */
const injectInlineCss = ( css ) => {
	if ( ! css || typeof css !== 'string' ) {
		return;
	}
	const cssHash = hashCode( css );
	const styleId = `gv-inline-css-${ cssHash }`;
	if ( ! document.getElementById( styleId ) ) {
		const el = document.createElement( 'style' );
		el.id = styleId;
		el.textContent = css;
		document.head.appendChild( el );
	}
};

export const loadAsset = ( { asset, type, onLoad } ) => {
	if ( type === 'js' ) {
		const el = document.createElement( 'script' );
		el.setAttribute( 'type', 'text/javascript' );
		el.setAttribute( 'src', asset );
		el.onload = onLoad;
		document.body.appendChild( el );
	} else {
		// CSS: asset can be a string (URL or raw CSS) or an object { src, before, after }.
		if ( typeof asset === 'object' && asset !== null ) {
			if ( asset.before ) {
				injectInlineCss( typeof asset.before === 'string' ? asset.before : asset.before.join( '\n' ) );
			}
			if ( asset.src && isUrl( asset.src ) ) {
				const el = document.createElement( 'link' );
				el.setAttribute( 'rel', 'stylesheet' );
				el.setAttribute( 'type', 'text/css' );
				el.setAttribute( 'href', asset.src );
				document.body.appendChild( el );
			}
			if ( asset.after ) {
				injectInlineCss( typeof asset.after === 'string' ? asset.after : asset.after.join( '\n' ) );
			}
		} else if ( isUrl( asset ) ) {
			const el = document.createElement( 'link' );
			el.setAttribute( 'rel', 'stylesheet' );
			el.setAttribute( 'type', 'text/css' );
			el.setAttribute( 'href', asset );
			document.body.appendChild( el );
		} else {
			injectInlineCss( asset );
		}
	}
};

const ServerSideRender = ( props ) => {
	const {
		block,
		blockPreviewImage,
		dataType,
		attributes,
		loadScripts,
		loadStyles,
		onEmptyResponse,
		onError,
		onLoading,
		onResponse
	} = props;

	const [ response, setResponse ] = useState( null );
	const [ isFetching, setIsFetching ] = useState( true );
	const [ error, setError ] = useState( null );

	const [ loadedScripts, setLoadedScripts ] = useAtom( globalStore.loadedScripts );
	const [ loadedStyles, setLoadedStyles ] = useAtom( globalStore.loadedStyles );

	useEffect( () => {
		const handler = setTimeout( () => fetch(), DEBOUNCE_FETCH );

		return () => clearTimeout( handler );
	}, [ attributes ] );

	const fetch = () => {
		const path = addQueryArgs( `${ API_PATH }/${ block }`, {
			context: 'edit',
			attributes,
		} );

		setIsFetching( true );

		apiFetch( { path } )
			.then( ( res ) => {
				if ( dataType === 'json' ) {
					const response = JSON.parse( res.rendered );

					if ( loadStyles ) {
						Object.values( response.styles ).forEach( ( asset ) => {
							if ( typeof asset === 'object' && asset !== null ) {
								// Always inject inline before/after CSS even if
								// the stylesheet URL was already loaded — each
								// block instance may have its own overrides.
								if ( asset.before ) {
									loadAsset( { asset: { before: asset.before }, type: 'css' } );
								}
								if ( asset.after ) {
									loadAsset( { asset: { after: asset.after }, type: 'css' } );
								}

								// Only load the stylesheet URL once.
								if ( asset.src && ! loadedStyles.has( asset.src ) ) {
									loadAsset( { asset: asset.src, type: 'css' } );
									setLoadedStyles( ( prev ) => new Set( prev ).add( asset.src ) );
								}
							} else if ( ! loadedStyles.has( asset ) ) {
								loadAsset( { asset, type: 'css' } );
								setLoadedStyles( ( prev ) => new Set( prev ).add( asset ) );
							}
						} );
					}

					if ( loadScripts ) {
						Object.values( response.scripts ).forEach( ( asset ) => {
							let assetToLoad = asset;

							if ( asset?.src ) {
								assetToLoad = asset.src;
							}

							// Always process inline data (localized scripts) even if the src is already loaded.
							if ( asset?.data ) {
								const el = document.createElement( 'script' );
								el.textContent = asset.data;
								document.head.appendChild( el );
							}

							if ( loadedScripts.has( asset ) || loadedScripts.has( assetToLoad ) ) {
								return;
							}

							loadAsset( { asset: assetToLoad, type: 'js' } );

							setLoadedScripts( ( prev ) => new Set( prev ).add( assetToLoad ) );
						} );
					}

					setTimeout( () => {
						setResponse( response.content );

						setIsFetching( false );
					}, 250 ); // Wait for scripts/styles to load.
				} else {
					setResponse( res.rendered );

					setIsFetching( false );
				}
			} )
			.catch( ( error ) => {
				setError( error );

				setIsFetching( false );
			} );
	};

	if ( error ) {
		return typeof onError === 'function'
			? onError( error )
			: (
				<div className="error-state">
					{
						_x( 'The block could not be rendered due to an error: [error]', '[error] placeholder will be replaced with an error message and is not to be translated.', 'gk-gravityview' )
							.replace( '[error]', error.message )
					}
				</div>
			);
	}

	// If the block was previously rendered, do not clear existing response and just display the spinner; this prevents the unsightly content shift.
	if ( isFetching && response ) {
		return typeof onLoading === 'function'
			? onLoading( response )
			: (
				<div className="loading-state">
					<div className="loader">
						<Spinner />
					</div>
					<InnerHTML html={ response } />
				</div>
			);
	}

	if ( isFetching ) {
		return typeof onLoading === 'function'
			? onLoading()
			: (
				<div className="loading-state initial">
					<div className="loader">
						<Spinner />
					</div>
					{ blockPreviewImage }
				</div>
			);
	}

	if ( !response ) {
		return typeof onEmptyResponse === 'function'
			? onEmptyResponse()
			: (
				<div className="empty-response">
					<p>
						{ __( 'The block did not render any content.', 'gk-gravityview' ) }
					</p>
				</div>
			);
	}

	return typeof onResponse === 'function'
		? onResponse( response )
		: <InnerHTML html={ response } />;
};

export default ServerSideRender;
