<?php
/**
 * HtmlBlock Component file.
 *
 * @package Etch
 */

declare(strict_types=1);

namespace Etch\Preprocessor\Blocks;

use Etch\Helpers\SvgLoader;
use Etch\Preprocessor\Data\EtchData;
use Etch\Preprocessor\Utilities\EtchParser;
use Etch\Preprocessor\Utilities\EtchTypeAsserter;
use Etch\Preprocessor\Registry\StylesRegister;

/**
 * HtmlBlock class for processing HTML blocks with EtchData.
 */
class HtmlBlock extends BaseBlock {

	/**
	 * Constructor for the HtmlBlock class.
	 *
	 * @param WpBlock                   $block WordPress block data.
	 * @param EtchData                  $data Etch data instance.
	 * @param array<string, mixed>|null $context Parent context to inherit.
	 * @param BaseBlock|null            $parent The parent block.
	 */
	public function __construct( WpBlock $block, EtchData $data, $context = null, $parent = null ) {
		parent::__construct( $block, $data, $context, $parent );

		// Fix for SVG tags: remove xmlns attribute if tag is svg
		if ( null !== $this->etch_data && 'svg' === $this->etch_data->tag && isset( $this->etch_data->attributes['xmlns'] ) ) {
			unset( $this->etch_data->attributes['xmlns'] );
		}
	}

	/**
	 * Get resolved attributes as a string.
	 *
	 * @return array{0: string, 1: bool} Resolved attributes string and detection flag.
	 */
	public function get_resolved_attributes_string(): array {
		$attributes = $this->etch_data->attributes ?? array();

		if ( empty( $attributes ) || ! is_array( $attributes ) ) {
			return array( '', false );
		}

		$has_templates = false;
		$attributes_string = '';
		foreach ( $attributes as $key => $value ) {
			// Convert value to string
			[$string_value, $templates_replaced] = EtchParser::replace_string( $value, $this->get_context(), true );

			if ( $templates_replaced ) {
				$has_templates = true;
			}

			// For empty values, output just the attribute name
			if ( empty( $string_value ) ) {
				$attributes_string .= ' ' . esc_attr( $key );
			} else {
				$attributes_string .= ' ' . esc_attr( $key ) . '="' . esc_attr( $string_value ) . '"';
			}
		}

		return array( $attributes_string, $has_templates );
	}

	/**
	 * Process the HTML block and return the transformed block data.
	 *
	 * @return array<string, mixed>|array<int, array<string, mixed>> Transformed block data, or array of blocks for etch-content.
	 */
	public function process(): array {

		$etchData = $this->get_etch_data();

		if ( null === $etchData ) {
			return $this->get_raw_block();
		}

		$tag = $etchData->tag;
		[$parsed_attributes, $has_dynamic_templates] = $this->get_resolved_attributes_string();

		// Register dynamic styles based on parsed attributes if templates were found
		if ( $has_dynamic_templates ) {
			$this->register_dynamic_styles( $parsed_attributes );
		}

		if ( $this->is_etch_svg() ) {
			$this->load_and_set_svg_content( $parsed_attributes );
		} else if ( $this->has_multiple_inner_content() ) {
			$this->wrap_inner_content( $tag, $parsed_attributes );
		} else {
			$processed_content = $this->process_content_block(
				$this->get_inner_content(),
				$etchData,
				$this->get_block_name(),
				$parsed_attributes
			);
			$this->set_inner_content( $processed_content );
		}

		$this->set_block_name( 'etch/block' );

		// Process inner blocks recursively if they exist
		$inner_blocks = $this->get_inner_blocks();
		if ( ! empty( $inner_blocks ) ) {
			$processed_inner_blocks_raw_blocks = $this->process_inner_blocks_to_raw_blocks( $inner_blocks );

			// Set the processed inner blocks directly as arrays in the current block
			$this->set_inner_blocks_from_raw_blocks( $processed_inner_blocks_raw_blocks );

			// We need this for loops/components to work properly
			$this->update_inner_content_slots_count();
		}

		return $this->get_raw_block();
	}

	/**
	 * Register dynamic styles based on parsed attributes.
	 * This method finds styles that match the parsed attribute values and registers them.
	 *
	 * @param string $parsed_attributes The parsed HTML attributes string.
	 * @return void
	 */
	private function register_dynamic_styles( string $parsed_attributes ): void {
		if ( empty( $parsed_attributes ) ) {
			return;
		}

		// Find styles that match the parsed attributes
		$matching_style_ids = StylesRegister::find_matching_styles( $parsed_attributes );

		// Register the matching styles
		if ( ! empty( $matching_style_ids ) ) {
			StylesRegister::register_styles( $matching_style_ids );
		}
	}

	/**
	 * Check if block has multiple inner content items.
	 *
	 * @return bool True if has multiple inner content items.
	 */
	private function has_multiple_inner_content(): bool {
		return count( $this->get_inner_content() ) > 1;
	}

