<?php
/**
 * Price Renderer for frontend display.
 *
 * Supports both Server-Side Rendering (SSR) and Web Component modes.
 * SSR is the default and recommended approach for best performance.
 *
 * @package OpusPriceBuilder\Frontend
 */

declare(strict_types=1);

namespace OpusPriceBuilder\Frontend;

use OpusPriceBuilder\Database\LayoutsTable;
use OpusPriceBuilder\Export\LayoutRenderer;
use OpusPriceBuilder\Locations;

/**
 * Handles rendering of price displays.
 *
 * The renderer supports two modes:
 * - SSR (Server-Side Rendering): Default, renders complete HTML immediately
 * - Web Component: Legacy mode, useful for dynamic price updates (AJAX)
 */
class PriceRenderer {

	/**
	 * Cached layouts by scenario and location.
	 *
	 * @var array<string, array>
	 */
	private static array $layoutCache = array();

	/**
	 * Render a price display for a product using Server-Side Rendering.
	 *
	 * This is the primary render method. It outputs fully styled HTML
	 * without requiring JavaScript for the initial display.
	 *
	 * @param \WC_Product $product      The WooCommerce product.
	 * @param string      $scenario     The price scenario.
	 * @param string      $location     The display location.
	 * @param int|null    $layout_id    Optional specific layout ID.
	 * @return string The HTML output.
	 */
	public static function render( \WC_Product $product, string $scenario = 'simple', string $location = 'single_product', ?int $layout_id = null ): string {
		$product_id = $product->get_id();
		$price_data = self::getPriceData( $product, $scenario );

		// Validate location using centralized Locations class.
		$location = Locations::sanitize( $location );

		// Check for product-specific layout override.
		if ( ! $layout_id ) {
			$overrides = get_post_meta( $product_id, '_oppb_layout_overrides', true );
			if ( is_array( $overrides ) && isset( $overrides[ $scenario ][ $location ] ) ) {
				$layout_id = (int) $overrides[ $scenario ][ $location ];
			} elseif ( is_array( $overrides ) && isset( $overrides[ $scenario ] ) && is_numeric( $overrides[ $scenario ] ) ) {
				// Legacy format: scenario => layout_id.
				$layout_id = (int) $overrides[ $scenario ];
			}
		}

		// Get the layout (specific or active).
		$layout = null;
		if ( $layout_id ) {
			$layout = self::getLayout( $layout_id );
		}

		if ( ! $layout ) {
			$layout = self::getActiveLayoutForLocation( $scenario, $location );
		}

		// No layout? Return empty string.
		if ( ! $layout ) {
			return '';
		}

		// Get the layout ID for CSS scoping.
		$current_layout_id = (int) ( $layout['id'] ?? 0 );

		$rendered = LayoutRenderer::render(
			$layout,
			array(
				'scenario'   => $scenario,
				'location'   => $location,
				'product_id' => $product_id,
			)
		);

		$html = self::processTemplate( $rendered['html'], $price_data, $product );

		// Generate unique container ID for potential JS targeting.
		$container_id = 'oppb-price-' . $product_id . '-' . wp_rand( 1000, 9999 );

		// Inject data attributes and scoping class into the existing container.
		// The exported HTML starts with <span class="oppb-price-container">
		$data_attrs = sprintf(
			'id="%s" data-scenario="%s" data-location="%s" data-product-id="%d" data-layout-id="%d"',
			esc_attr( $container_id ),
			esc_attr( $scenario ),
			esc_attr( $location ),
			$product_id,
			$current_layout_id
		);

		// Add layout-specific class for CSS scoping and inject data attributes.
		$scoping_class = 'oppb-layout-' . $current_layout_id;
		$html          = preg_replace(
			'/^<span class="oppb-price-container"/',
			'<span class="oppb-price-container ' . $scoping_class . '" ' . $data_attrs,
			$html,
			1
		);

		return $html;
	}

