/**
 * External dependencies.
 */
import { select, resolveSelect } from '@wordpress/data';
// eslint-disable-next-line import/named
import { dateI18n, humanTimeDiff } from '@wordpress/date';
import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies.
 */
import API from '@spectra-pro-helpers/api';
import {
	IMAGE_SUPPORTED_BLOCKS,
	LINK_SUPPORTED_BLOCKS,
	SOURCE_FIELDS,
	SUPPORTED_BLOCKS,
} from '@spectra-pro-extensions/dynamic-content/utils/constants';
import urls from '@spectra-pro-constants/urls';

/**
 * Checks if a block supports dynamic text content.
 *
 * @since x.x.x
 *
 * @param {Object} block - The block object to check.
 * @return {boolean} True if the block is supported for dynamic text content, false otherwise.
 */
export const isAllowedBlock = ( block ) => {
	// Verify if block exists and its name is in the supported blocks list.
	return block?.name ? SUPPORTED_BLOCKS.includes( block.name ) : false;
};

/**
 * Checks if a block supports dynamic image content.
 *
 * @since x.x.x
 *
 * @param {Object} block - The block object to check.
 * @return {boolean} True if the block is supported for dynamic image content, false otherwise.
 */
export const isAllowedBlockForImage = ( block ) => {
	// Verify if block exists and its name is in the supported image blocks list.
	return block?.name ? IMAGE_SUPPORTED_BLOCKS.includes( block.name ) : false;
};

/**
 * Checks if a block supports dynamic link content.
 *
 * @since x.x.x
 *
 * @param {Object} block - The block object to check.
 * @return {boolean} True if the block is supported for dynamic link content, false otherwise.
 */
export const isAllowedBlockForLink = ( block ) => {
	// Verify if block exists and its name is in the supported link blocks list.
	return block?.name ? LINK_SUPPORTED_BLOCKS.includes( block.name ) : false;
};

/**
 * Fetches dynamic content based on the provided settings and content type.
 *
 * @since x.x.x
 *
 * @param {Object} settings      - The dynamic content settings, including source and field.
 * @param {string} [type='text'] - The type of dynamic content ('text', 'image', 'link', etc.).
 * @return {Promise<string|Object>} The fetched dynamic content or fallback value.
 */
export const getDynamicContent = async ( settings, type = 'text' ) => {
	const {
		source: { type: dataSource, field: sourceField, postId = 0, postType = 'post' },
		advanced = {},
	} = settings;

	let content = '';

	try {
		switch ( dataSource ) {
			case 'current_post': {
				let post = null;

				// If a post ID is provided, fetch the post by ID.
				if ( postId ) {
					// Try to get the post type from the current editor context if not provided.
					const currentPostType =
						settings.source.postType || select( 'core/editor' )?.getCurrentPostType() || 'post';
					post = await resolveSelect( 'core' ).getEntityRecord( 'postType', currentPostType, postId );
				}

				// If no post ID is provided, retrieve the current post from the editor.
				if ( ! post ) {
					// Retrieve the current post from the editor.
					post = select( 'core/editor' ).getCurrentPost();

					// Throw an error if the current post is not available.
					if ( ! post ) throw new Error( __( 'Current post not available', 'spectra-pro' ) );
				}

				content = await getPostFieldValue( settings, post, type );
				break;
			}
			case 'post_type': {
				// Fetch a specific post by ID.
				if ( ! postId ) throw new Error( __( 'Post ID is required', 'spectra-pro' ) );

				const post = await resolveSelect( 'core' ).getEntityRecord( 'postType', postType, postId );

				if ( ! post ) throw new Error( __( 'Post not found.', 'spectra-pro' ) );

				content = await getPostFieldValue( settings, post, type );
				break;
			}
			case 'site': {
				// Fetch site-related dynamic content.
				content = await getSiteFieldValue( settings );
				break;
			}
			case 'current_user': {
				// Fetch user-related dynamic content.
				content = await getUserFieldValue( settings );
				break;
			}
			case 'archive': {
				// Fetch archive-related dynamic content.
				content = await getArchiveFieldValue( settings );
				break;
			}
			case 'request_parameter': {
				// Fetch request parameter-based dynamic content.
				content = await getRequestParamValue( settings );
				break;
			}
			case 'shortcode': {
				// Use the source field as the shortcode content.
				content = sourceField;
				break;
			}
			default:
				content = '';
		}

		if ( 'image' === type ) {
			//  empty or  empty object or empty array
			if ( ! content || ( typeof content === 'object' && Object.keys( content ).length === 0 ) ) {
				return advanced?.fallback || '';
			}

			return content;
		}

		// Format the content before returning.
		return formatDynamicContent( content, settings, type );
	} catch ( error ) {
		// Return fallback value or default 'N/A' on error.
		return advanced?.fallback || __( 'N/A', 'spectra-pro' );
	}
};

