<?php
/**
 * Server-side layout exporter.
 *
 * Generates exported HTML, CSS, and JS from sanitized canvas data.
 *
 * @package OpusPriceBuilder\Export
 */

declare(strict_types=1);

namespace OpusPriceBuilder\Export;

/**
 * Exports canvas layouts to HTML/CSS/JS for frontend rendering.
 */
class LayoutExporter {

	/**
	 * Supported CSS animation types (Pro CountUp is handled in pro/includes/Frontend/CountUpAssets.php).
	 *
	 * @var string[]
	 */
	private const CSS_ANIMATION_TYPES = array(
		'fadeIn',
		'slideIn',
		'scaleIn',
		'pulse',
		'glow',
	);

	/**
	 * Element type to CSS class map.
	 *
	 * @var array<string, string>
	 */
	private const CLASS_MAP = array(
		'regularPrice'       => 'oppb-regular-price',
		'salePrice'          => 'oppb-sale-price',
		'variablePriceRange' => 'oppb-variable-range',
		'currencySymbol'     => 'oppb-currency',
		'discountBadge'      => 'oppb-discount-badge',
		'text'               => 'oppb-text',
	);

	/**
	 * Export canvas data to HTML, CSS, and JS.
	 *
	 * @param array<string, mixed> $canvas_data Sanitized canvas data.
	 * @return array{exported_html: string, exported_css: string, exported_js: string}
	 */
	public static function export( array $canvas_data ): array {
		$elements           = self::getVisibleElements( $canvas_data['elements'] ?? array() );
		$container_settings = is_array( $canvas_data['containerSettings'] ?? null )
			? $canvas_data['containerSettings']
			: array();
		$preset_id = isset( $canvas_data['presetId'] ) ? (string) $canvas_data['presetId'] : '';
		$preset_assets = null;

		if ( '' !== $preset_id ) {
			$preset_assets = self::resolvePresetAssets( $preset_id );
		}

		if ( empty( $elements ) && ( null === $preset_assets || empty( $preset_assets['html'] ) ) ) {
			return array(
				'exported_html' => '<!-- No visible elements -->',
				'exported_css'  => '',
				'exported_js'   => '',
			);
		}

		$analysis = LayoutAnalyzer::analyze( $elements );
		$html     = self::generateHtml( $analysis, $preset_assets );
		$css      = self::generateCss( $elements, $container_settings, $analysis );
		$js     = '';

		if ( null !== $preset_assets ) {
			if ( ! empty( $preset_assets['css'] ) ) {
				$css .= "\n\n/* Preset CSS */\n" . trim( $preset_assets['css'] );
			}
			if ( ! empty( $preset_assets['js'] ) ) {
				$js = trim( $preset_assets['js'] );
			}
		}

		return array(
			'exported_html' => $html,
			'exported_css'  => trim( $css ),
			'exported_js'   => $js,
		);
	}

	/**
	 * Resolve preset CSS/JS/HTML via the oppb_preset_assets filter.
	 *
	 * Pro registers PresetHooks on oppb_init; free builds have no bundled presets.
	 *
	 * @param string $preset_id Preset identifier from canvas_data.
	 * @return array{css: string, js: string, html: string|null}|null
	 */
	private static function resolvePresetAssets( string $preset_id ): ?array {
		/**
		 * Filter preset assets for server-side export.
		 *
		 * @param array{css: string, js: string, html: string|null}|null $assets    Preset assets.
		 * @param string                                                  $preset_id Preset identifier.
		 */
		return apply_filters( 'oppb_preset_assets', null, $preset_id );
	}

	/**
	 * Filter to visible elements only.
	 *
	 * @param mixed $elements Canvas elements.
	 * @return array<int, array<string, mixed>>
	 */
	private static function getVisibleElements( $elements ): array {
		if ( ! is_array( $elements ) ) {
			return array();
		}

		return array_values(
			array_filter(
				$elements,
				static function ( $element ): bool {
					return is_array( $element ) && ( $element['visible'] ?? true );
				}
			)
		);
	}

	/**
	 * @param array<string, mixed>                                    $analysis Layout analysis from LayoutAnalyzer.
	 * @param array{css: string, js: string, html: string|null}|null $preset_assets Preset assets.
	 */
	private static function generateHtml( array $analysis, ?array $preset_assets ): string {
		if ( null !== $preset_assets && ! empty( $preset_assets['html'] ) ) {
			$inner = trim( $preset_assets['html'] );

			return '<span class="oppb-price-container">' . "\n  " . $inner . "\n" . '</span>';
		}

		$lines = array( '<span class="oppb-price-container">' );

		if ( 'columns' === ( $analysis['layout_type'] ?? 'rows' ) ) {
			foreach ( $analysis['columns'] as $col_index => $col ) {
				$col_num   = $col_index + 1;
				$col_class = 'oppb-price-column oppb-price-column--' . $col_num;
				$lines[]   = '  <span class="' . esc_attr( $col_class ) . '">';

				foreach ( $col['rows'] as $row_index => $row ) {
					if ( count( $row['elements'] ) > 1 || count( $col['rows'] ) > 1 ) {
						$row_class = 'oppb-price-row oppb-price-row--' . ( $row_index + 1 );
						$lines[]   = '    <span class="' . esc_attr( $row_class ) . '">';
						$indent    = '      ';
					} else {
						$indent = '    ';
					}

					foreach ( $row['elements'] as $element ) {
						$lines[] = $indent . self::generateElementHtml( $element );
					}

					if ( count( $row['elements'] ) > 1 || count( $col['rows'] ) > 1 ) {
						$lines[] = '    </span>';
					}
				}

				$lines[] = '  </span>';
			}
		} else {
			$row_num = 0;
			foreach ( $analysis['rows'] as $row ) {
				++$row_num;
				$multiple_rows = count( $analysis['rows'] ) > 1;
				$row_class     = $multiple_rows ? 'oppb-price-row oppb-price-row--' . $row_num : 'oppb-price-row';

				if ( count( $row['elements'] ) > 1 || $multiple_rows ) {
					$lines[] = '  <span class="' . esc_attr( $row_class ) . '">';
					$indent  = '    ';
				} else {
					$indent = '  ';
				}

				foreach ( $row['elements'] as $element ) {
					$lines[] = $indent . self::generateElementHtml( $element );
				}

				if ( count( $row['elements'] ) > 1 || $multiple_rows ) {
					$lines[] = '  </span>';
				}
			}
		}

		foreach ( $analysis['absolute_elements'] as $abs ) {
			$element = $abs['element'] ?? null;
			if ( is_array( $element ) ) {
				$lines[] = '  ' . self::generateElementHtml( $element );
			}
		}

		$lines[] = '</span>';

		return implode( "\n", $lines );
	}