	/**
	 * Render a price Web Component for a product.
	 *
	 * Legacy method - use render() for most cases.
	 * Web Components are useful for:
	 * - Dynamic price updates (variable product selection)
	 * - AJAX cart/checkout scenarios
	 * - When JavaScript enhancement is required
	 *
	 * @param \WC_Product $product      The WooCommerce product.
	 * @param string      $scenario     The price scenario.
	 * @param string      $location     The display location.
	 * @param int|null    $layout_id    Optional specific layout ID.
	 * @return string The HTML output.
	 */
	public static function renderWebComponent( \WC_Product $product, string $scenario = 'simple', string $location = 'single_product', ?int $layout_id = null ): string {
		// Enable web component mode to ensure JS is loaded.
		FrontendAssets::enableWebComponentMode();

		$product_id = $product->get_id();
		$price_data = self::getPriceData( $product, $scenario );

		// Validate location.
		$location = Locations::sanitize( $location );

		// Check for product-specific layout override.
		if ( ! $layout_id ) {
			$overrides = get_post_meta( $product_id, '_oppb_layout_overrides', true );
			if ( is_array( $overrides ) && isset( $overrides[ $scenario ][ $location ] ) ) {
				$layout_id = (int) $overrides[ $scenario ][ $location ];
			} elseif ( is_array( $overrides ) && isset( $overrides[ $scenario ] ) && is_numeric( $overrides[ $scenario ] ) ) {
				// Legacy format: scenario => layout_id.
				$layout_id = (int) $overrides[ $scenario ];
			}
		}

		// If no specific layout, get the active layout for this scenario and location.
		if ( ! $layout_id ) {
			$active_layout = self::getActiveLayoutForLocation( $scenario, $location );
			if ( $active_layout ) {
				$layout_id = (int) $active_layout['id'];
			}
		}

		// Build data attributes.
		$attributes = array(
			'data-product-id'         => $product_id,
			'data-scenario'           => esc_attr( $scenario ),
			'data-location'           => esc_attr( $location ),
			'data-regular-price'      => esc_attr( $price_data['regular_price'] ),
			'data-currency-symbol'    => esc_attr( $price_data['currency_symbol'] ),
			'data-product-name'       => esc_attr( $product->get_name() ),
			'data-tax-label'          => esc_attr( self::getTaxLabel( $product ) ),
			'data-stock-status'       => esc_attr( $product->get_stock_status() ),
			'data-decimal-separator'  => esc_attr( wc_get_price_decimal_separator() ),
			'data-thousand-separator' => esc_attr( wc_get_price_thousand_separator() ),
		);

		if ( ! empty( $price_data['sale_price'] ) ) {
			$attributes['data-sale-price'] = esc_attr( $price_data['sale_price'] );
		}

		if ( ! empty( $price_data['discount_percentage'] ) ) {
			$attributes['data-discount-percentage'] = esc_attr( $price_data['discount_percentage'] );
		}

		// Variable price range attributes.
		if ( ! empty( $price_data['min_price'] ) ) {
			$attributes['data-min-price'] = esc_attr( $price_data['min_price'] );
		}
		if ( ! empty( $price_data['max_price'] ) ) {
			$attributes['data-max-price'] = esc_attr( $price_data['max_price'] );
		}
		if ( ! empty( $price_data['min_regular_price'] ) ) {
			$attributes['data-min-regular-price'] = esc_attr( $price_data['min_regular_price'] );
		}
		if ( ! empty( $price_data['max_regular_price'] ) ) {
			$attributes['data-max-regular-price'] = esc_attr( $price_data['max_regular_price'] );
		}
		if ( ! empty( $price_data['min_sale_price'] ) ) {
			$attributes['data-min-sale-price'] = esc_attr( $price_data['min_sale_price'] );
		}
		if ( ! empty( $price_data['max_sale_price'] ) ) {
			$attributes['data-max-sale-price'] = esc_attr( $price_data['max_sale_price'] );
		}

		if ( $layout_id ) {
			$attributes['data-layout-id'] = (int) $layout_id;
		}

		// Build attribute string.
		$attr_string = '';
		foreach ( $attributes as $key => $value ) {
			$attr_string .= sprintf( ' %s="%s"', $key, $value );
		}

		// Build fallback content (visible before JS loads).
		$fallback_html = self::buildFallbackHtml( $price_data, $scenario );

		// Return Web Component HTML with fallback content inside.
		return sprintf( '<oppb-price%s>%s</oppb-price>', $attr_string, $fallback_html );
	}