/**
 * Retrieves the value for a specific post field based on settings.
 *
 * @since x.x.x
 *
 * @param {Object} settings      - The dynamic content settings.
 * @param {Object} [post=null]   - The post object to extract the field from.
 * @param {string} [type='text'] - The type of content ('text', 'image', etc.).
 * @return {Promise<string|Object>} The field value or empty string if unavailable.
 */
export const getPostFieldValue = async ( settings, post = null, type = 'text' ) => {
	if ( ! post || ( 'post_type' === settings.source.type && ! post.id ) ) {
		return '';
	}

	const { field = 'post_title' } = settings.source;

	const authorFields = [
		'name',
		'first_name',
		'last_name',
		'display_name',
		'login',
		'description',
		'nicename',
		'description',
		'email',
		'ID',
		'url',
		'avatar',
		'author_info',
	];

	// Redirect to user field value if the field is an author-related field.
	if ( authorFields.includes( field ) ) {
		return getUserFieldValue( { ...settings, source: { ...settings.source, postId: post.id } } );
	}

	switch ( field ) {
		case 'post_title':
			return post.title?.rendered || post.title || '';

		case 'post_excerpt':
			// Get the excerpt - prefer raw over rendered to avoid double HTML encoding.
			const excerpt = post.excerpt?.raw || post.excerpt?.rendered || post.excerpt || '';
			// If we got the rendered version, we need to strip the HTML tags.
			if ( post.excerpt?.rendered && ! post.excerpt?.raw ) {
				// Create a temporary element to parse the HTML and extract text content.
				const temp = document.createElement( 'div' );
				temp.innerHTML = excerpt;
				return decodeEntities( temp.textContent || temp.innerText || '' );
			}
			return excerpt;

		case 'featured_image':
			return getFeaturedImageField( post, settings, type );

		case 'custom_field':
			return await getCustomField( post, settings, type );

		case 'post_date':
			return getPostDate( post, settings );

		case 'post_time':
			return getPostTime( post, settings );

		case 'post_terms':
			return getPostTerms( post, settings );

		case 'post_ID':
			return post.id.toString();

		case 'comments_number':
			return getCommentsNumber( post, settings );

		default:
			return '';
	}
};

/**
 * Retrieves the value for a featured image field.
 *
 * @since x.x.x
 *
 * @param {Object} post          - The post object containing the featured media.
 * @param {Object} settings      - The dynamic content settings.
 * @param {string} [type='text'] - The type of content ('text', 'image', etc.).
 * @return {Promise<string|Object>} The featured image field value or empty string.
 */
const getFeaturedImageField = async ( post, settings, type = 'text' ) => {
	const attachmentId = post?.featured_media;

	if ( ! attachmentId ) return '';

	// Attempt to fetch attachment synchronously.
	let attachment = select( 'core' )?.getMedia( attachmentId );

	// If not available synchronously, fetch asynchronously
	if ( ! attachment ) {
		try {
			attachment = await resolveSelect( 'core' ).getEntityRecord( 'postType', 'attachment', attachmentId );
		} catch ( error ) {
			return '';
		}
	}

	if ( ! attachment ) return '';

	// Return image object for image type.
	if ( 'image' === type ) {
		return {
			url: attachment.source_url,
			id: attachment.id,
		};
	}

	// Return specific metadata field for text type.
	switch ( settings.source?.featuredImageField ) {
		case 'title':
			return attachment.title?.raw || '';
		case 'alt_text':
			return attachment.alt_text || '';
		case 'caption':
			return attachment.caption?.raw || '';
		case 'description':
			return attachment.description?.raw || '';
		case 'link':
			return attachment.media_link || '';
		case 'source_url':
			return attachment.source_url || '';
		default:
			return attachment.source_url || '';
	}
};