	/**
	 * @param array<int, array<string, mixed>> $elements Visible elements.
	 * @param array<string, mixed>             $container_settings Container settings.
	 * @param array<string, mixed>               $analysis Layout analysis.
	 */
	private static function generateCss( array $elements, array $container_settings, array $analysis ): string {
		$rules     = array( '/* Opus Price Builder - Generated CSS */', '' );
		$rules[]   = '.oppb-regular-price__decimal,';
		$rules[]   = '.oppb-sale-price__decimal {';
		$rules[]   = '  white-space: nowrap;';
		$rules[]   = '}';
		$rules[]   = '';
		$bounds    = $analysis['container_bounds'] ?? array();
		$has_abs   = ! empty( $analysis['absolute_elements'] );
		$container_props = array( 'position: relative' );

		if ( 'columns' === ( $analysis['layout_type'] ?? 'rows' ) ) {
			$container_props[] = 'display: inline-flex';
			$container_props[] = 'flex-direction: row';
			$container_props[] = 'align-items: ' . esc_attr( (string) ( $analysis['container_align_items'] ?? 'center' ) );
			$container_props[] = 'gap: ' . (int) ( $analysis['column_gap'] ?? 8 ) . 'px';
		} else {
			$container_horizontal = 'flex-start';
			if ( ! empty( $analysis['rows'] ) ) {
				$row_alignments = array_map(
					static fn( $row ) => (string) ( $row['alignment']['horizontal'] ?? 'flex-start' ),
					$analysis['rows']
				);
				$all_same = count( array_unique( $row_alignments ) ) === 1;
				if ( $all_same ) {
					$container_horizontal = $row_alignments[0];
				}
			}

			$container_props[] = 'display: inline-flex';
			$container_props[] = 'flex-direction: column';
			$container_props[] = 'align-items: ' . esc_attr( $container_horizontal );
			$container_props[] = 'gap: ' . (int) ( $analysis['row_gap'] ?? 8 ) . 'px';
		}

		if ( $has_abs && ! empty( $bounds ) ) {
			$min_width  = (int) round( (float) ( $bounds['width'] ?? 0 ) );
			$min_height = (int) round( (float) ( $bounds['height'] ?? 0 ) );
			$container_props[] = '--oppb-min-width: ' . $min_width . 'px';
			$container_props[] = '--oppb-min-height: ' . $min_height . 'px';
			$container_props[] = 'min-width: var(--oppb-min-width)';
			$container_props[] = 'min-height: var(--oppb-min-height)';
		}

		$container_props = array_merge( $container_props, self::generateContainerStyleProps( $container_settings ) );
		$rules[]         = ".oppb-price-container {\n  " . implode( ";\n  ", $container_props ) . ";\n}";

		if ( 'columns' === ( $analysis['layout_type'] ?? 'rows' ) ) {
			foreach ( $analysis['columns'] as $col_index => $col ) {
				$col_class = '.oppb-price-column--' . ( $col_index + 1 );
				$col_props = array(
					'display: inline-flex',
					'flex-direction: column',
					'align-items: flex-start',
					'justify-content: ' . esc_attr( (string) ( $col['vertical_align'] ?? 'flex-start' ) ),
				);

				if ( count( $col['rows'] ) > 1 ) {
					$col_props[] = 'gap: ' . (int) ( $analysis['row_gap'] ?? 4 ) . 'px';
				}

				$rules[] = '';
				$rules[] = $col_class . " {\n  " . implode( ";\n  ", $col_props ) . ";\n}";

				foreach ( $col['rows'] as $row_index => $row ) {
					if ( count( $row['elements'] ) > 1 || count( $col['rows'] ) > 1 ) {
						$row_class       = $col_class . ' .oppb-price-row--' . ( $row_index + 1 );
						$align_items     = LayoutAnalyzer::alignmentToCss( (string) ( $row['alignment']['vertical'] ?? 'baseline' ) );
						$justify_content = esc_attr( (string) ( $row['alignment']['horizontal'] ?? 'flex-start' ) );
						$rules[]         = '';
						$rules[]         = $row_class . " {\n  display: inline-flex;\n  align-items: {$align_items};\n  justify-content: {$justify_content};\n  gap: " . (int) ( $row['recommended_gap'] ?? 4 ) . "px;\n}";
						self::appendCustomAlignmentOffsets( $rules, $row_class, $row );
					}
				}
			}
		} else {
			foreach ( $analysis['rows'] as $row_index => $row ) {
				$row_selector    = count( $analysis['rows'] ) > 1 ? '.oppb-price-row--' . ( $row_index + 1 ) : '.oppb-price-row';
				$align_items     = LayoutAnalyzer::alignmentToCss( (string) ( $row['alignment']['vertical'] ?? 'baseline' ) );
				$justify_content = esc_attr( (string) ( $row['alignment']['horizontal'] ?? 'flex-start' ) );
				$needs_full_width = 'flex-start' !== ( $row['alignment']['horizontal'] ?? 'flex-start' );

				$rules[] = '';
				$row_css = "{$row_selector} {\n  display: inline-flex;\n  align-items: {$align_items};\n  justify-content: {$justify_content};";
				if ( $needs_full_width ) {
					$row_css .= "\n  width: 100%;";
				}
				$row_css .= "\n  gap: " . (int) ( $row['recommended_gap'] ?? 4 ) . "px;\n}";
				$rules[] = $row_css;
				self::appendCustomAlignmentOffsets( $rules, $row_selector, $row );
			}
		}

		$animation_keyframes = array();

		foreach ( $elements as $element ) {
			$class_name = self::getClassName( (string) ( $element['type'] ?? '' ) );
			if ( '' === $class_name ) {
				continue;
			}

			$rules[] = '';
			$props   = array();

			if ( ( $element['positionType'] ?? 'relative' ) === 'absolute' ) {
				$props[] = self::generateAbsoluteCss( $element, $bounds );
			}

			$props[] = self::generateBaseElementCss( $element );

			$rules[] = ".{$class_name} {\n  " . implode( ";\n  ", array_filter( $props ) ) . ";\n}";

			if ( self::isPriceElement( $element ) ) {
				$decimal_css = self::generateDecimalCss( $element );
				if ( '' !== $decimal_css ) {
					$rules[] = '';
					$rules[] = $decimal_css;
				}

				$line_through_css = self::generateLineThroughCss( $element );
				if ( '' !== $line_through_css ) {
					$rules[] = '';
					$rules[] = $line_through_css;
				}
			}

			if ( self::isVariablePriceRangeElement( $element ) ) {
				$variable_css = self::generateVariablePriceRangeCss( $element );
				if ( '' !== $variable_css ) {
					$rules[] = '';
					$rules[] = $variable_css;
				}
			}

			if ( self::isDiscountBadgeElement( $element ) ) {
				$format = (string) ( $element['format'] ?? 'percentage' );
				$custom = (string) ( $element['customFormat'] ?? '' );
				if ( 'custom' === $format && str_contains( $custom, "\n" ) ) {
					$rules[] = '';
					$rules[] = ".{$class_name} {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}";
					$rules[] = '';
					$rules[] = ".oppb-discount-line {\n  display: block;\n}";
				}
			}

			if ( self::isTextElement( $element ) ) {
				$text_rules = array(
					'text-align: ' . esc_attr( (string) ( $element['textAlign'] ?? 'left' ) ),
					'white-space: pre-wrap',
				);

				$width_mode = (string) ( $element['widthMode'] ?? 'auto' );
				if ( 'full' === $width_mode ) {
					$text_rules[] = 'width: 100%';
				} elseif ( 'custom' === $width_mode ) {
					$text_rules[] = 'width: ' . (int) ( $element['customWidth'] ?? 0 ) . 'px';
				}

				if ( ! empty( $element['backgroundImage'] ) ) {
					$text_rules[] = "background-image: url('" . esc_url( (string) $element['backgroundImage'] ) . "')";
					$text_rules[] = 'background-size: ' . esc_attr( (string) ( $element['backgroundSize'] ?? 'cover' ) );
					$text_rules[] = 'background-position: ' . esc_attr( (string) ( $element['backgroundPosition'] ?? 'center' ) );
					$text_rules[] = 'background-repeat: ' . esc_attr( (string) ( $element['backgroundRepeat'] ?? 'no-repeat' ) );
				}

				$rules[] = '';
				$rules[] = ".{$class_name} {\n  " . implode( ";\n  ", $text_rules ) . ";\n}";

				$text = (string) ( $element['text'] ?? '' );
				if ( str_contains( $text, "\n" ) ) {
					$rules[] = '';
					$rules[] = ".oppb-text-line {\n  display: block;\n}";
				}
			}

			if ( self::isCurrencyElement( $element ) && ! empty( $element['useCustomImage'] ) && ! empty( $element['customImageUrl'] ) ) {
				$font_size = (int) self::desktopValue( $element['typography']['fontSize'] ?? null, 16 );
				$rules[]   = '';
				$rules[]   = ".{$class_name}__img {\n  height: {$font_size}px;\n  width: auto;\n  vertical-align: middle;\n}";
			}

			$animation = is_array( $element['animation'] ?? null ) ? $element['animation'] : array();
			$anim_type = (string) ( $animation['type'] ?? 'none' );
			if ( in_array( $anim_type, self::CSS_ANIMATION_TYPES, true ) ) {
				$animation_css = self::generateAnimationCss( $element, $class_name, $animation_keyframes );
				if ( '' !== $animation_css ) {
					$rules[] = '';
					$rules[] = $animation_css;
				}
			}
		}

		if ( ! empty( $animation_keyframes ) ) {
			array_splice( $rules, 2, 0, array_merge( array( '' ), array_values( $animation_keyframes ) ) );
		}

		return implode( "\n", $rules );
	}