	/**
	 * Process HTML template by replacing placeholders with actual values.
	 *
	 * All placeholders from availablePlaceholders in layoutExporter/placeholders.ts:
	 * - {productName}, {productId}, {productID}
	 * - {regularPrice}, {regularPriceMain}, {regularPriceDecimal}, {regularPriceSeparator}
	 * - {salePrice}, {salePriceMain}, {salePriceDecimal}, {salePriceSeparator}
	 * - {minPrice}, {minPriceMain}, {minPriceDecimal}, {minPriceSeparator}, {maxPrice}, {maxPriceMain}, {maxPriceDecimal}, {maxPriceSeparator}
	 * - {minRegularPrice}, {minRegularPriceMain}, {minRegularPriceDecimal}
	 * - {maxRegularPrice}, {maxRegularPriceMain}, {maxRegularPriceDecimal}
	 * - {minSalePrice}, {minSalePriceMain}, {minSalePriceDecimal}
	 * - {maxSalePrice}, {maxSalePriceMain}, {maxSalePriceDecimal}
	 * - {currencySymbol}
	 * - {discount}, {discountPercentage}, {discountAmount}, {discountAmountRaw}, {savedAmount}
	 * - {taxLabel}, {stockStatus}
	 *
	 * Canvas text/prefix placeholders ({priceRangePrefix}, {text}, etc.) are
	 * resolved earlier in LayoutRenderer via CanvasPlaceholders.
	 *
	 * @param string      $html       The HTML template.
	 * @param array       $price_data The price data.
	 * @param \WC_Product $product    The product.
	 * @return string Processed HTML.
	 */
	private static function processTemplate( string $html, array $price_data, \WC_Product $product ): string {
		$regular_parts = self::splitPrice( $price_data['regular_price'] );
		$sale_parts    = $price_data['sale_price'] ? self::splitPrice( $price_data['sale_price'] ) : null;
		$separator     = wc_get_price_decimal_separator();
		$currency      = $price_data['currency_symbol'];
		$discount_pct  = $price_data['discount_percentage'] ?? '';

		// Variable price range parts.
		$min_price_parts         = $price_data['min_price'] ? self::splitPrice( $price_data['min_price'] ) : null;
		$max_price_parts         = $price_data['max_price'] ? self::splitPrice( $price_data['max_price'] ) : null;
		$min_regular_price_parts = $price_data['min_regular_price'] ? self::splitPrice( $price_data['min_regular_price'] ) : null;
		$max_regular_price_parts = $price_data['max_regular_price'] ? self::splitPrice( $price_data['max_regular_price'] ) : null;
		$min_sale_price_parts    = $price_data['min_sale_price'] ? self::splitPrice( $price_data['min_sale_price'] ) : null;
		$max_sale_price_parts    = $price_data['max_sale_price'] ? self::splitPrice( $price_data['max_sale_price'] ) : null;

		// Calculate discount amounts if applicable using raw product prices.
		$discount_amount_raw = '';
		$discount_amount     = '';
		$saved_amount        = '';

		if ( $price_data['sale_price'] && $price_data['regular_price'] ) {
			// Use raw price values from product to avoid locale parsing issues.
			$regular_raw = (float) $product->get_regular_price();
			$sale_raw    = (float) $product->get_sale_price();

			// For variable products on sale, use min variation prices.
			if ( $product->is_type( 'variable' ) && $product->is_on_sale() ) {
				/** @var \WC_Product_Variable $product */
				$regular_raw = (float) $product->get_variation_regular_price( 'min', true );
				$sale_raw    = (float) $product->get_variation_sale_price( 'min', true );
			}

			$diff                = $regular_raw - $sale_raw;
			$discount_amount_raw = self::formatPrice( $diff );
			$discount_amount     = $currency . $discount_amount_raw;
			$saved_amount        = $currency . $discount_amount_raw;
		}

		$replacements = array(
			// Product info.
			'{productName}'            => esc_html( $product->get_name() ),
			'{productId}'              => (string) $product->get_id(),
			'{productID}'              => (string) $product->get_id(),

			// Full prices with currency.
			'{regularPrice}'           => esc_html( $currency . $price_data['regular_price'] ),
			'{salePrice}'              => $price_data['sale_price'] ? esc_html( $currency . $price_data['sale_price'] ) : '',

			// Price parts (for split decimal display).
			'{regularPriceMain}'       => esc_html( $regular_parts['main'] ),
			'{regularPriceDecimal}'    => esc_html( $regular_parts['decimal'] ),
			'{regularPriceSeparator}'  => esc_html( $separator ),
			'{salePriceMain}'          => $sale_parts ? esc_html( $sale_parts['main'] ) : '',
			'{salePriceDecimal}'       => $sale_parts ? esc_html( $sale_parts['decimal'] ) : '',
			'{salePriceSeparator}'     => esc_html( $separator ),

			// Variable price range - min/max current prices.
			'{minPrice}'               => $price_data['min_price'] ? esc_html( $currency . $price_data['min_price'] ) : '',
			'{minPriceMain}'           => $min_price_parts ? esc_html( $min_price_parts['main'] ) : '',
			'{minPriceDecimal}'        => $min_price_parts ? esc_html( $min_price_parts['decimal'] ) : '',
			'{minPriceSeparator}'      => esc_html( $separator ),
			'{maxPrice}'               => $price_data['max_price'] ? esc_html( $currency . $price_data['max_price'] ) : '',
			'{maxPriceMain}'           => $max_price_parts ? esc_html( $max_price_parts['main'] ) : '',
			'{maxPriceDecimal}'        => $max_price_parts ? esc_html( $max_price_parts['decimal'] ) : '',
			'{maxPriceSeparator}'      => esc_html( $separator ),

			// Variable price range - min/max regular prices (before sale).
			'{minRegularPrice}'        => $price_data['min_regular_price'] ? esc_html( $currency . $price_data['min_regular_price'] ) : '',
			'{minRegularPriceMain}'    => $min_regular_price_parts ? esc_html( $min_regular_price_parts['main'] ) : '',
			'{minRegularPriceDecimal}' => $min_regular_price_parts ? esc_html( $min_regular_price_parts['decimal'] ) : '',
			'{maxRegularPrice}'        => $price_data['max_regular_price'] ? esc_html( $currency . $price_data['max_regular_price'] ) : '',
			'{maxRegularPriceMain}'    => $max_regular_price_parts ? esc_html( $max_regular_price_parts['main'] ) : '',
			'{maxRegularPriceDecimal}' => $max_regular_price_parts ? esc_html( $max_regular_price_parts['decimal'] ) : '',

			// Variable price range - min/max sale prices.
			'{minSalePrice}'           => $price_data['min_sale_price'] ? esc_html( $currency . $price_data['min_sale_price'] ) : '',
			'{minSalePriceMain}'       => $min_sale_price_parts ? esc_html( $min_sale_price_parts['main'] ) : '',
			'{minSalePriceDecimal}'    => $min_sale_price_parts ? esc_html( $min_sale_price_parts['decimal'] ) : '',
			'{maxSalePrice}'           => $price_data['max_sale_price'] ? esc_html( $currency . $price_data['max_sale_price'] ) : '',
			'{maxSalePriceMain}'       => $max_sale_price_parts ? esc_html( $max_sale_price_parts['main'] ) : '',
			'{maxSalePriceDecimal}'    => $max_sale_price_parts ? esc_html( $max_sale_price_parts['decimal'] ) : '',

			// Currency.
			'{currencySymbol}'         => esc_html( $currency ),

			// Discount info.
			'{discount}'               => esc_html( $discount_pct ),
			'{discountPercentage}'     => esc_html( $discount_pct ),
			'{discountAmount}'         => esc_html( $discount_amount ),
			'{discountAmountRaw}'      => esc_html( $discount_amount_raw ),
			'{savedAmount}'            => esc_html( $saved_amount ),

			// Additional info.
			'{taxLabel}'               => esc_html( self::getTaxLabel( $product ) ),
			'{stockStatus}'            => esc_html( self::getStockStatusText( $product ) ),
		);

		return str_replace( array_keys( $replacements ), array_values( $replacements ), $html );
	}