/**
 * Retrieves a custom field value for a post.
 *
 * @since x.x.x
 *
 * @param {Object} post          - The post object.
 * @param {Object} settings      - The dynamic content settings.
 * @param {string} [type='text'] - The type of content.
 * @return {Promise<string>} The custom field value or empty string.
 */
const getCustomField = async ( post, settings, type = 'text' ) => {
	if ( ! post || ! post.id ) {
		return '';
	}

	const { postCustomField = '' } = settings.source;

	if ( ! postCustomField ) return '';

	// Update settings with post ID for API call.
	settings.source.postId = post.id;
	// Preserve post type if it exists.
	if ( post.type ) {
		settings.source.postType = post.type;
	}

	return await fetchDynamicContent( settings, type );
};

/**
 * Formats and retrieves the post date.
 *
 * @since x.x.x
 *
 * @param {Object} post     - The post object.
 * @param {Object} settings - The dynamic content settings.
 * @return {string} The formatted date string.
 */
const getPostDate = ( post, settings ) => {
	const { dateType, dateFormat, customDateFormat } = settings.source;
	const date = dateType === 'modified' ? post.modified_gmt : post.date_gmt;

	if ( ! date ) return '';

	return formatDate( date, dateFormat, customDateFormat );
};

/**
 * Formats and retrieves the post time.
 *
 * @since x.x.x
 *
 * @param {Object} post     - The post object.
 * @param {Object} settings - The dynamic content settings.
 * @return {string} The formatted time string.
 */
const getPostTime = ( post, settings ) => {
	const { timeType, timeFormat, customTimeFormat } = settings.source;
	const date = timeType === 'modified' ? post.modified_gmt : post.date_gmt;

	if ( ! date ) return '';

	return formatTime( date, timeFormat, customTimeFormat );
};

/**
 * Retrieves and formats post terms for a given taxonomy.
 *
 * @since x.x.x
 *
 * @param {Object} post     - The post object.
 * @param {Object} settings - The dynamic content settings.
 * @return {Promise<string>} The formatted terms string.
 */
const getPostTerms = async ( post, settings ) => {
	const { taxonomyType = 'category', taxonomyValueSeparator = ',' } = settings.source;

	if ( ! taxonomyType || ! post?.id ) return '';

	// Attempt to fetch terms synchronously.
	let terms = select( 'core' )?.getEntityRecords( 'taxonomy', taxonomyType, { post: post.id } );

	// Fetch asynchronously if not available.
	if ( ! terms ) {
		try {
			terms = await resolveSelect( 'core' ).getEntityRecords( 'taxonomy', taxonomyType, { post: post.id } );
		} catch ( error ) {
			return '';
		}
	}

	if ( ! terms ) return '';

	// Join term names with the specified separator.
	return terms.map( ( term ) => term.name ).join( taxonomyValueSeparator ) || '';
};

/**
 * Retrieves and formats the number of comments for a post.
 *
 * @since x.x.x
 *
 * @param {Object} post     - The post object.
 * @param {Object} settings - The dynamic content settings.
 * @return {Promise<string>} The formatted comments count string.
 */
const getCommentsNumber = async ( post, settings ) => {
	if ( ! post || ! post.id ) {
		return '';
	}

	let commentCount = 0;

	// Fetch comment count via API.
	const commentsCountAPI = new API( urls.commentsCount );
	const response = await commentsCountAPI.list( { post_id: post.id } );

	if ( response?.data ) {
		commentCount = parseInt( response.data, 10 );
	}

	const count = commentCount || 0;
	const {
		noComments = __( 'No Responses', 'spectra-pro' ),
		oneComment = __( 'One Response', 'spectra-pro' ),
		manyComments = __( '{number} Responses', 'spectra-pro' ),
	} = settings.source;

	// Return appropriate string based on comment count.
	if ( count === 0 ) return noComments;
	if ( count === 1 ) return oneComment;
	return manyComments.replace( '{number}', count );
};

