/**
 * @license Copyright (c) 2017-2025, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md.
 */

'use strict';

const { randomId, regenerateRandomIds } = require( '../random-id' );
const renderPugComponent = require( './render-pug-component' );

/**
 * Creates a template function for a Pug component with a built-in caching mechanism.
 *
 * This function is useful for optimizing the rendering of components that are used multiple times
 * with minor changes in attributes. It divides attributes into three categories: `static`, `dynamic`, and `mask`,
 * to efficiently manage caching.
 *
 * - `static`: Attributes that are the same for all instances of the component.
 *   They are passed to the Pug component during the first render.
 *
 * - `dynamic`: Attributes whose values can change and affect the component's appearance or structure.
 *   A cache key is generated based on the combination of these attribute values.
 *   A new version of the component is rendered and cached for each unique combination of these attribute values.
 *
 * - `mask`: Attributes whose values are unique for each call and should not be part of the cached HTML.
 *   Instead, unique identifiers (masks) are inserted into the cached template, which are replaced
 *   with the actual values after retrieving the template from the cache. This is ideal for content
 *   such as code in a code block.
 *
 * @example
 * const createPrerenderPugTemplate = require( './create-prerender-pug-template' );
 *
 * // Creates a template for the 'code-block' component.
 * const codeBlockTemplate = createPrerenderPugTemplate( {
 *   // Attributes for `renderPugComponent`.
 *   includes: [ '_components/code-block/index' ],
 *   component: 'code-block',
 *
 *   // Attributes for the component itself.
 *   componentAttrs: {
 *     // Static attributes are the same for every render.
 *     static: {
 *       copyable: true
 *     },
 *     // Dynamic attributes are used for caching.
 *     // A new version of the component will be rendered and cached
 *     // for each unique combination of these attribute values.
 *     dynamic: [ 'language' ],
 *     // Masked attributes are replaced after rendering (even from the cache).
 *     // They are used for unique content that should not be cached.
 *     mask: [ 'code' ]
 *   }
 * } );
 *
 * // Using the template.
 * // The first call will render the component for the 'javascript' language and cache it.
 * const html1 = codeBlockTemplate( { language: 'javascript', code: 'console.log( "Hello" );' } );
 *
 * // This call will use the cached version for 'javascript'.
 * // Only 'code' will be replaced.
 * const html2 = codeBlockTemplate( { language: 'javascript', code: 'console.log( "World" );' } );
 *
 * // This will render a new version for 'html' and cache it.
 * const html3 = codeBlockTemplate( { language: 'html', code: '<h1>Hello</h1>' } );
 */
module.exports = function createPrerenderPugTemplate( { componentAttrs, skipCacheIf, ...renderAttrs } ) {
	const cacheStorage = new Map();
	const maskAttrsMap = assignMaskAttrsIds( componentAttrs.mask || [] );

	const allKnownAttributes = [
		...componentAttrs.dynamic || [],
		...Object.keys( componentAttrs.static || {} ),
		...componentAttrs.mask || []
	];

	return dynamicAttrs => {
		// Check if we should skip caching based on the provided condition.
		const skipCache = skipCacheIf?.( dynamicAttrs );

		if ( skipCache ) {
			return renderPugComponent( {
				...renderAttrs,
				componentAttrs: {
					...componentAttrs.static,
					...dynamicAttrs
				}
			} );
		}

		// Check if component is already pre-rendered, if so, return cached HTML.
		const _hasUnknownAttributes = hasUnknownAttributes( allKnownAttributes, dynamicAttrs );
		const cacheKey = createCacheKey( componentAttrs.dynamic, dynamicAttrs );

		let renderedHtml;

		if ( !_hasUnknownAttributes && cacheStorage.has( cacheKey ) ) {
			renderedHtml = cacheStorage.get( cacheKey );
		} else {
			renderedHtml = renderPugComponent( {
				...renderAttrs,
				componentAttrs: {
					...componentAttrs.static,
					...dynamicAttrs,
					...maskAttrsMap
				}
			} );

			if ( !_hasUnknownAttributes ) {
				cacheStorage.set( cacheKey, renderedHtml );
			}
		}

		// Regenerate random IDs in the rendered HTML to ensure uniqueness.
		renderedHtml = regenerateRandomIds( renderedHtml );

		for ( const maskKey in maskAttrsMap ) {
			const dynamicValue = dynamicAttrs[ maskKey ] || '';

			renderedHtml = renderedHtml.replace(
				maskAttrsMap[ maskKey ],
				dynamicValue
			);
		}

		return renderedHtml;
	};
};

/**
 * Checks if the provided component attributes contain any unknown attributes.
 */
function hasUnknownAttributes( knownAttributes, componentAttrs ) {
	return Object.keys( componentAttrs ).some( attr => !knownAttributes.includes( attr ) );
}

/**
 * Creates a cache key based on the specified attributes and their values.
 *
 * @param cacheByAttributes An array of attribute names to be included in the cache key.
 * @param componentAttrs An object containing component attributes and their values.
 * @returns The generated cache key.
 */
function createCacheKey( cacheByAttributes, componentAttrs ) {
	if ( !cacheByAttributes?.length ) {
		return 'default';
	}

	let acc = '';

	for ( const attr of cacheByAttributes ) {
		acc += `${ attr }-${ componentAttrs[ attr ] || 'undefined' }-`;
	}

	return acc;
}

/**
 * Creates an object that maps mask attribute names to unique random IDs.
 *
 * @param maskAttributes An array of attribute names that should be masked.
 * @returns An object where keys are attribute names and values are unique mask IDs.
 */
function assignMaskAttrsIds( maskAttributes ) {
	return maskAttributes.reduce(
		( acc, attr ) => {
			acc[ attr ] = randomId( 'mask', false );

			return acc;
		},
		Object.create( {} )
	);
}