	/**
	 * Get human-readable stock status text.
	 *
	 * @param \WC_Product $product The product.
	 * @return string
	 */
	private static function getStockStatusText( \WC_Product $product ): string {
		$status = $product->get_stock_status();

		$status_labels = array(
			'instock'     => __( 'In stock', 'opus-price-builder' ),
			'outofstock'  => __( 'Out of stock', 'opus-price-builder' ),
			'onbackorder' => __( 'On backorder', 'opus-price-builder' ),
		);

		return $status_labels[ $status ] ?? $status;
	}

	/**
	 * Split a price string into main and decimal parts.
	 * Correctly handles thousand separators by checking expected decimal places.
	 *
	 * @param string $price The formatted price string.
	 * @return array{main: string, decimal: string}
	 */
	private static function splitPrice( string $price ): array {
		$decimal_separator = wc_get_price_decimal_separator();
		$expected_decimals = wc_get_price_decimals();

		// If no decimals expected, return price as-is.
		if ( $expected_decimals === 0 ) {
			return array(
				'main'    => $price,
				'decimal' => '',
			);
		}

		// Find the last occurrence of the decimal separator.
		$last_sep_pos = strrpos( $price, $decimal_separator );

		if ( $last_sep_pos === false ) {
			// No decimal separator found, use default.
			return array(
				'main'    => $price,
				'decimal' => str_repeat( '0', $expected_decimals ),
			);
		}

		// Verify this is actually the decimal separator (not thousand separator)
		// by checking that there are exactly $expected_decimals digits after it.
		$after_sep = substr( $price, $last_sep_pos + strlen( $decimal_separator ) );

		if ( strlen( $after_sep ) === $expected_decimals ) {
			// This is the decimal separator.
			return array(
				'main'    => substr( $price, 0, $last_sep_pos ),
				'decimal' => $after_sep,
			);
		}

		// The separator found is likely a thousand separator, not decimal.
		return array(
			'main'    => $price,
			'decimal' => str_repeat( '0', $expected_decimals ),
		);
	}