/**
 * Formats a date string according to the specified format.
 *
 * @since x.x.x
 *
 * @param {string} dateStr        - The date string to format.
 * @param {string} format         - The date format to apply.
 * @param {string} [customFormat] - Custom format, if provided.
 * @return {string} The formatted date string.
 */
const formatDate = ( dateStr, format, customFormat ) => {
	const formatMap = {
		// eslint-disable-next-line quote-props
		default: 'F j, Y', // Default: "April 6, 2025"
		'F j, Y': 'F j, Y', // "April 6, 2025"
		'Y-m-d': 'Y-m-d', // "2025-04-06"
		'm/d/Y': 'm/d/Y', // "04/06/2025"
		'd/m/Y': 'd/m/Y', // "06/04/2025"
		// eslint-disable-next-line quote-props
		human_readable: '', // Handled by humanTimeDiff
		// eslint-disable-next-line quote-props
		custom: customFormat || 'F j, Y', // Use custom format or fallback
	};

	if ( format === 'human_readable' ) {
		return humanTimeDiff( dateStr );
	}

	const formatString = formatMap[ format ] || formatMap.default;
	return dateI18n( formatString, dateStr );
};

/**
 * Formats a time string according to the specified format.
 *
 * @since x.x.x
 *
 * @param {string} dateStr        - The date string to format.
 * @param {string} format         - The time format to apply.
 * @param {string} [customFormat] - Custom format, if provided.
 * @return {string} The formatted time string.
 */
const formatTime = ( dateStr, format, customFormat ) => {
	const formatMap = {
		// eslint-disable-next-line quote-props
		default: 'g:i a', // "2:30 pm"
		'g:i a': 'g:i a', // "2:30 pm"
		'g:i A': 'g:i A', // "2:30 PM"
		'H:i': 'H:i', // "14:30"
		// eslint-disable-next-line quote-props
		custom: customFormat || 'g:i a', // Use custom format or fallback to "2:30 pm"
	};

	const formatString = formatMap[ format ] || formatMap.default;
	return dateI18n( formatString, dateStr );
};

/**
 * Fetches archive field value via API.
 *
 * @since x.x.x
 *
 * @param {Object} settings - The dynamic content settings.
 * @return {Promise<string>} The archive field value or fallback.
 */
const getArchiveFieldValue = async ( settings ) => {
	return await fetchDynamicContent( settings );
};

/**
 * Fetches site field value via API.
 *
 * @since x.x.x
 *
 * @param {Object} settings - The dynamic content settings.
 * @return {Promise<string>} The site field value or empty string.
 */
const getSiteFieldValue = async ( settings ) => {
	if ( ! settings.source?.field ) return '';

	return await fetchDynamicContent( settings );
};

/**
 * Fetches user field value via API.
 *
 * @since x.x.x
 *
 * @param {Object} settings - The dynamic content settings.
 * @return {Promise<string>} The user field value or empty string.
 */
const getUserFieldValue = async ( settings ) => {
	if ( ! settings.source?.field ) return '';

	return await fetchDynamicContent( settings );
};

/**
 * Fetches request parameter value via API.
 *
 * @since x.x.x
 *
 * @param {Object} settings - The dynamic content settings.
 * @return {Promise<string>} The request parameter value or empty string.
 */
const getRequestParamValue = async ( settings ) => {
	if ( ! settings.source?.requestParamKey ) return '';

	return await fetchDynamicContent( settings );
};

/**
 * Formats dynamic content by applying advanced settings (e.g., truncation, prefixes).
 *
 * @since x.x.x
 *
 * @param {string|Object|Promise} content  - The content to format.
 * @param {Object}                settings - The dynamic content settings.
 * @return {Promise<string>} The formatted content or fallback value.
 */