	/**
	 * Wrap inner content with tag and attributes.
	 *
	 * @param string $tag HTML tag.
	 * @param string $mapped_attributes Mapped attributes string.
	 * @return void
	 */
	private function wrap_inner_content( string $tag, string $mapped_attributes ): void {
		$inner_content = $this->get_inner_content();

		if ( ! is_array( $inner_content ) ) {
			return;
		}

		// Parse dynamic placeholders in all inner content items
		$parsed_inner_content = array();
		foreach ( $inner_content as $content_item ) {
			if ( is_string( $content_item ) ) {
				$parsed_result = EtchParser::replace_string( $content_item, $this->get_context() );
				$parsed_inner_content[] = EtchTypeAsserter::parse_result_to_string_or_null( $parsed_result );
			} else {
				// Ensure content_item is string|null as expected by set_inner_content
				$parsed_inner_content[] = EtchTypeAsserter::to_string_or_null( $content_item );
			}
		}

		$inner_content_count = count( $parsed_inner_content );
		if ( $inner_content_count > 0 ) {
			$parsed_inner_content[0] = '<' . $tag . $mapped_attributes . '>';
			$parsed_inner_content[ $inner_content_count - 1 ] = '</' . $tag . '>';
			$this->set_inner_content( $parsed_inner_content );
		}
	}

	/**
	 * Processes a content block.
	 *
	 * @param list<?string> $innerContent The inner content of the block.
	 * @param EtchData      $etchData The etch data.
	 * @param string        $type The block type.
	 * @param string        $parsed_attributes The pre-parsed attributes string.
	 * @return list<string> The processed content.
	 */
	private function process_content_block( $innerContent, $etchData, $type, $parsed_attributes ) {
		$tag = $etchData->tag;

		$content_string = $this->extract_content_string( $innerContent );
		$inner_html = $this->extract_inner_html( $content_string );

		$skip_replacement = false;
		// Skip replacement for style tags to avoid breaking CSS
		if ( isset( $this->parent ) ) {
			$etch_data = $this->parent->etch_data;
			$skip_replacement = null !== $etch_data && 'style' === $etch_data->tag;
		}

		if ( $skip_replacement ) {
			$parsed_inner_html = $inner_html;
		} else {
			// Parse dynamic placeholders in inner HTML using current context
			$parsed_inner_html = EtchParser::replace_string( $inner_html, $this->get_context() );
		}

		$new_content_string = $this->build_content_string(
			$etchData,
			$tag,
			$parsed_attributes,
			EtchTypeAsserter::parse_result_to_string( $parsed_inner_html )
		);

		if ( $etchData->has_nested_data() ) {
			$new_content_string = $this->process_nested_data(
				$new_content_string,
				$etchData
			);
		}

		return array( $new_content_string );
	}

	/**
	 * Extract content string from inner content array.
	 *
	 * @param list<?string> $innerContent Inner content array.
	 * @return string Content string.
	 */
	private function extract_content_string( $innerContent ) {
		return $innerContent[0] ?? '';
	}

	/**
	 * Extract inner HTML from content string.
	 *
	 * @param string $content_string Content string.
	 * @return string Inner HTML.
	 */
	private function extract_inner_html( string $content_string ): string {

		if ( false === strpos( $content_string, '>' ) ) {
			return $content_string;
		}

		$start = strpos( $content_string, '>' ) + 1;
		$end = strrpos( $content_string, '<' );

		$content_string = substr( $content_string, $start, $end - $start );

		return $content_string;
	}

	/**
	 * Build content string based on etch data.
	 *
	 * @param EtchData $etchData Etch data.
	 * @param string   $tag HTML tag.
	 * @param string   $mapped_attributes Mapped attributes.
	 * @param string   $inner_html Inner HTML.
	 * @return string Built content string.
	 */
	private function build_content_string( EtchData $etchData, string $tag, string $mapped_attributes, string $inner_html ): string {
		if ( $etchData->removeWrapper ) {
			return $inner_html;
		}

		return '<' . $tag . $mapped_attributes . '>' . $inner_html . '</' . $tag . '>';
	}