	/**
	 * Build fallback HTML for when JS hasn't loaded yet.
	 *
	 * @param array<string, string|null> $price_data The price data.
	 * @param string                     $scenario   The price scenario.
	 * @return string The fallback HTML.
	 */
	private static function buildFallbackHtml( array $price_data, string $scenario ): string {
		$currency = esc_html( $price_data['currency_symbol'] );
		$regular  = esc_html( $price_data['regular_price'] );
		$sale     = ! empty( $price_data['sale_price'] ) ? esc_html( $price_data['sale_price'] ) : '';

		// Sale scenario with sale price.
		if ( ( 'sale' === $scenario || 'variable_sale' === $scenario ) && $sale ) {
			return sprintf(
				'<del>%s%s</del> <ins>%s%s</ins>',
				$currency,
				$regular,
				$currency,
				$sale
			);
		}

		// Simple price.
		return sprintf( '%s%s', $currency, $regular );
	}

	/**
	 * Get price data from a product.
	 *
	 * @param \WC_Product $product  The product.
	 * @param string      $scenario The price scenario.
	 * @return array<string, string|null>
	 */
	public static function getPriceData( \WC_Product $product, string $scenario ): array {
		// Decode HTML entities (e.g., &#36; -> $) for proper display in frontend and web components.
		$currency_symbol = html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' );
		$regular_price   = '';
		$sale_price      = null;
		$discount        = null;

		// Variable price range data.
		$min_price         = null;
		$max_price         = null;
		$min_regular_price = null;
		$max_regular_price = null;
		$min_sale_price    = null;
		$max_sale_price    = null;

		if ( $product->is_type( 'variable' ) ) {
			/** @var \WC_Product_Variable $product */
			$min_price_raw = $product->get_variation_price( 'min', true );
			$max_price_raw = $product->get_variation_price( 'max', true );

			// Store formatted min/max prices for variablePriceRange element.
			$min_price = self::formatPrice( (float) $min_price_raw );
			$max_price = self::formatPrice( (float) $max_price_raw );

			// Get regular and sale price ranges.
			$min_regular_raw = $product->get_variation_regular_price( 'min', true );
			$max_regular_raw = $product->get_variation_regular_price( 'max', true );
			$min_sale_raw    = $product->get_variation_sale_price( 'min', true );
			$max_sale_raw    = $product->get_variation_sale_price( 'max', true );

			$min_regular_price = self::formatPrice( (float) $min_regular_raw );
			$max_regular_price = self::formatPrice( (float) $max_regular_raw );

			if ( $min_sale_raw ) {
				$min_sale_price = self::formatPrice( (float) $min_sale_raw );
			}
			if ( $max_sale_raw ) {
				$max_sale_price = self::formatPrice( (float) $max_sale_raw );
			}

			if ( $min_price_raw === $max_price_raw ) {
				$regular_price = self::formatPrice( (float) $min_price_raw );
			} else {
				$regular_price = self::formatPrice( (float) $min_price_raw ) . ' – ' . self::formatPrice( (float) $max_price_raw );
			}

			// Check for sale prices in variations.
			if ( $product->is_on_sale() ) {
				if ( $min_regular_raw && $min_sale_raw && $min_sale_raw < $min_regular_raw ) {
					$regular_price = self::formatPrice( (float) $min_regular_raw );
					$sale_price    = self::formatPrice( (float) $min_sale_raw );
					$discount      = self::calculateDiscount( (float) $min_regular_raw, (float) $min_sale_raw );
				}
			}
		} else {
			// Simple product
			$regular = $product->get_regular_price();
			$sale    = $product->get_sale_price();

			if ( $regular ) {
				$regular_price = self::formatPrice( (float) $regular );
			}

			if ( $product->is_on_sale() && $sale ) {
				$sale_price = self::formatPrice( (float) $sale );
				$discount   = self::calculateDiscount( (float) $regular, (float) $sale );
			}
		}

		return array(
			'regular_price'       => $regular_price,
			'sale_price'          => $sale_price,
			'discount_percentage' => $discount,
			'currency_symbol'     => $currency_symbol,
			// Variable price range data.
			'min_price'           => $min_price,
			'max_price'           => $max_price,
			'min_regular_price'   => $min_regular_price,
			'max_regular_price'   => $max_regular_price,
			'min_sale_price'      => $min_sale_price,
			'max_sale_price'      => $max_sale_price,
		);
	}