export const formatDynamicContent = async ( content, settings ) => {
	const { advanced } = settings;

	try {
		// Ensure content is resolved if it's a Promise.
		content = await content;

		// Use fallback if content is empty and fallback is provided.
		if ( ( typeof content !== 'string' || ! content.trim() ) && advanced?.fallback )
			content = advanced?.fallback || '';

		if ( ! content.length > 0 ) return __( 'N/A', 'spectra-pro' );

		// Truncate content if character length is specified.
		if ( advanced?.charLength && content.length > parseInt( advanced?.charLength ) ) {
			content = content.substring( 0, parseInt( advanced?.charLength ) ) + '…';
		}

		// Add before/after text if specified.
		if ( advanced?.before ) content = advanced?.before + content;
		if ( advanced?.after ) content += advanced?.after;

		return content;
	} catch ( error ) {
		// Return fallback or default 'N/A' on error.
		return advanced?.fallback || __( 'N/A', 'spectra-pro' );
	}
};

/**
 * Retrieves available fields for a given data source.
 *
 * @since x.x.x
 *
 * @param {string} dataSource - The data source identifier.
 * @return {Object[]} The fields available for the data source.
 */
export const getSourceFields = ( dataSource ) => SOURCE_FIELDS[ dataSource ] || [];

/**
 * Gets link field options for a given data source.
 *
 * @since x.x.x
 *
 * @param {string} dataSource - The data source type.
 * @return {Array} Array of link field options.
 */
export const getLinkFieldOptions = ( dataSource ) => {
	const linkFieldOptions = {
		current_post: [
			{ label: 'Post Permalink', value: 'post_permalink' },
			{ label: 'Comments Area', value: 'comments_area' },
			{ label: 'Featured Image URL', value: 'featured_image' },
			{ label: 'Post Custom Field', value: 'custom_field' },
			{ label: 'Author Archive', value: 'author_archive' },
			{ label: 'Author Website', value: 'author_website' },
			{ label: 'First Term Archive', value: 'post_terms' },
		],
		post_type: [
			{ label: 'Post Permalink', value: 'post_permalink' },
			{ label: 'Comments Area', value: 'comments_area' },
			{ label: 'Featured Image URL', value: 'featured_image' },
			{ label: 'Post Custom Field', value: 'custom_field' },
			{ label: 'Author Archive', value: 'author_archive' },
			{ label: 'Author Website', value: 'author_website' },
			{ label: 'First Term Archive', value: 'post_terms' },
		],
		site: [
			{ label: 'Site URL', value: 'site_url' },
			{ label: 'Home URL', value: 'home_url' },
		],
		archive: [
			{ label: 'Archive URL', value: 'archive_url' },
			{ label: 'Archive Custom Field', value: 'archive_meta' },
		],
		current_user: [
			{ label: 'User Profile URL', value: 'user_profile' },
			{ label: 'User Website URL', value: 'user_website' },
			{ label: 'User Custom Field', value: 'custom_field' },
		],
		request_parameter: [
			{ label: 'Parameter Value as URL', value: 'parameter_url' },
		],
		shortcode: [
			{ label: 'Shortcode Output as URL', value: 'shortcode_url' },
		],
	};
	
	return linkFieldOptions[ dataSource ] || [];
};

/**
 * Fetches dynamic content from the API.
 *
 * @since x.x.x
 *
 * @param {Object} settings      - The dynamic content settings.
 * @param {string} [type='text'] - The type of content to fetch.
 * @return {Promise<string>} The fetched content or fallback value.
 */
export const fetchDynamicContent = async ( settings, type = 'text' ) => {
	const DynamicContentAPI = new API( urls.dynamicContent );

	try {
		// Fetch content from the dynamic content API.
		const response = await DynamicContentAPI.list( { settings: JSON.stringify( settings ), type } );

		if ( response && response?.data ) {
			return response.data;
		}

		// Return fallback or default 'N/A' if no data is returned.
		return settings?.advanced?.fallback || __( 'N/A', 'spectra-pro' );
	} catch ( e ) {
		// Return fallback or default 'N/A' on error.
		return settings?.advanced?.fallback || __( 'N/A', 'spectra-pro' );
	}
};