	/**
	 * @param array<string, mixed> $container_settings Container settings.
	 * @return string[]
	 */
	private static function generateContainerStyleProps( array $container_settings ): array {
		$props = array();

		$padding = $container_settings['padding'] ?? null;
		if ( is_array( $padding ) ) {
			$pt = (int) self::desktopValue( $padding['top'] ?? null, 0 );
			$pr = (int) self::desktopValue( $padding['right'] ?? null, 0 );
			$pb = (int) self::desktopValue( $padding['bottom'] ?? null, 0 );
			$pl = (int) self::desktopValue( $padding['left'] ?? null, 0 );
			if ( $pt || $pr || $pb || $pl ) {
				$props[] = "padding: {$pt}px {$pr}px {$pb}px {$pl}px";
			}
		}

		$margin = $container_settings['margin'] ?? null;
		if ( is_array( $margin ) ) {
			$mt = (int) self::desktopValue( $margin['top'] ?? null, 0 );
			$mr = (int) self::desktopValue( $margin['right'] ?? null, 0 );
			$mb = (int) self::desktopValue( $margin['bottom'] ?? null, 0 );
			$ml = (int) self::desktopValue( $margin['left'] ?? null, 0 );
			if ( $mt || $mr || $mb || $ml ) {
				$props[] = "margin: {$mt}px {$mr}px {$mb}px {$ml}px";
			}
		}

		$border = $container_settings['border'] ?? null;
		if ( is_array( $border ) ) {
			$width = (int) self::desktopValue( $border['width'] ?? null, 0 );
			if ( $width > 0 ) {
				$style = esc_attr( (string) ( $border['style'] ?? 'solid' ) );
				$color = esc_attr( (string) ( $border['color'] ?? '#000000' ) );
				$props[] = "border: {$width}px {$style} {$color}";
			}
			$radius = (int) ( $border['radius'] ?? 0 );
			if ( $radius > 0 ) {
				$props[] = "border-radius: {$radius}px";
			}
		}

		$background_color = (string) ( $container_settings['backgroundColor'] ?? 'transparent' );
		if ( '' !== $background_color && 'transparent' !== $background_color ) {
			$props[] = 'background-color: ' . esc_attr( $background_color );
		}

		if ( ! empty( $container_settings['backgroundImage'] ) ) {
			$props[] = "background-image: url(" . esc_url( (string) $container_settings['backgroundImage'] ) . ')';
			$props[] = 'background-size: ' . esc_attr( (string) ( $container_settings['backgroundSize'] ?? 'cover' ) );
			$props[] = 'background-position: ' . esc_attr( (string) ( $container_settings['backgroundPosition'] ?? 'center' ) );
			$props[] = 'background-repeat: ' . esc_attr( (string) ( $container_settings['backgroundRepeat'] ?? 'no-repeat' ) );
		}

		return $props;
	}