	/**
	 * Format a price without HTML.
	 *
	 * @param float $price The price.
	 * @return string
	 */
	private static function formatPrice( float $price ): string {
		$decimals           = wc_get_price_decimals();
		$decimal_separator  = wc_get_price_decimal_separator();
		$thousand_separator = wc_get_price_thousand_separator();

		return number_format( $price, $decimals, $decimal_separator, $thousand_separator );
	}

	/**
	 * Calculate discount percentage.
	 *
	 * @param float $regular Regular price.
	 * @param float $sale    Sale price.
	 * @return string|null
	 */
	private static function calculateDiscount( float $regular, float $sale ): ?string {
		if ( $regular <= 0 || $sale >= $regular ) {
			return null;
		}

		$discount = round( ( ( $regular - $sale ) / $regular ) * 100 );
		return (string) $discount;
	}

	/**
	 * Get tax label for product.
	 *
	 * @param \WC_Product $product The product.
	 * @return string
	 */
	private static function getTaxLabel( \WC_Product $product ): string {
		if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
			$tax_string = WC()->countries->inc_tax_or_vat();
			return $tax_string ? $tax_string : '';
		}

		if ( wc_tax_enabled() && 'excl' === get_option( 'woocommerce_tax_display_shop' ) ) {
			$tax_string = WC()->countries->ex_tax_or_vat();
			return $tax_string ? $tax_string : '';
		}

