/**
 * External dependencies
 */
import {
	BlockContextProvider,
	__experimentalUseBlockPreview as useBlockPreview,
	useBlockProps,
	useInnerBlocksProps,
	store as blockEditorStore,
} from '@wordpress/block-editor';
import { Spinner } from '@wordpress/components';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { memo, useMemo, useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies.
 */
import { spectraClassNames } from '@spectra-helpers';
import { useSpectraStyles } from '@spectra-hooks';

/**
 * Device type constant.
 */
const DESKTOP = 'Desktop';

/**
 * Default template for inner blocks.
 * Defines the structure of each post item.
 */
const TEMPLATE = [
	[
		'core/post-featured-image',
		{
			aspectRatio: '16/9',
		},
	],
	[ 'core/post-title' ],
	[ 'core/post-date' ],
];

/**
 * Renders the inner blocks for a single post in the template.
 *
 * @param {string[]|string} classList The array of CSS classes for the post or a string.
 * @return {Element} The rendered list item containing inner blocks.
 */
const PostTemplateInnerBlocks = ( { classList } ) => {
	const innerBlocksProps = useInnerBlocksProps(
		{
			className: spectraClassNames( [ 'spectra-block-post', ...( Array.isArray( classList ) ? classList : [] ) ] ),
		},
		{ template: TEMPLATE, __unstableDisableLayoutClassNames: true }
	);
	return <li { ...innerBlocksProps } />;
};

/**
 * Renders the inner blocks for a single post in the template.
 *
 * @param {Object} props - The component's props.
 * @return {Element} The rendered list item containing inner blocks.
 */
const PostTemplateBlockPreview = ( props ) => {
	const { blocks, blockContextId, classList, isHidden, setActiveBlockContextId } = props;
	const blockPreviewProps = useBlockPreview( {
		blocks,
		props: {
			className: spectraClassNames( [ 'spectra-block-post', ...( Array.isArray( classList ) ? classList : [] ) ] ),
		},
	} );

	// Handler to set the active block context when the preview is clicked or activated via keyboard.
	const handleOnClick = () => {
		setActiveBlockContextId( blockContextId );
	};

	const style = {
		display: isHidden ? 'none' : undefined,
	};

	return (
		<li
			{ ...blockPreviewProps }
			tabIndex={ 0 }
			// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
			role="button"
			onClick={ handleOnClick }
			onKeyPress={ handleOnClick }
			style={ style }
		/>
	);
};

// Memoize the preview component to prevent unnecessary re-renders.
const MemoizedPostTemplateBlockPreview = memo( PostTemplateBlockPreview );

/**
 * Renders a post template block for a Loop Builder.
 * Fetches and displays posts based on query parameters.
 *
 * @since x.x.x
 *
 * @param {Object} props The block props, including context and attributes.
 * @return {Element} The rendered block element.
 */
const Render = ( props ) => {
	const {
		clientId,
		context: {
			'spectra-pro/loop-builder/query': {
				perPage,
				offset = 0,
				postType,
				order,
				orderBy,
				author,
				search,
				exclude,
				sticky,
				taxQuery,
				parents,
				format,
				...restQueryArgs
			} = {},
		},
		attributes,
		__unstableLayoutClassNames,
	} = props;

	// State for tracking active block context.
	const [ activeBlockContextId, setActiveBlockContextId ] = useState();

	// State for tracking device type changes to force re-renders.
	const [ renderKey, setRenderKey ] = useState( 0 );

	// Fetch posts and inner blocks data using useSelect.
	const { posts, blocks, deviceType } = useSelect(
		( select ) => {
			// Selectors for core data.
			const { getEntityRecords, getTaxonomies } = select( coreStore );

			// Selector for block editor data.
			const { getBlocks } = select( blockEditorStore );

			// Get current device type to trigger re-render on device change.
			const currentDeviceType = select( editorStore )?.getDeviceType() || DESKTOP;

			const query = {
				offset: offset || 0,
				order,
				orderby: orderBy,
			};

			// Handle taxonomy queries.
			if ( taxQuery ) {
				const taxonomies = getTaxonomies( {
					type: postType,
					per_page: -1,
					context: 'view',
				} );

				// We have to build the tax query for the REST API and use as.
				// keys the taxonomies `rest_base` with the `term ids` as values.
				const builtTaxQuery = Object.entries( taxQuery ).reduce( ( accumulator, [ taxonomySlug, terms ] ) => {
					// Find the taxonomy by slug.
					const taxonomy = taxonomies?.find( ( { slug } ) => slug === taxonomySlug );

					// If the taxonomy is found, add it to the accumulator.
					if ( taxonomy?.rest_base ) {
						accumulator[ taxonomy?.rest_base ] = terms;
					}

					// Return the accumulator.
					return accumulator;
				}, {} );

				// If the built tax query is not empty, add it to the query.
				if ( !! Object.keys( builtTaxQuery ).length ) {
					Object.assign( query, builtTaxQuery );
				}
			}

			if ( perPage ) query.per_page = perPage; // Limit the number of posts per page.
			if ( author ) query.author = author; // Filter by author.
			if ( search ) query.search = search; // Filter by search term.
			if ( exclude?.length ) query.exclude = exclude; // Filter out specific post IDs.
			if ( parents?.length ) query.parent = parents; // Filter by parent post ID.
			if ( format?.length ) query.format = format; // Filter by post format.

			// Handle cases where sticky is set to `exclude` or `only`. Which works as a `post__in/post__not_in` query for sticky posts.
			if ( sticky && sticky !== 'ignore' ) {
				query.sticky = sticky === 'only';
			}

			if ( sticky === 'ignore' ) {
				// Remove any leftover sticky query parameter.
				delete query.sticky;
				query.ignore_sticky = true;
			}

			return {
				posts: getEntityRecords( 'postType', postType, {
					...query,
					...restQueryArgs,
				} ),
				blocks: getBlocks( clientId ),
				deviceType: currentDeviceType,
			};
		},
		[
			perPage,
			offset,
			order,
			orderBy,
			clientId,
			author,
			search,
			postType,
			exclude,
			sticky,
			taxQuery,
			parents,
			format,
			restQueryArgs,
		]
	);

	// Force re-render when device type changes to update responsive styles.
	useEffect( () => {
		setRenderKey( ( prev ) => prev + 1 );
	}, [ deviceType ] );

	// Map over posts to create block context data.
	const blockContexts = useMemo(
		() =>
			posts?.map( ( post ) => ( {
				postType: post.type,
				postId: post.id,
				classList: post.class_list ?? [],
			} ) ),
		[ posts ]
	);

	// Configuration for the useSpectraStyles hook.
	const config = [
		{ key: 'textColor' },
		{ key: 'textColorHover' },
		{ key: 'backgroundColor' },
		{ key: 'backgroundColorHover' },
		{ key: 'backgroundGradient' },
		{ key: 'backgroundGradientHover' },
	];

	// Generate styles and class names.
	const { style, classNames } = useSpectraStyles( attributes, config, [ __unstableLayoutClassNames ] );

	// Use the block props, with the added CSS variables and their related classes.
	const blockProps = useBlockProps( {
		className: spectraClassNames( classNames ),
		style,
	} );

	// If no posts are fetched, show a spinner.
	if ( ! posts ) {
		return (
			<p { ...blockProps }>
				<Spinner />
			</p>
		);
	}

	// If no posts are found, display a 'No results' message.
	if ( ! posts.length ) {
		return <p { ...blockProps }> { __( 'No results found.', 'spectra-pro' ) }</p>;
	}

	return (
		<>
			<ul { ...blockProps } key={ `render-${ renderKey }` }>
				{ blockContexts &&
					blockContexts.map( ( blockContext ) => (
						<BlockContextProvider key={ blockContext.postId } value={ blockContext }>
							{ blockContext.postId === ( activeBlockContextId || blockContexts[ 0 ]?.postId ) ? (
								<PostTemplateInnerBlocks classList={ blockContext.classList } />
							) : null }
							<MemoizedPostTemplateBlockPreview
								blocks={ blocks }
								blockContextId={ blockContext.postId }
								classList={ blockContext.classList }
								setActiveBlockContextId={ setActiveBlockContextId }
								isHidden={
									blockContext.postId === ( activeBlockContextId || blockContexts[ 0 ]?.postId )
								}
							/>
						</BlockContextProvider>
					) ) }
			</ul>
		</>
	);
};

export default memo( Render );