	/**
	 * Process nested data for content string.
	 *
	 * @param string   $content_string Content string.
	 * @param EtchData $etchData Etch data.
	 * @return string Processed content string.
	 */
	private function process_nested_data( string $content_string, EtchData $etchData ): string {
		if ( ! $etchData->has_nested_data() ) {
			return $content_string;
		}

		foreach ( $etchData->nestedData as $id => $nestedData ) {
			if ( ! $nestedData instanceof EtchData ) {
				continue;
			}

			// Remove the 'xmlns' attribute for SVG tags to avoid issues when rendering.
			if ( 'svg' === $nestedData->tag ) {
				$nestedData->remove_attribute( 'xmlns' );
				$content_string = EtchTypeAsserter::to_string(
					preg_replace(
						'/xmlns="[^"]*"/',
						'',
						EtchTypeAsserter::to_string( $content_string )
					),
					''
				);
			}

			$mapped_attributes = $this->map_nested_attributes(
				$nestedData->attributes
			);

			if ( $this->is_tag_nested_data( $id, $content_string ) ) {
				$content_string = $this->replace_tag_nested_data( $content_string, $id, $mapped_attributes );
			} else {
				$content_string = $this->replace_ref_nested_data( $content_string, $id, $mapped_attributes );
			}
		}

		return EtchTypeAsserter::to_string( $content_string );
	}

	/**
	 * Check the given nested data id is an data-etch-ref or a tag attribute.
	 *
	 * @param string $id The ID to check.
	 * @param string $content_string The content string.
	 * @return bool True if tag, false if common data-etch-ref.
	 */
	private function is_tag_nested_data( string $id, string $content_string ): bool {
		return ! strpos( $content_string, 'data-etch-ref="' . $id . '"' );
	}

	/**
	 * Replace tag nested data in content string.
	 *
	 * @param string $content_string Content string.
	 * @param string $id Nested data ID.
	 * @param string $mapped_attributes Mapped attributes.
	 * @return string Updated content string.
	 */
	private function replace_tag_nested_data( string $content_string, string $id, string $mapped_attributes ): string {
		return str_replace( '<' . $id, '<' . $id . $mapped_attributes, $content_string );
	}

	/**
	 * Replace ref nested data in content string.
	 *
	 * @param string $content_string Content string.
	 * @param string $id Nested data ID.
	 * @param string $mapped_attributes Mapped attributes.
	 * @return string Updated content string.
	 */
	private function replace_ref_nested_data( string $content_string, string $id, string $mapped_attributes ): string {
		// Use regex to replace data-etch-ref and normalize spacing
		$pattern = '/\s*data-etch-ref="' . preg_quote( esc_attr( (string) $id ), '/' ) . '"\s*/';
		return EtchTypeAsserter::to_string(
			preg_replace(
				$pattern,
				$mapped_attributes,
				EtchTypeAsserter::to_string( $content_string )
			)
		);
	}


	/**
	 * Maps an array of attributes to a string.
	 *
	 * @param array<string, mixed> $attributes The attributes to map.
	 * @return string The mapped attributes.
	 */
	private function map_nested_attributes( array $attributes ): string {
		if ( empty( $attributes ) ) {
			return '';
		}

		$mapped_attributes = '';
		$has_dynamic_content = false;

		foreach ( $attributes as $key => $value ) {
			if ( ! is_string( $key ) || ! EtchTypeAsserter::is_stringable( $value ) ) {
				continue;
			}

			$original_value = EtchTypeAsserter::to_string( $value );

			// Parse dynamic placeholders and detect if templates were found
			[$parsed_value, $has_templates] = EtchParser::replace_string( $original_value, $this->get_context(), true );

			// Track if any attribute had dynamic content
			if ( $has_templates ) {
				$has_dynamic_content = true;
			}

			$mapped_attributes .= ' ' . esc_attr( $key ) . '="' . esc_attr( EtchTypeAsserter::parse_result_to_string( $parsed_value ) ) . '"';
		}

		// Register dynamic styles based on the mapped attributes only if dynamic content was found
		if ( $has_dynamic_content ) {
			$this->register_dynamic_styles( $mapped_attributes );
		}

		return $mapped_attributes;
	}

	/**
	 * Check if the current block is an etch SVG block.
	 *
	 * @return bool True if SVG block, false otherwise.
	 */
	private function is_etch_svg(): bool {
		return null !== $this->etch_data && 'svg' === $this->etch_data->specialized;
	}

	/**
	 * Load SVG content from URL and set as inner content.
	 *
	 * @param string $parsed_attributes The parsed attributes for the SVG.
	 *
	 * @return void
	 */
	private function load_and_set_svg_content( $parsed_attributes ): void {
		$src = EtchParser::replace_string( $this->etch_data->attributes['src'] ?? '', $this->get_context() );

		// Load SVG content from the URL
		$svg_content = SvgLoader::fetch_svg_cached( $src );

		if ( empty( $svg_content ) ) {
			return;
		}

		$strip_colors = EtchParser::replace_string( $this->etch_data->attributes['stripColors'] ?? '', $this->get_context() );

		$options = array(
			'strip_colors' => EtchTypeAsserter::to_bool( $strip_colors ),
			'attributes'   => $parsed_attributes,
		);

		$svg_content = SvgLoader::prepare_svg_for_output(
			$svg_content,
			$options
		);

		$this->set_inner_content( array( $svg_content ) );
		$this->set_inner_html( $svg_content );
	}
}