	/**
	 * Append margin rules for custom vertical alignment offsets.
	 *
	 * @param array<int, string>   $rules        CSS rules (modified in place).
	 * @param string               $row_selector Row CSS selector.
	 * @param array<string, mixed> $row          Analyzed row from LayoutAnalyzer.
	 */
	private static function appendCustomAlignmentOffsets( array &$rules, string $row_selector, array $row ): void {
		$alignment = $row['alignment'] ?? null;
		if ( ! is_array( $alignment ) || 'custom' !== ( $alignment['vertical'] ?? '' ) ) {
			return;
		}

		$offsets = $alignment['offsets'] ?? array();
		if ( ! is_array( $offsets ) || empty( $offsets ) ) {
			return;
		}

		$elements = $row['elements'] ?? array();
		if ( ! is_array( $elements ) ) {
			return;
		}

		foreach ( $offsets as $element_id => $offset ) {
			if ( ! is_array( $offset ) ) {
				continue;
			}

			$element = null;
			foreach ( $elements as $el ) {
				if ( is_array( $el ) && (string) ( $el['id'] ?? '' ) === (string) $element_id ) {
					$element = $el;
					break;
				}
			}

			if ( null === $element ) {
				continue;
			}

			$class_name = self::getClassName( (string) ( $element['type'] ?? '' ) );
			if ( '' === $class_name ) {
				continue;
			}

			$top    = (int) ( $offset['top'] ?? 0 );
			$bottom = (int) ( $offset['bottom'] ?? 0 );

			if ( $top > 0 ) {
				$rules[] = "{$row_selector} .{$class_name} { margin-top: {$top}px; }";
			} elseif ( $bottom > 0 ) {
				$rules[] = "{$row_selector} .{$class_name} { margin-bottom: {$bottom}px; }";
			}
		}
	}