		return '';
	}

	/**
	 * Determine the price scenario for a product.
	 *
	 * @param \WC_Product $product The product.
	 * @return string
	 */
	public static function determineScenario( \WC_Product $product ): string {
		$is_variable = $product->is_type( 'variable' );
		$is_on_sale  = $product->is_on_sale();

		if ( $is_variable && $is_on_sale ) {
			return 'variable_sale';
		}

		if ( $is_variable ) {
			return 'variable_simple';
		}

		if ( $is_on_sale ) {
			return 'sale';
		}

		return 'simple';
	}

	/**
	 * Get active layout for a scenario and location.
	 *
	 * @param string $scenario The price scenario.
	 * @param string $location The display location.
	 * @return array|null
	 */
	public static function getActiveLayoutForLocation( string $scenario, string $location = 'single_product' ): ?array {
		$cache_key = $scenario . '_' . $location;

		// Check cache first.
		if ( isset( self::$layoutCache[ $cache_key ] ) ) {
			return self::$layoutCache[ $cache_key ];
		}

		// Use location-specific query.
		$layout = LayoutsTable::getActiveForLocation( $scenario, $location );

		if ( $layout ) {
			$layout_array                    = (array) $layout;
			self::$layoutCache[ $cache_key ] = $layout_array;
			return $layout_array;
		}

		return null;
	}

	/**
	 * Get all active layouts indexed by scenario and location.
	 *
	 * @param string $location The display location.
	 * @return array<string, array{html: string, css: string, customJs: string, id: int, canvas_data: string}>
	 */
	public static function getAllActiveLayouts( string $location = 'single_product' ): array {
		$scenarios = array( 'simple', 'sale', 'variable_simple', 'variable_sale' );
		$layouts   = array();

		foreach ( $scenarios as $scenario ) {
			$layout = self::getActiveLayoutForLocation( $scenario, $location );
			if ( $layout ) {
				$rendered = LayoutRenderer::render(
					$layout,
					array(
						'scenario' => $scenario,
						'location' => $location,
					)
				);

				$layouts[ $scenario ] = array(
					'html'        => $rendered['html'],
					'css'         => $rendered['css'],
					'customJs'    => $rendered['js'],
					'id'          => (int) ( $layout['id'] ?? 0 ),
					'canvas_data' => $layout['canvas_data'] ?? '',
				);
			}
		}

		return $layouts;
	}

	/**
	 * Get layout by ID.
	 *
	 * @param int $layout_id The layout ID.
	 * @return array|null
	 */
	public static function getLayout( int $layout_id ): ?array {
		return LayoutsTable::getById( $layout_id );
	}

	/**
	 * Clear the layout cache.
	 *
	 * @return void
	 */
	public static function clearCache(): void {
		self::$layoutCache = array();
	}
}
