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

'use strict';

const transformXMLTreeToPug = require( './transform-xml-tree-to-pug.js' );
const XMLComponentsParser = require( './parser/xml-components-parser.js' );
const { walkXMLTree, TreeWalkActions } = require( './parser/walk-xml-tree.js' );

/**
 * Slices XML components from Markdown content and transforms them to Pug format.
 *
 * This function takes text containing XML-like components mixed with regular content,
 * parses it into a tree structure, and then walks through the tree to transform
 * XML elements into Pug mixin calls while preserving text content.
 *
 * @param xml - The input text containing XML components and regular content
 * @param options - Configuration options for parsing and transformation
 * @param options.hexo - The Hexo instance for context (required)
 * @param options.sourceName - The name of the source file (for error reporting)
 * @param options.mixinContextVars - Additional attributes to pass to the Pug mixin
 * @param [options.parserConfig={}] - Configuration passed to XMLComponentsParser
 * @param [options.pugTransformerConfig={}] - Configuration passed to transformXMLTreeToPug
 * @returns The transformed content with XML components converted to Pug syntax
 */
function renderXMLPugComponentsInMarkdown( xml, options ) {
	const {
		sourceName,
		mixinContextVars,
		hexo,
		basePath,
		patternElements = [],
		replaceFn = createReplacementBase64Tag
	} = options;

	const parser = XMLComponentsParser.parseText( xml, {
		trimRootText: false,
		patternElements
	} );

	const markdown = walkXMLTree( parser.tree, {
		enter( node, depth ) {
			switch ( node.type ) {
				case 'text':
					return TreeWalkActions.replace( node.content );

				case 'element': {
					const { patternElement } = node;

					if ( patternElement.renderer ) {
						return TreeWalkActions.replace(
							patternElement.renderer( node )
						);
					}

					// Hexo rendering engine is a little bit clumsy and rendered markdown within
					// pug mixins often results in broken HTML. It's easier to do it manually, after
					// the whole tree is rendered.
					const postfixTexts = [];
					const content = transformXMLTreeToPug( node, depth, {
						fallbackGlobalRequire: '_components/index',
						parseText: ( node, str ) => {
							if ( !str.trim().length ) {
								return '';
							}

							// Handle the text inside the pug.
							if ( !node.patternElement?.allowMarkdownContent ) {
								return str;
							}

							// If the content is a markdown text, we'll postprocess it after rendering whole tree.
							postfixTexts.push( str );
							return `__MARKDOWN_TEXT_${ postfixTexts.length - 1 }__`;
						}
					} );

					// Let's render the mixin.
					const locals = Object.assign(
						{},
						hexo.locals.toObject(),
						hexo.extend.helper.list(),
						{
							...mixinContextVars,
							config: hexo.config,
							mixinContextVars: patternElement.mixinContextVars
						}
					);

					// Render component wrapper
					let finalReplacement = null;

					try {
						finalReplacement = hexo.render.renderSync(
							{
								engine: 'pug',
								text: content,
								path: basePath
							},
							locals
						).trim();
					} catch ( error ) {
						console.error( error );
						console.error( `Error rendering component "${ node.name }" in file ${ sourceName }` );
						console.error( content );

						throw error;
					}

					// Render markdown content inside the mixin, if present.
					for ( let i = 0; i < postfixTexts.length; i++ ) {
						const renderedInnerText = hexo.render.renderSync(
							{
								text: postfixTexts[ i ],
								engine: 'markdown'
							}
						).trim();

						finalReplacement = finalReplacement.replace(
							`__MARKDOWN_TEXT_${ i }__`,
							renderedInnerText
						);
					}

					// Leave magic base64 tag that will be replaced AFTER processing the whole markdown document.
					return TreeWalkActions.replace(
						replaceFn( finalReplacement )
					);
				}
			}
		}
	} ).join( '' );

	return markdown;
}

/**
 * Creates a Base64-encoded replacement tag for component content.
 *
 * This function encodes the provided content into a Base64 string and wraps it
 * in an HTML comment marker that can be later extracted and replaced with the
 * original content.
 *
 * @param content - The HTML/text content to be encoded and wrapped
 * @returns A comment tag containing the Base64-encoded content
 */
function createReplacementBase64Tag( content ) {
	const encodedContent = Buffer.from( content ).toString( 'base64' );

	return `<!-- COMPONENT:${ encodedContent } -->`;
}

/**
 * Extracts and decodes Base64-encoded component markers from page content.
 *
 * This function finds HTML comment markers containing Base64-encoded content,
 * decodes them back to their original form, and replaces the markers with the
 * decoded content. It processes the content iteratively to handle nested
 * components and includes a maximum iteration limit to prevent infinite loops.
 *
 * @param options - Configuration object
 * @param options.page - The page object containing the content to process
 * @param options.maxIterations - Maximum number of processing iterations to prevent infinite loops
 * @returns The processed content with all component markers replaced
 */
function extractReplacementBase64Tag( { page, maxIterations = 5 } ) {
	let content = page.content;
	let previousContent;

	const markerRegex = /<!-- COMPONENT:(.*?) -->/g;

	do {
		previousContent = content;
		content = content.replace(
			/<!-- COMPONENT:([a-zA-Z0-9+/=]+) -->/g,
			( match, encodedContent ) => {
				try {
					return Buffer.from( encodedContent, 'base64' ).toString();
				} catch ( error ) {
					console.error( `Error decoding component marker in file ${ page.source }`, error );
					return match;
				}
			}
		);
	} while ( content !== previousContent && markerRegex.test( content ) && maxIterations-- > 0 );

	return content;
}

exports.renderXMLPugComponentsInMarkdown = renderXMLPugComponentsInMarkdown;
exports.extractReplacementBase64Tag = extractReplacementBase64Tag;