	/**
	 * @param array<int, array<string, mixed>> $row Row elements.
	 */
	private static function calculateElementGap( array $row ): int {
		if ( count( $row ) < 2 ) {
			return 8;
		}

		$gaps = array();

		for ( $i = 1; $i < count( $row ); $i++ ) {
			$prev = $row[ $i - 1 ];
			$next = $row[ $i ];

			$prev_right = (float) ( $prev['position']['x'] ?? 0 ) + (float) ( $prev['size']['width'] ?? 100 );
			$next_left  = (float) ( $next['position']['x'] ?? 0 );
			$gaps[]     = max( 0.0, $next_left - $prev_right );
		}

		return (int) round( array_sum( $gaps ) / count( $gaps ) );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function generateElementHtml( array $element ): string {
		$type = (string) ( $element['type'] ?? '' );
		$id   = (string) ( $element['id'] ?? '' );

		switch ( $type ) {
			case 'regularPrice':
			case 'salePrice':
				$html = self::generatePriceHtml( $element );
				break;
			case 'variablePriceRange':
				$html = self::generateVariablePriceRangeHtml( $element );
				break;
			case 'currencySymbol':
				$html = self::generateCurrencyHtml( $element );
				break;
			case 'discountBadge':
				$html = self::generateDiscountBadgeHtml( $element );
				break;
			case 'text':
				$html = self::generateTextHtml( $element );
				break;
			default:
				$html = '<span class="oppb-element"></span>';
		}

		if ( '' !== $id ) {
			$html = preg_replace( '/^<span /', '<span data-element-id="' . esc_attr( $id ) . '" ', $html, 1 ) ?? $html;
		}

		return $html;
	}

	/**
	 * @param array<string, mixed> $element Price element.
	 */
	private static function generatePriceHtml( array $element ): string {
		$class_name         = self::getClassName( (string) $element['type'] );
		$placeholder        = 'regularPrice' === $element['type'] ? '{regularPrice}' : '{salePrice}';
		$main_placeholder   = 'regularPrice' === $element['type'] ? '{regularPriceMain}' : '{salePriceMain}';
		$decimal_placeholder = 'regularPrice' === $element['type'] ? '{regularPriceDecimal}' : '{salePriceDecimal}';
		$decimal            = is_array( $element['decimal'] ?? null ) ? $element['decimal'] : array();
		$separator          = self::getDecimalSeparatorChar( $decimal );
		$show_currency      = $decimal['showCurrency'] ?? true;
		$currency_html      = $show_currency
			? '<span class="' . esc_attr( $class_name ) . '__currency">{currencySymbol}</span>'
			: '';

		if ( ! empty( $decimal['show'] ) && ( $decimal['position'] ?? 'inline' ) !== 'inline' ) {
			return '<span class="' . esc_attr( $class_name ) . '">' . $currency_html .
				'<span class="' . esc_attr( $class_name ) . '__main">' . $main_placeholder . '</span>' .
				'<span class="' . esc_attr( $class_name ) . '__decimal">' . esc_html( $separator ) . $decimal_placeholder . '</span>' .
				'</span>';
		}

		if ( isset( $decimal['show'] ) && false === $decimal['show'] ) {
			if ( $show_currency ) {
				return '<span class="' . esc_attr( $class_name ) . '">{currencySymbol}' . $main_placeholder . '</span>';
			}

			return '<span class="' . esc_attr( $class_name ) . '">' . $main_placeholder . '</span>';
		}

		if ( $show_currency ) {
			return '<span class="' . esc_attr( $class_name ) . '">' . $placeholder . '</span>';
		}

		return '<span class="' . esc_attr( $class_name ) . '">' . $main_placeholder . esc_html( $separator ) . $decimal_placeholder . '</span>';
	}

	/**
	 * @param array<string, mixed> $element Variable range element.
	 */
	private static function generateVariablePriceRangeHtml( array $element ): string {
		$class_name   = 'oppb-variable-range';
		$display_mode = (string) ( $element['displayMode'] ?? 'range' );
		$separator    = esc_html( (string) ( $element['separator'] ?? ' – ' ) );
		$prefix_text  = esc_html( (string) ( $element['prefixText'] ?? 'From ' ) );
		$show_currency = $element['showCurrency'] ?? true;

		$min_decimal = is_array( $element['minDecimal'] ?? null ) ? $element['minDecimal'] : array();
		$max_decimal = is_array( $element['maxDecimal'] ?? null ) ? $element['maxDecimal'] : array();
		$min_sep     = self::getDecimalSeparatorChar( $min_decimal );
		$max_sep     = self::getDecimalSeparatorChar( $max_decimal );

		$min_has_decimal = ! empty( $min_decimal['show'] ) && ( $min_decimal['position'] ?? 'inline' ) !== 'inline';
		$max_has_decimal = ! empty( $max_decimal['show'] ) && ( $max_decimal['position'] ?? 'inline' ) !== 'inline';

		$get_min_html = static function ( bool $with_decimal ) use ( $class_name, $show_currency, $min_sep, $min_has_decimal, $min_decimal ): string {
			if ( $with_decimal && $min_has_decimal ) {
				$currency_span = $show_currency
					? '<span class="' . esc_attr( $class_name ) . '__min-currency">{currencySymbol}</span>'
					: '';

				return $currency_span .
					'<span class="' . esc_attr( $class_name ) . '__min-main">{minPriceMain}</span>' .
					'<span class="' . esc_attr( $class_name ) . '__min-decimal">' . esc_html( $min_sep ) . '{minPriceDecimal}</span>';
			}

			if ( isset( $min_decimal['show'] ) && false === $min_decimal['show'] ) {
				return $show_currency ? '{currencySymbol}{minPriceMain}' : '{minPriceMain}';
			}

			return $show_currency ? '{minPrice}' : '{minPriceMain}';
		};

		$get_max_html = static function ( bool $with_decimal ) use ( $class_name, $show_currency, $max_sep, $max_has_decimal, $max_decimal ): string {
			if ( $with_decimal && $max_has_decimal ) {
				$currency_span = $show_currency
					? '<span class="' . esc_attr( $class_name ) . '__max-currency">{currencySymbol}</span>'
					: '';

				return $currency_span .
					'<span class="' . esc_attr( $class_name ) . '__max-main">{maxPriceMain}</span>' .
					'<span class="' . esc_attr( $class_name ) . '__max-decimal">' . esc_html( $max_sep ) . '{maxPriceDecimal}</span>';
			}

			if ( isset( $max_decimal['show'] ) && false === $max_decimal['show'] ) {
				return $show_currency ? '{currencySymbol}{maxPriceMain}' : '{maxPriceMain}';
			}

			return $show_currency ? '{maxPrice}' : '{maxPriceMain}';
		};

		$inner = '';
		switch ( $display_mode ) {
			case 'from':
				$inner = '<span class="' . esc_attr( $class_name ) . '__prefix">' . $prefix_text . '</span>' .
					'<span class="' . esc_attr( $class_name ) . '__min">' . $get_min_html( $min_has_decimal ) . '</span>';
				break;
			case 'up-to':
				$inner = '<span class="' . esc_attr( $class_name ) . '__prefix">' . $prefix_text . '</span>' .
					'<span class="' . esc_attr( $class_name ) . '__max">' . $get_max_html( $max_has_decimal ) . '</span>';
				break;
			case 'min-only':
				$inner = '<span class="' . esc_attr( $class_name ) . '__min">' . $get_min_html( $min_has_decimal ) . '</span>';
				break;
			case 'max-only':
				$inner = '<span class="' . esc_attr( $class_name ) . '__max">' . $get_max_html( $max_has_decimal ) . '</span>';
				break;
			case 'range':
			default:
				$inner = '<span class="' . esc_attr( $class_name ) . '__min">' . $get_min_html( $min_has_decimal ) . '</span>' .
					'<span class="' . esc_attr( $class_name ) . '__separator">' . $separator . '</span>' .
					'<span class="' . esc_attr( $class_name ) . '__max">' . $get_max_html( $max_has_decimal ) . '</span>';
				break;
		}

		return '<span class="' . esc_attr( $class_name ) . '">' . $inner . '</span>';
	}

	/**
	 * @param array<string, mixed> $element Currency element.
	 */
	private static function generateCurrencyHtml( array $element ): string {
		$class_name = self::getClassName( 'currencySymbol' );

		if ( ! empty( $element['useCustomImage'] ) && ! empty( $element['customImageUrl'] ) ) {
			$url = esc_url( (string) $element['customImageUrl'] );

			return '<span class="' . esc_attr( $class_name ) . '"><img src="' . $url . '" alt="' . esc_attr__( 'Currency', 'opus-price-builder' ) . '" class="' . esc_attr( $class_name ) . '__img" /></span>';
		}

		return '<span class="' . esc_attr( $class_name ) . '">{currencySymbol}</span>';
	}

	/**
	 * @param array<string, mixed> $element Discount badge element.
	 */
	private static function generateDiscountBadgeHtml( array $element ): string {
		$class_name = self::getClassName( 'discountBadge' );
		$format     = (string) ( $element['format'] ?? 'percentage' );

		switch ( $format ) {
			case 'amount':
				$content = '-{discountAmount}';
				break;
			case 'custom':
				$custom_content = (string) ( $element['customFormat'] ?? '-{discount}%' );
				if ( str_contains( $custom_content, "\n" ) ) {
					$lines   = explode( "\n", $custom_content );
					$content = '';
					foreach ( $lines as $line ) {
						$content .= '<span class="oppb-discount-line">' . esc_html( $line ) . '</span>';
					}
				} else {
					$content = esc_html( $custom_content );
				}
				break;
			case 'percentage':
			default:
				$content = '-{discount}%';
				break;
		}

		return '<span class="' . esc_attr( $class_name ) . '">' . $content . '</span>';
	}

	/**
	 * @param array<string, mixed> $element Text element.
	 */
	private static function generateTextHtml( array $element ): string {
		$class_name = self::getClassName( 'text' );
		$text       = (string) ( $element['text'] ?? '' );

		if ( str_contains( $text, "\n" ) ) {
			$lines_html = '';
			foreach ( explode( "\n", $text ) as $line ) {
				$lines_html .= '<span class="oppb-text-line">' . ( '' !== $line ? esc_html( $line ) : '&nbsp;' ) . '</span>';
			}

			return '<span class="' . esc_attr( $class_name ) . '">' . $lines_html . '</span>';
		}

		return '<span class="' . esc_attr( $class_name ) . '">' . esc_html( $text ) . '</span>';
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function generateBaseElementCss( array $element ): string {
		$rules = array();

		if ( isset( $element['color'] ) ) {
			$rules[] = 'color: ' . esc_attr( (string) $element['color'] );
		}

		$background = (string) ( $element['backgroundColor'] ?? 'transparent' );
		if ( '' !== $background && 'transparent' !== $background ) {
			$rules[] = 'background-color: ' . esc_attr( $background );
		}

		$opacity = (float) ( $element['opacity'] ?? 1 );
		if ( $opacity < 1 ) {
			$rules[] = 'opacity: ' . $opacity;
		}

		$rules[] = self::generateTypographyCss( $element );

		$border = is_array( $element['border'] ?? null ) ? $element['border'] : array();
		$width  = (int) self::desktopValue( $border['width'] ?? null, 0 );
		if ( $width > 0 ) {
			$rules[] = 'border: ' . $width . 'px ' . esc_attr( (string) ( $border['style'] ?? 'solid' ) ) . ' ' . esc_attr( (string) ( $border['color'] ?? '#000000' ) );
		}
		$radius = (int) ( $border['radius'] ?? 0 );
		if ( $radius > 0 ) {
			$rules[] = 'border-radius: ' . $radius . 'px';
		}

		$padding = is_array( $element['padding'] ?? null ) ? $element['padding'] : array();
		$pt      = (int) self::desktopValue( $padding['top'] ?? null, 0 );
		$pr      = (int) self::desktopValue( $padding['right'] ?? null, 0 );
		$pb      = (int) self::desktopValue( $padding['bottom'] ?? null, 0 );
		$pl      = (int) self::desktopValue( $padding['left'] ?? null, 0 );
		if ( $pt || $pr || $pb || $pl ) {
			$rules[] = "padding: {$pt}px {$pr}px {$pb}px {$pl}px";
		}

		$margin = is_array( $element['margin'] ?? null ) ? $element['margin'] : array();
		$mt     = (int) self::desktopValue( $margin['top'] ?? null, 0 );
		$mr     = (int) self::desktopValue( $margin['right'] ?? null, 0 );
		$mb     = (int) self::desktopValue( $margin['bottom'] ?? null, 0 );
		$ml     = (int) self::desktopValue( $margin['left'] ?? null, 0 );
		if ( $mt || $mr || $mb || $ml ) {
			$rules[] = "margin: {$mt}px {$mr}px {$mb}px {$ml}px";
		}

		$rotation = (float) ( $element['rotation'] ?? 0 );
		if ( 0.0 !== $rotation ) {
			$rules[] = 'transform: rotate(' . $rotation . 'deg)';
		}

		return implode( ";\n  ", $rules );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function generateTypographyCss( array $element ): string {
		$typography = is_array( $element['typography'] ?? null ) ? $element['typography'] : array();
		$rules      = array();

		$font_family = (string) ( $typography['fontFamily'] ?? '' );
		if ( '' !== $font_family && 'inherit' !== $font_family && '__default__' !== $font_family && 'default' !== $font_family ) {
			$rules[] = "font-family: '" . esc_attr( $font_family ) . "', sans-serif";
		}

		$rules[] = 'font-size: ' . (int) self::desktopValue( $typography['fontSize'] ?? null, 16 ) . 'px';
		$rules[] = 'font-weight: ' . (int) self::desktopValue( $typography['fontWeight'] ?? null, 400 );

		$font_style = (string) ( $typography['fontStyle'] ?? 'normal' );
		if ( 'normal' !== $font_style ) {
			$rules[] = 'font-style: ' . esc_attr( $font_style );
		}

		$rules[] = 'line-height: ' . (float) ( $typography['lineHeight'] ?? 1.2 );

		$letter_spacing = (int) self::desktopValue( $typography['letterSpacing'] ?? null, 0 );
		if ( 0 !== $letter_spacing ) {
			$rules[] = 'letter-spacing: ' . $letter_spacing . 'px';
		}

		$text_transform = (string) ( $typography['textTransform'] ?? 'none' );
		if ( 'none' !== $text_transform ) {
			$rules[] = 'text-transform: ' . esc_attr( $text_transform );
		}

		$text_decoration = (string) ( $typography['textDecoration'] ?? 'none' );
		if ( 'none' !== $text_decoration ) {
			$rules[] = 'text-decoration: ' . esc_attr( $text_decoration );
		}

		return implode( ";\n  ", $rules );
	}

	/**
	 * @param array<string, mixed>                              $element Element data.
	 * @param array{x: float, y: float, width: float, height: float} $bounds  Container bounds.
	 */
	private static function generateAbsoluteCss( array $element, array $bounds ): string {
		$x = (int) round( (float) ( $element['position']['x'] ?? 0 ) - $bounds['x'] );
		$y = (int) round( (float) ( $element['position']['y'] ?? 0 ) - $bounds['y'] );

		return "position: absolute;\n  left: {$x}px;\n  top: {$y}px";
	}

	/**
	 * @param array<string, mixed> $element Price element.
	 */
	private static function generateDecimalCss( array $element ): string {
		$decimal = is_array( $element['decimal'] ?? null ) ? $element['decimal'] : array();
		if ( empty( $decimal['show'] ) || ( $decimal['position'] ?? 'inline' ) === 'inline' ) {
			return '';
		}

		$class_name = self::getClassName( (string) $element['type'] );
		$typo       = is_array( $decimal['typography'] ?? null ) ? $decimal['typography'] : array();
		$rules      = array();

		$main_size     = (int) self::desktopValue( $element['typography']['fontSize'] ?? null, 16 );
		$decimal_size  = isset( $typo['fontSize'] )
			? (int) self::desktopValue( $typo['fontSize'], (int) round( $main_size * 0.6 ) )
			: (int) round( $main_size * 0.6 );

		switch ( $decimal['position'] ?? 'inline' ) {
			case 'top':
				$rules[] = 'vertical-align: top';
				$rules[] = 'font-size: ' . $decimal_size . 'px';
				break;
			case 'superscript':
				$rules[] = 'vertical-align: super';
				$rules[] = 'font-size: ' . $decimal_size . 'px';
				break;
			case 'baseline':
				$rules[] = 'vertical-align: baseline';
				$rules[] = 'font-size: ' . $decimal_size . 'px';
				break;
			case 'absolute':
				$rules[] = 'position: absolute';
				if ( is_array( $decimal['absoluteOffset'] ?? null ) ) {
					$rules[] = 'left: ' . (int) ( $decimal['absoluteOffset']['x'] ?? 0 ) . 'px';
					$rules[] = 'top: ' . (int) ( $decimal['absoluteOffset']['y'] ?? 0 ) . 'px';
				}
				$rules[] = 'font-size: ' . $decimal_size . 'px';
				break;
		}

		if ( isset( $typo['fontWeight'] ) ) {
			$rules[] = 'font-weight: ' . (int) self::desktopValue( $typo['fontWeight'], 400 );
		}

		if ( ! empty( $typo['fontFamily'] ) && '__default__' !== $typo['fontFamily'] && 'default' !== $typo['fontFamily'] ) {
			$rules[] = "font-family: '" . esc_attr( (string) $typo['fontFamily'] ) . "', sans-serif";
		}

		$rules[] = 'font-style: ' . esc_attr( (string) ( $typo['fontStyle'] ?? 'normal' ) );
		$rules[] = 'white-space: nowrap';

		$letter_spacing = (int) self::desktopValue( $typo['letterSpacing'] ?? null, 0 );
		if ( 0 !== $letter_spacing ) {
			$rules[] = 'letter-spacing: ' . $letter_spacing . 'px';
		}

		$text_decoration = (string) ( $typo['textDecoration'] ?? 'none' );
		if ( 'none' !== $text_decoration ) {
			$rules[] = 'text-decoration: ' . esc_attr( $text_decoration );
		}

		return ".{$class_name}__decimal {\n  " . implode( ";\n  ", $rules ) . ";\n}";
	}

	/**
	 * @param array<string, mixed> $element Price element.
	 */
	private static function generateLineThroughCss( array $element ): string {
		$line_through = is_array( $element['lineThrough'] ?? null ) ? $element['lineThrough'] : array();
		if ( empty( $line_through['enabled'] ) ) {
			return '';
		}

		$class_name     = self::getClassName( (string) $element['type'] );
		$thickness      = (int) ( $line_through['thickness'] ?? 1 );
		$color          = esc_attr( (string) ( $line_through['color'] ?? '#000000' ) );
		$position_rule  = ( $element['positionType'] ?? 'relative' ) === 'absolute' ? '' : "position: relative;\n";
		$style          = (string) ( $line_through['style'] ?? 'single' );
		$direction      = (string) ( $line_through['direction'] ?? 'horizontal' );

		if ( 'cross' === $style ) {
			return ".{$class_name}::before,\n.{$class_name}::after {\n  content: \"\";\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 50%;\n  height: {$thickness}px;\n  background-color: {$color};\n}\n\n.{$class_name}::before {\n  transform: translateY(-50%) rotate(15deg);\n}\n\n.{$class_name}::after {\n  transform: translateY(-50%) rotate(-15deg);\n}\n\n.{$class_name} {\n  {$position_rule}}";
		}

		$rotation = 0;
		if ( 'diagonal-down' === $direction ) {
			$rotation = 15;
		} elseif ( 'diagonal-up' === $direction ) {
			$rotation = -15;
		}

		$transform = 'translateY(-50%)' . ( 0 !== $rotation ? " rotate({$rotation}deg)" : '' );
		$double    = 'double' === $style ? "box-shadow: 0 " . ( $thickness + 2 ) . "px 0 {$color};" : '';

		return ".{$class_name}::after {\n  content: \"\";\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 50%;\n  height: {$thickness}px;\n  background-color: {$color};\n  transform: {$transform};\n  {$double}\n}\n\n.{$class_name} {\n  {$position_rule}}";
	}

	/**
	 * @param array<string, mixed> $element Variable range element.
	 */
	private static function generateVariablePriceRangeCss( array $element ): string {
		$class_name      = 'oppb-variable-range';
		$main_font_size  = (int) self::desktopValue( $element['typography']['fontSize'] ?? null, 16 );
		$main_font_weight = (int) self::desktopValue( $element['typography']['fontWeight'] ?? null, 600 );
		$rules           = array();

		$sep_font_size = isset( $element['separatorTypography']['fontSize'] )
			? (int) self::desktopValue( $element['separatorTypography']['fontSize'], (int) round( $main_font_size * 0.75 ) )
			: (int) round( $main_font_size * 0.75 );
		$sep_font_weight = isset( $element['separatorTypography']['fontWeight'] )
			? (int) self::desktopValue( $element['separatorTypography']['fontWeight'], $main_font_weight )
			: $main_font_weight;
		$sep_letter_spacing = (int) self::desktopValue( $element['separatorTypography']['letterSpacing'] ?? null, 4 );

		$rules[] = ".{$class_name}__separator {\n  font-size: {$sep_font_size}px;\n  font-weight: {$sep_font_weight};\n  padding: 0 {$sep_letter_spacing}px;\n}";
		$rules[] = ".{$class_name}__prefix {\n  font-size: {$sep_font_size}px;\n  font-weight: {$sep_font_weight};\n  margin-right: 4px;\n}";
		$rules[] = ".{$class_name}__min {\n  font-size: {$main_font_size}px;\n  font-weight: {$main_font_weight};\n}";
		$rules[] = ".{$class_name}__max {\n  font-size: {$main_font_size}px;\n  font-weight: {$main_font_weight};\n}";

		return implode( "\n\n", $rules );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 * @param string               $class_name CSS class.
	 * @param array<string, string> $keyframes_collector Shared keyframe definitions.
	 */
	private static function generateAnimationCss( array $element, string $class_name, array &$keyframes_collector ): string {
		$animation = is_array( $element['animation'] ?? null ) ? $element['animation'] : array();
		$type      = (string) ( $animation['type'] ?? 'none' );

		if ( ! in_array( $type, self::CSS_ANIMATION_TYPES, true ) ) {
			return '';
		}

		$keyframe_map = array(
			'fadeIn'  => "@keyframes oppb-fadeIn {\n  from { opacity: 0; }\n  to { opacity: 1; }\n}",
			'slideIn' => "@keyframes oppb-slideIn {\n  from { opacity: 0; transform: translateY(10px); }\n  to { opacity: 1; transform: translateY(0); }\n}",
			'scaleIn' => "@keyframes oppb-scaleIn {\n  from { opacity: 0; transform: scale(0.9); }\n  to { opacity: 1; transform: scale(1); }\n}",
			'pulse'   => "@keyframes oppb-pulse {\n  0%, 100% { transform: scale(1); }\n  50% { transform: scale(1.05); }\n}",
			'glow'    => "@keyframes oppb-glow {\n  0%, 100% { box-shadow: 0 0 5px currentColor; }\n  50% { box-shadow: 0 0 20px currentColor; }\n}",
		);

		if ( ! isset( $keyframe_map[ $type ] ) ) {
			return '';
		}

		$keyframes_collector[ 'oppb-' . $type ] = $keyframe_map[ $type ];

		$duration = (int) ( $animation['duration'] ?? 500 );
		$delay    = (int) ( $animation['delay'] ?? 0 );
		$easing   = esc_attr( (string) ( $animation['easing'] ?? 'ease' ) );
		$extra    = in_array( $type, array( 'pulse', 'glow' ), true )
			? 'animation-iteration-count: infinite;'
			: 'animation-fill-mode: forwards;';

		return ".{$class_name} {\n  animation: oppb-{$type} {$duration}ms {$easing} {$delay}ms;\n  {$extra}\n}";
	}

	/**
	 * @param array<string, mixed> $decimal_settings Decimal settings.
	 */
	private static function getDecimalSeparatorChar( array $decimal_settings ): string {
		$separator = (string) ( $decimal_settings['separator'] ?? 'point' );

		if ( 'comma' === $separator ) {
			return ',';
		}

		if ( 'none' === $separator ) {
			return '';
		}

		return '.';
	}

	/**
	 * @param mixed $value Responsive or scalar value.
	 * @param mixed $fallback Fallback when missing.
	 * @return mixed
	 */
	private static function desktopValue( $value, $fallback ) {
		if ( ! is_array( $value ) ) {
			return $value ?? $fallback;
		}

		return $value['desktop'] ?? $fallback;
	}

	private static function getClassName( string $type ): string {
		return self::CLASS_MAP[ $type ] ?? '';
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function isPriceElement( array $element ): bool {
		$type = (string) ( $element['type'] ?? '' );

		return 'regularPrice' === $type || 'salePrice' === $type;
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function isVariablePriceRangeElement( array $element ): bool {
		return 'variablePriceRange' === ( $element['type'] ?? '' );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function isCurrencyElement( array $element ): bool {
		return 'currencySymbol' === ( $element['type'] ?? '' );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function isDiscountBadgeElement( array $element ): bool {
		return 'discountBadge' === ( $element['type'] ?? '' );
	}

	/**
	 * @param array<string, mixed> $element Element data.
	 */
	private static function isTextElement( array $element ): bool {
		return 'text' === ( $element['type'] ?? '' );
	}
}
