<?php
/**
 * Payload Builder
 *
 * Generates a JSON array that complies with the Amazon Product-Type schema
 * using the merchant’s field mappings, channel defaults and live product data.
 *
 * NOTE: This initial implementation supports only flat scalar mappings and
 * simple array wrappers (`something[].value`). It will be iterated as new edge
 * cases surface.
 *
 * @package    MOMCS_Sync
 * @subpackage MOMCS_Sync\includes\common\handlers\payload
 */

declare( strict_types=1 );

namespace MOMCS_Sync\Includes\Common\Handlers\Payload;

use MOMCS_Sync\Includes\Common\Services\Amazon\Product_Type_Service;
use MOMCS_Sync\Includes\Common\Handlers\Mapping_Handler;
use MOMCS_Sync\Includes\Common\Services\Default_Meta_Service;
use MOMCS_Sync\Includes\Common\Utils\Logger;
use MOMCS_Sync\Includes\Common\Utils\Variation_Helper;
use MOMCS_Sync\Includes\Common\Constants\Plugin_Constants;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Payload Builder
 *
 * Generates a JSON array that complies with the Amazon Product-Type schema
 * using the merchant’s field mappings, channel defaults and live product data.
 */
class Payload_Builder {

	/**
	 * Schema service for product type schema.
	 *
	 * @var Product_Type_Service
	 */
	private $schema_service;

	/**
	 * Mapping handler for field mappings.
	 *
	 * @var Mapping_Handler
	 */
	private $mapping_handler;

	/**
	 * Default meta service for default meta values.
	 *
	 * @var Default_Meta_Service
	 */
	private $meta_service;

	/**
	 * Logger for logging.
	 *
	 * @var Logger
	 */
	private $logger;

	/**
	 * Holds the channel UUID during build so helpers can access it.
	 *
	 * @var string
	 */
	private $current_channel_uuid = '';

	/**
	 * Variation context for building variation-specific payloads.
	 *
	 * Contains:
	 * - is_variation: bool - Whether current product is a variation.
	 * - is_variable: bool - Whether current product is a variable (parent).
	 * - parent_sku: string - Parent SKU (for variations).
	 * - parent_id: int - Parent product ID (for variations).
	 *
	 * @var array<string,mixed>
	 */
	private array $variation_context = array();

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->schema_service       = new Product_Type_Service();
		$this->mapping_handler      = new Mapping_Handler();
		$this->meta_service         = new Default_Meta_Service();
		$this->logger               = new Logger( 'payload-builder' );
	}

	/**
	 * Build schema-compliant payload for a product.
	 *
	 * @param int        $product_id     WooCommerce product ID.
	 * @param string     $channel_uuid   Channel UUID.
	 * @param string     $product_type   Amazon product-type name.
	 * @param string     $marketplace_id Marketplace ID (e.g. A21TJRUUN4KGV).
	 * @param string     $asin          Whether this is an offer for existing ASIN (true) or new catalog item (false).
	 * @param array|null $template_config Pre-resolved template configuration (optimization to avoid recalculation).
	 * @return array<string,mixed>   Payload ready to json_encode().
	 * @throws \RuntimeException When mapping is incomplete or validation fails.
	 */
	public function build( int $product_id, string $channel_uuid, string $product_type, string $marketplace_id, string $asin = '', ?array $template_config = null ): array {
		// Stash channel UUID for helper usage.
		$this->current_channel_uuid = $channel_uuid;
		// 1. Parse schema
		$schema = $this->schema_service->get_product_type_schema( $channel_uuid, $product_type, $marketplace_id );
		if ( empty( $schema ) ) {
			throw new \RuntimeException( esc_html( 'Unable to retrieve schema for product-type ' . $product_type ) );
		}

		// Extract field mappings from resolved template or fallback to defaults.
		$field_mapping = array();
		if ( $template_config ) {
			// Use template-specific field mappings (already includes defaults merged in template).
			$field_mapping = $template_config['field_mappings'] ?? array();
		} else {
			// Fallback to default mappings only when no template matches.
			$this->logger->warning(
				'No template configuration found for product, using default mappings only',
				array(
					'product_id'   => $product_id,
					'channel_uuid' => $channel_uuid,
				)
			);
			$mappings_row    = $this->mapping_handler->get_unified_mapping( Plugin_Constants::AMAZON, $channel_uuid );
			$default_mapping = $mappings_row['default_mappings'] ?? array();
			if ( is_string( $default_mapping ) ) {
				$default_mapping = json_decode( $default_mapping, true ) ?? array();
			}
			$field_mapping = $default_mapping;
		}

		// 3. WooCommerce product instance
		$wc_product = wc_get_product( $product_id );
		if ( ! $wc_product ) {
			throw new \RuntimeException( esc_html( 'Invalid WooCommerce product ID ' . $product_id ) );
		}

		// 4. Extract defaults from schema only.
		$final_marketplace_id = $schema['$defs']['marketplace_id']['default'] ?? $marketplace_id;
		$final_language_tag   = $schema['$defs']['language_tag']['default'] ?? 'en_IN';

		// 5. Initialize Amazon SP-API payload structure.
		$payload    = array(
			'productType'  => $product_type,
			'requirements' => 'LISTING',
			'attributes'   => array(),
		);
		$attributes = &$payload['attributes'];

		// 6. Traverse schema properties and build initial attributes.
		$attributes = $this->build_mapped_attributes(
			$schema,
			$field_mapping,
			$wc_product,
			$final_marketplace_id,
			$final_language_tag
		);

		if ( $asin ) {
			$this->add_merchant_suggested_asin( $attributes, $wc_product, $final_marketplace_id, $asin );
		}

		// Final pass: apply fallbacks for required fields whose mappings may be absent.
		$this->apply_item_name_brand_and_contact_fallbacks( $attributes, $wc_product, $final_marketplace_id, $final_language_tag, $product_id );

		return $payload;
	}

	/**
	 * Build partial payload with only selected WooCommerce attributes.
	 *
	 * This method builds a PARTIAL_UPDATE payload containing only the specified
	 * WooCommerce attributes, making it efficient for scheduled sync and selective updates.
	 *
	 * @param int        $product_id          WooCommerce product ID.
	 * @param string     $channel_uuid        Channel UUID.
	 * @param string     $product_type        Amazon product-type name.
	 * @param string     $marketplace_id      Marketplace ID (e.g. A21TJRUUN4KGV).
	 * @param string     $asin                ASIN for PARTIAL_UPDATE.
	 * @param array      $selected_attributes Array of WooCommerce attribute names to include (e.g., ['pa_model-number', '_brand']).
	 * @param array|null $template_config     Pre-resolved template configuration.
	 * @return array<string,mixed> Partial payload ready to json_encode().
	 * @throws \RuntimeException When mapping is incomplete or validation fails.
	 */
	public function build_partial( int $product_id, string $channel_uuid, string $product_type, string $marketplace_id, string $asin = '', array $selected_attributes = array(), ?array $template_config = null ): array {
		// Stash channel UUID for helper usage.
		$this->current_channel_uuid = $channel_uuid;

		// 1. Parse schema.
		$schema = $this->schema_service->get_product_type_schema( $channel_uuid, $product_type, $marketplace_id );
		if ( empty( $schema ) ) {
			throw new \RuntimeException( esc_html( 'Unable to retrieve schema for product-type ' . $product_type ) );
		}

		// 2. Extract field mappings from resolved template or fallback to defaults.
		$field_mapping = array();
		if ( $template_config ) {
			$field_mapping = $template_config['field_mappings'] ?? array();
		} else {
			$this->logger->warning(
				'No template configuration found for product, using default mappings only',
				array(
					'product_id'   => $product_id,
					'channel_uuid' => $channel_uuid,
				)
			);
			$mappings_row    = $this->mapping_handler->get_unified_mapping( Plugin_Constants::AMAZON, $channel_uuid );
			$default_mapping = $mappings_row['default_mappings'] ?? array();
			if ( is_string( $default_mapping ) ) {
				$default_mapping = json_decode( $default_mapping, true ) ?? array();
			}
			$field_mapping = $default_mapping;
		}

		// 3. WooCommerce product instance.
		$wc_product = wc_get_product( $product_id );
		if ( ! $wc_product ) {
			throw new \RuntimeException( esc_html( 'Invalid WooCommerce product ID ' . $product_id ) );
		}

		// 4. Extract defaults from schema.
		$final_marketplace_id = $schema['$defs']['marketplace_id']['default'] ?? $marketplace_id;
		$final_language_tag   = $schema['$defs']['language_tag']['default'] ?? 'en_IN';

		// 5. Initialize partial payload structure.
		$payload    = array(
			'productType'  => $product_type,
			'requirements' => 'LISTING_OFFER_ONLY',
			'attributes'   => array(),
		);
		$attributes = &$payload['attributes'];

		// 6. Build reverse mapping: WooCommerce attribute -> Amazon field(s).
		$reverse_mapping = $this->build_reverse_mapping( $field_mapping );

		$this->logger->info(
			'Building partial payload for selected attributes',
			array(
				'product_id'          => $product_id,
				'selected_attributes' => $selected_attributes,
				'mapping_count'       => count( $reverse_mapping ),
			)
		);

		$amazon_fields_to_build = array();

		// 7. Process only selected WooCommerce attributes.
		foreach ( $selected_attributes as $wc_attribute ) {
			$amazon_fields = $reverse_mapping[ $wc_attribute ] ?? array();

			if ( empty( $amazon_fields ) ) {
				$this->logger->warning(
					'Selected attribute not found in field mappings',
					array(
						'wc_attribute' => $wc_attribute,
						'product_id'   => $product_id,
					)
				);
				continue;
			}

			foreach ( $amazon_fields as $amazon_field ) {
				if ( ! isset( $schema['properties'][ $amazon_field ] ) ) {
					$this->logger->warning(
						'Amazon field not found in schema',
						array(
							'amazon_field' => $amazon_field,
							'wc_attribute' => $wc_attribute,
							'product_id'   => $product_id,
						)
					);
					continue;
				}

				$amazon_fields_to_build[ $amazon_field ] = true;
			}
		}

		$attributes = $this->build_mapped_attributes(
			$schema,
			$field_mapping,
			$wc_product,
			$final_marketplace_id,
			$final_language_tag,
			array_keys( $amazon_fields_to_build )
		);

		// 8. Always add merchant_suggested_asin for PARTIAL_UPDATE.
		if ( $asin ) {
			$this->add_merchant_suggested_asin( $attributes, $wc_product, $final_marketplace_id, $asin );
		}

		$this->logger->info(
			'Partial payload built successfully',
			array(
				'product_id'       => $product_id,
				'attributes_built' => count( $attributes ),
			)
		);

		return $payload;
	}

	/**
	 * Build reverse mapping from field_mappings (Amazon -> WooCommerce to WooCommerce -> Amazon[]).
	 *
	 * @param array $field_mapping Field mappings from template config.
	 * @return array Reverse mapping: ['wc_attribute' => ['amazon_field1', 'amazon_field2']].
	 */
	private function build_reverse_mapping( array $field_mapping ): array {
		$reverse = array();

		foreach ( $field_mapping as $amazon_field => $mapping_data ) {
			$root_amazon_field = is_string( $amazon_field ) ? explode( '.', $amazon_field )[0] : $amazon_field;
			$this->collect_reverse_mapping_sources( $mapping_data, (string) $root_amazon_field, $reverse );
		}

		return $reverse;
	}

	/**
	 * Build mapped attributes from schema and saved field mappings.
	 *
	 * @param array       $schema         Root schema document.
	 * @param array       $field_mapping  Saved field mappings.
	 * @param \WC_Product $wc_product     WooCommerce product.
	 * @param string      $marketplace_id Marketplace ID.
	 * @param string      $language_tag   Language tag.
	 * @param array|null  $allowed_fields Optional list of Amazon fields to build.
	 * @return array<string,mixed>
	 */
	private function build_mapped_attributes(
		array $schema,
		array $field_mapping,
		\WC_Product $wc_product,
		string $marketplace_id,
		string $language_tag,
		?array $allowed_fields = null
	): array {
		$attributes        = array();
		$allowed_field_set = null;

		if ( is_array( $allowed_fields ) ) {
			$allowed_field_set = array_fill_keys( $allowed_fields, true );
		}

		if ( ! isset( $schema['properties'] ) || ! is_array( $schema['properties'] ) ) {
			return $attributes;
		}

		foreach ( $schema['properties'] as $field => $prop ) {
			if ( null !== $allowed_field_set && ! isset( $allowed_field_set[ $field ] ) ) {
				continue;
			}

			if ( $this->is_special_field( $field ) ) {
				$special_value = $this->extract_special_field_value( $field, $wc_product, $marketplace_id, $language_tag );
				if ( null !== $special_value ) {
					$attributes[ $field ] = $special_value;
				}
				continue;
			}

			$mapping_node = $this->get_mapping_node_for_field( $field_mapping, $field );
			if ( null === $mapping_node ) {
				continue;
			}

			$built_value = $this->build_payload_node_from_mapping(
				$mapping_node,
				$prop,
				$schema,
				$wc_product,
				$marketplace_id,
				$language_tag
			);

			if ( $this->has_payload_value( $built_value ) ) {
				$attributes[ $field ] = $built_value;
			}
		}

		return $attributes;
	}

	/**
	 * Get the saved mapping node for an Amazon field.
	 *
	 * Supports both direct top-level mappings and legacy dot-notation mappings.
	 *
	 * @param array  $field_mapping Saved field mappings.
	 * @param string $field         Amazon field name.
	 * @return mixed|null
	 */
	private function get_mapping_node_for_field( array $field_mapping, string $field ) {
		if ( array_key_exists( $field, $field_mapping ) ) {
			return $field_mapping[ $field ];
		}

		$nested_mapping = array();
		$nested_prefix  = $field . '.';

		foreach ( $field_mapping as $mapped_field => $mapping_data ) {
			if ( ! is_string( $mapped_field ) || 0 !== strpos( $mapped_field, $nested_prefix ) ) {
				continue;
			}

			$relative_path = substr( $mapped_field, strlen( $nested_prefix ) );
			if ( '' === $relative_path ) {
				continue;
			}

			$this->assign_by_path( $nested_mapping, $relative_path, $mapping_data );
		}

		return array() === $nested_mapping ? null : $nested_mapping;
	}

	/**
	 * Build one payload node by walking the saved mapping and schema together.
	 *
	 * @param mixed       $mapping_node   Saved mapping node.
	 * @param array       $schema_node    Matching schema node.
	 * @param array       $root_schema    Root schema document.
	 * @param \WC_Product $wc_product     WooCommerce product.
	 * @param string      $marketplace_id Marketplace ID.
	 * @param string      $language_tag   Language tag.
	 * @return mixed
	 */
	private function build_payload_node_from_mapping(
		$mapping_node,
		array $schema_node,
		array $root_schema,
		\WC_Product $wc_product,
		string $marketplace_id,
		string $language_tag
	) {
		$schema_node = $this->dereference_json_schema_fragment( $root_schema, $schema_node );
		$schema_type = $schema_node['type'] ?? '';

		if ( 'array' === $schema_type ) {
			$item_schema = $this->dereference_json_schema_fragment( $root_schema, $schema_node['items'] ?? array() );
			$items       = array();

			if ( is_array( $mapping_node ) && $this->is_list_array( $mapping_node ) ) {
				foreach ( $mapping_node as $mapping_item ) {
					$item_value = $this->build_payload_node_from_mapping(
						$mapping_item,
						$item_schema,
						$root_schema,
						$wc_product,
						$marketplace_id,
						$language_tag
					);

					if ( ! $this->has_payload_value( $item_value ) ) {
						continue;
					}

					if ( is_array( $item_value ) ) {
						$this->add_required_selectors( $item_value, $schema_node, $marketplace_id, $language_tag );
					}

					$items[] = $item_value;
				}
			} else {
				$item_value = $this->build_payload_node_from_mapping(
					$mapping_node,
					$item_schema,
					$root_schema,
					$wc_product,
					$marketplace_id,
					$language_tag
				);

				if ( ! $this->has_payload_value( $item_value ) ) {
					return null;
				}

				if ( is_array( $item_value ) ) {
					$this->add_required_selectors( $item_value, $schema_node, $marketplace_id, $language_tag );
				}

				$items[] = $item_value;
			}

			return array() === $items ? null : $items;
		}

		if ( 'object' === $schema_type || isset( $schema_node['properties'] ) ) {
			$result = array();

			foreach ( $schema_node['properties'] ?? array() as $child_name => $child_schema ) {
				$child_mapping_exists = false;
				$child_mapping        = null;

				if ( is_array( $mapping_node ) && array_key_exists( $child_name, $mapping_node ) ) {
					$child_mapping_exists = true;
					$child_mapping        = $mapping_node[ $child_name ];
				} elseif ( 'value' === $child_name && ! is_array( $mapping_node ) ) {
					$child_mapping_exists = true;
					$child_mapping        = $mapping_node;
				}

				if ( ! $child_mapping_exists ) {
					continue;
				}

				$child_value = $this->build_payload_node_from_mapping(
					$child_mapping,
					$child_schema,
					$root_schema,
					$wc_product,
					$marketplace_id,
					$language_tag
				);

				if ( $this->has_payload_value( $child_value ) ) {
					$result[ $child_name ] = $child_value;
				}
			}

			if ( array() === $result ) {
				return null;
			}

			$this->add_required_selectors_to_object( $result, $schema_node, $marketplace_id, $language_tag );

			if ( ! $this->is_complete_object_payload( $result, $schema_node ) ) {
				return null;
			}

			return $result;
		}

		return $this->resolve_scalar_mapping_value( $mapping_node, $schema_node, $wc_product );
	}

	/**
	 * Resolve one scalar leaf from a saved mapping node.
	 *
	 * @param mixed       $mapping_node Saved mapping node.
	 * @param array       $schema_node  Matching schema node.
	 * @param \WC_Product $wc_product   WooCommerce product.
	 * @return mixed|null
	 */
	private function resolve_scalar_mapping_value( $mapping_node, array $schema_node, \WC_Product $wc_product ) {
		$raw_value = null;

		if ( is_array( $mapping_node ) ) {
			if ( isset( $mapping_node['wc_source'] ) && is_string( $mapping_node['wc_source'] ) ) {
				$raw_value = $this->extract_wc_value( $wc_product, $mapping_node['wc_source'] );
			} elseif ( array_key_exists( 'value', $mapping_node ) && ! is_array( $mapping_node['value'] ) ) {
				$value = $mapping_node['value'];
				if ( is_string( $value ) ) {
					$raw_value = $this->extract_wc_value( $wc_product, $value );
				} else {
					$raw_value = $value;
				}
			}
		} elseif ( is_string( $mapping_node ) ) {
			$raw_value = $this->extract_wc_value( $wc_product, $mapping_node );
		} elseif ( is_bool( $mapping_node ) || is_int( $mapping_node ) || is_float( $mapping_node ) ) {
			$raw_value = $mapping_node;
		}

		if ( null === $raw_value ) {
			return null;
		}

		if ( is_string( $raw_value ) && '' === trim( $raw_value ) ) {
			return null;
		}

		return $this->cast_value_by_schema( $raw_value, $schema_node );
	}

	/**
	 * Collect reverse mapping entries from nested mapping nodes.
	 *
	 * @param mixed  $mapping_data Saved mapping node.
	 * @param string $amazon_field Root Amazon field.
	 * @param array  &$reverse     Reverse mapping accumulator.
	 * @return void
	 */
	private function collect_reverse_mapping_sources( $mapping_data, string $amazon_field, array &$reverse ): void {
		if ( is_string( $mapping_data ) ) {
			$this->register_reverse_mapping_source( $mapping_data, $amazon_field, $reverse );
			return;
		}

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

		if ( isset( $mapping_data['wc_source'] ) && is_string( $mapping_data['wc_source'] ) ) {
			$this->register_reverse_mapping_source( $mapping_data['wc_source'], $amazon_field, $reverse );
		}

		if ( array_key_exists( 'value', $mapping_data ) && ! is_array( $mapping_data['value'] ) && is_string( $mapping_data['value'] ) ) {
			$this->register_reverse_mapping_source( $mapping_data['value'], $amazon_field, $reverse );
		}

		foreach ( $mapping_data as $child_key => $child_mapping ) {
			if ( 'wc_source' === $child_key ) {
				continue;
			}

			if ( 'value' === $child_key && ! is_array( $child_mapping ) ) {
				continue;
			}

			$this->collect_reverse_mapping_sources( $child_mapping, $amazon_field, $reverse );
		}
	}

	/**
	 * Add a WooCommerce source to the reverse mapping if it is a real source.
	 *
	 * @param string $wc_source    Candidate WooCommerce source.
	 * @param string $amazon_field Root Amazon field.
	 * @param array  &$reverse     Reverse mapping accumulator.
	 * @return void
	 */
	private function register_reverse_mapping_source( string $wc_source, string $amazon_field, array &$reverse ): void {
		if ( ! $this->is_wc_source_mapping_string( $wc_source ) ) {
			return;
		}

		if ( ! isset( $reverse[ $wc_source ] ) ) {
			$reverse[ $wc_source ] = array();
		}

		if ( ! in_array( $amazon_field, $reverse[ $wc_source ], true ) ) {
			$reverse[ $wc_source ][] = $amazon_field;
		}
	}

	/**
	 * Determine whether a string represents a WooCommerce source.
	 *
	 * Literal strings such as enum values should not be indexed for partial
	 * payload lookups.
	 *
	 * @param string $value Candidate mapping string.
	 * @return bool
	 */
	private function is_wc_source_mapping_string( string $value ): bool {
		$value = trim( $value );
		if ( '' === $value ) {
			return false;
		}

		if ( 0 === strpos( $value, 'pa_' ) || 0 === strpos( $value, '_' ) ) {
			return true;
		}

		return in_array(
			$value,
			array(
				'post_title',
				'post_content',
				'post_excerpt',
				'short_description',
				'price',
				'_regular_price',
				'_sale_price',
				'quantity',
				'brand',
				'title',
				'product_brand',
				'_thumbnail_id',
			),
			true
		);
	}

	/**
	 * Whether a built payload node contains meaningful data.
	 *
	 * @param mixed $value Built payload node.
	 * @return bool
	 */
	private function has_payload_value( $value ): bool {
		if ( null === $value ) {
			return false;
		}

		if ( is_string( $value ) ) {
			return '' !== trim( $value );
		}

		if ( is_array( $value ) ) {
			foreach ( $value as $child_value ) {
				if ( $this->has_payload_value( $child_value ) ) {
					return true;
				}
			}

			return false;
		}

		return true;
	}

	/**
	 * Check whether an object payload has the minimum data required to submit.
	 *
	 * Objects that expose a `value` property are treated as value-centric nodes.
	 * If they do not resolve a `value`, partial literals like `unit` should not
	 * be emitted because Amazon will reject them as incomplete.
	 *
	 * @param array $payload_object Built object payload.
	 * @param array $schema_node    Matching schema node.
	 * @return bool
	 */
	private function is_complete_object_payload( array $payload_object, array $schema_node ): bool {
		$properties = $schema_node['properties'] ?? array();

		if ( isset( $properties['value'] ) && ! array_key_exists( 'value', $payload_object ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine whether an array is a sequential list.
	 *
	 * @param array $value Array to inspect.
	 * @return bool
	 */
	private function is_list_array( array $value ): bool {
		$expected_index = 0;

		foreach ( array_keys( $value ) as $key ) {
			if ( $expected_index !== $key ) {
				return false;
			}

			++$expected_index;
		}

		return true;
	}

	/**
	 * Extract WooCommerce source from a field mapping value.
	 *
	 * Handles multiple formats:
	 * - Flat string: "pa_taxonomy" (legacy)
	 * - Nested with 'value' key: {"value": "pa_taxonomy"} (auto-mapper format)
	 * - Nested with 'wc_source' key: {"wc_source": "pa_taxonomy"} (manual mapping format)
	 *
	 * @param mixed $mapping_data The field mapping value (string or array).
	 * @return string The WooCommerce source attribute/meta key, or empty string if not found.
	 */
	private function extract_wc_source_from_mapping( $mapping_data ): string {
		if ( is_string( $mapping_data ) ) {
			return $mapping_data;
		}

		if ( is_array( $mapping_data ) ) {
			// Check for 'value' key first (auto-mapper nested format).
			if ( isset( $mapping_data['value'] ) && is_string( $mapping_data['value'] ) ) {
				return $mapping_data['value'];
			}

			// Check for 'wc_source' key (manual mapping format).
			if ( isset( $mapping_data['wc_source'] ) && is_string( $mapping_data['wc_source'] ) ) {
				return $mapping_data['wc_source'];
			}
		}

		return '';
	}

	/**
	 * Helpers.
	 */

	/**
	 * Extract WooCommerce value from product.
	 *
	 * @param \WC_Product $product WooCommerce product.
	 * @param string      $source Source field name.
	 * @return mixed WooCommerce value or null if not found.
	 */
	private function extract_wc_value( \WC_Product $product, string $source ) {
		// Handle global attributes with pa_ prefix.
		if ( 0 === strpos( $source, 'pa_' ) ) {
			// For variations, get the specific attribute value for this variation.
			if ( $this->is_building_variation() && $product instanceof \WC_Product_Variation ) {
				$this->logger->debug(
					'Extracting variation attribute',
					array(
						'source'            => $source,
						'product_id'        => $product->get_id(),
						'variation_context' => $this->variation_context,
						'attributes'        => $product->get_attributes(),
					)
				);

				$variation_value = Variation_Helper::get_variation_attribute_value( $product, $source );

				$this->logger->debug(
					'Variation attribute extraction result',
					array(
						'source'           => $source,
						'product_id'       => $product->get_id(),
						'extracted_value'  => $variation_value,
					)
				);

				if ( null !== $variation_value && '' !== $variation_value ) {
					return $variation_value;
				}
			}

			// For variable (parent) products, skip variation attributes entirely.
			// Variation attributes (color, size, etc.) should only appear on child products.
			if ( $this->is_building_variable() && $product instanceof \WC_Product_Variable ) {
				if ( $this->is_variation_attribute( $product, $source ) ) {
					$this->logger->debug(
						'Skipping variation attribute for parent product',
						array(
							'source'     => $source,
							'product_id' => $product->get_id(),
						)
					);
					return null;
				}
			}

			return $product->get_attribute( $source ) ?? null;
		}

		// Attribute or meta key.
		if ( 0 === strpos( $source, '_' ) && '_thumbnail_id' !== $source && '_regular_price' !== $source && '_sale_price' !== $source ) {
			return get_post_meta( $product->get_id(), $source, true );
		}

		switch ( $source ) {
			case 'post_title':
				return $product->get_title();
			case 'post_content':
				return $product->get_description();
			case 'post_excerpt':
			case 'short_description':
				return $product->get_short_description();
			case 'price':
				return $this->apply_price_markup( $product->get_price(), $this->current_channel_uuid );
			case '_regular_price':
				return $this->apply_price_markup( $product->get_regular_price(), $this->current_channel_uuid );
			case '_sale_price':
				return $this->apply_price_markup( $product->get_sale_price(), $this->current_channel_uuid );
			case 'quantity':
				return $product->get_stock_quantity();
			case 'brand':
				// Templates often map Amazon "brand" to WC source "brand" (not a meta key).
				$from_meta = get_post_meta( $product->get_id(), '_brand', true );
				if ( is_string( $from_meta ) && '' !== trim( $from_meta ) ) {
					return trim( $from_meta );
				}
				$from_attr = $product->get_attribute( 'brand' );
				if ( is_string( $from_attr ) && '' !== trim( $from_attr ) ) {
					return trim( $from_attr );
				}
				$terms = get_the_terms( $product->get_id(), 'product_brand' );
				if ( $terms && ! is_wp_error( $terms ) ) {
					return $terms[0]->name;
				}
				return '';
			case 'title':
				return $product->get_title();
			case 'product_brand':
				$brand = get_the_terms( $product->get_id(), 'product_brand' );
				if ( $brand && ! is_wp_error( $brand ) ) {
					return $brand[0]->name;
				}
				return '';
			case '_thumbnail_id':
				$thumbnail_id = $product->get_image_id();
				if ( $thumbnail_id ) {
					return wp_get_attachment_url( $thumbnail_id );
				}
				return null;
			default:
				return $source;
		}
	}

	/**
	 * Check if a field requires special handling.
	 *
	 * @param string $field_name Field name.
	 * @return bool True if field needs special handling.
	 */
	private function is_special_field( string $field_name ): bool {
		return in_array(
			$field_name,
			array(
				'externally_assigned_product_identifier',
				'condition_type',
				'bullet_point',
			),
			true
		);
	}

	/**
	 * Extract special field values with proper structure.
	 *
	 * @param string      $field_name Field name.
	 * @param \WC_Product $product WooCommerce product.
	 * @param string      $marketplace_id Marketplace ID.
	 * @param string      $language_tag Language tag.
	 * @return array|null Field value or null if not available.
	 */
	private function extract_special_field_value( string $field_name, \WC_Product $product, string $marketplace_id, string $language_tag ): ?array {
		switch ( $field_name ) {
			case 'externally_assigned_product_identifier':
				return $this->build_external_product_id( $product, $marketplace_id );
			case 'condition_type':
				return $this->build_condition_type( $product, $marketplace_id );
			case 'bullet_point':
				return $this->build_bullet_point( $product, $language_tag );
			default:
				return null;
		}
	}

	/**
	 * Build bullet point.
	 *
	 * @param \WC_Product $product WooCommerce product.
	 * @param string      $language_tag Language tag.
	 * @return array|null Bullet point or null if not available.
	 */
	private function build_bullet_point( \WC_Product $product, string $language_tag ): ?array {
		$bullet_points = array();
		for ( $i = 1; $i <= 5; $i++ ) {
			$bullet_point = get_post_meta( $product->get_id(), '_momcs_amz_bullet_point_' . strval( $i ), true );
			if ( ! empty( $bullet_point ) ) {
				$bullet_points[] = array(
					'value'        => $bullet_point,
					'language_tag' => $language_tag,
				);
			}
		}
		return $bullet_points;
	}

	/**
	 * Process hierarchical field mappings with unlimited depth support.
	 *
	 * @param array       &$attributes Reference to attributes array.
	 * @param string      $field_name Field name.
	 * @param array       $mapping_data Hierarchical mapping data.
	 * @param \WC_Product $wc_product WooCommerce product.
	 * @param array       $field_schema Field schema definition.
	 * @param string      $marketplace_id Marketplace ID.
	 * @param string      $language_tag Language tag.
	 */
	private function process_hierarchical_field_mapping( array &$attributes, string $field_name, array $mapping_data, \WC_Product $wc_product, array $field_schema, string $marketplace_id, string $language_tag ): void {
		// Build nested structure from mapping data with schema-aware type casting.
		// Pass $field_name so nested builders can resolve units / companion objects correctly.
		$nested_result = $this->build_nested_structure_with_schema( $mapping_data, $wc_product, $field_schema, $marketplace_id, $language_tag, $field_name );

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

		// Handle array fields with proper structure.
		if ( isset( $field_schema['type'] ) && 'array' === $field_schema['type'] ) {
			$item = $nested_result;

			// Add selectors and required fields (language_tag, marketplace_id).
			$this->add_required_selectors( $item, $field_schema, $marketplace_id, $language_tag );

			$attributes[ $field_name ] = array( $item );
		} else {
			$attributes[ $field_name ] = $nested_result;
		}
	}

	/**
	 * Recursively build nested structure from hierarchical mappings with schema-aware type casting.
	 *
	 * @param array       $mappings          Hierarchical mapping data.
	 * @param \WC_Product $wc_product        WooCommerce product.
	 * @param array       $parent_schema     Parent field schema for navigation.
	 * @param string      $marketplace_id    Marketplace ID.
	 * @param string      $language_tag      Language tag.
	 * @param string      $amazon_field_name Top-level Amazon field name (e.g. 'item_dimensions'), used for unit resolution.
	 * @return array Built nested structure.
	 */
	private function build_nested_structure_with_schema( array $mappings, \WC_Product $wc_product, array $parent_schema, string $marketplace_id = '', string $language_tag = '', string $amazon_field_name = '' ): array {
		$result = array();

		foreach ( $mappings as $field_name => $field_data ) {
			// Check if this is a nested object (has child fields, not a leaf mapping).
			// Skip if it has 'value' or 'wc_source' keys - those are leaf mappings.
			if ( is_array( $field_data ) && ! isset( $field_data['wc_source'] ) && ! isset( $field_data['value'] ) && ! is_numeric( key( $field_data ) ) ) {
				// This is a nested object - get the schema for this field and recurse deeper.
				$field_schema  = $this->get_nested_field_schema( $parent_schema, $field_name );
				$nested_result = $this->build_nested_structure_with_schema( $field_data, $wc_product, $field_schema, $marketplace_id, $language_tag, $amazon_field_name );
				if ( ! empty( $nested_result ) ) {
					// Check if this field is an array type or object type.
					$field_type = $field_schema['type'] ?? 'object';
					if ( 'array' === $field_type ) {
						$result[ $field_name ] = array( $nested_result );
					} else {
						// For object type, don't wrap in array but add required selectors if needed.
						$this->add_required_selectors_to_object( $nested_result, $field_schema, $marketplace_id, $language_tag );
						$result[ $field_name ] = $nested_result;
					}
				}
			} else {
				// This is a leaf field mapping - get the exact field schema and apply type casting.
				$wc_source = $this->extract_wc_source_from_mapping( $field_data );
				if ( '' !== $wc_source ) {
					$raw_value = $this->extract_wc_value( $wc_product, $wc_source );

					// Debug logging for nested field extraction (especially for pa_* attributes).
					if ( $this->is_building_variation() && 0 === strpos( $wc_source, 'pa_' ) ) {
						$this->logger->debug(
							'Nested field extraction for variation attribute',
							array(
								'field_name'  => $field_name,
								'wc_source'   => $wc_source,
								'product_id'  => $wc_product->get_id(),
								'raw_value'   => $raw_value,
							)
						);
					}

					if ( null !== $raw_value && '' !== $raw_value ) {
						$field_schema = $this->get_nested_field_schema( $parent_schema, $field_name );

						// When the schema says this leaf is a measurement object {value, unit},
						// build the correct shape instead of casting to a bare scalar.
						if ( $this->is_measurement_schema( $field_schema ) ) {
							$numeric = is_numeric( $raw_value )
								? (float) $raw_value
								: (float) filter_var( $raw_value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION );

							// Honour an explicit unit literal in the mapping: {"value": "_length", "unit": "inches"}.
							$unit_literal = '';
							if ( is_array( $field_data ) && isset( $field_data['unit'] ) && is_string( $field_data['unit'] ) ) {
								$unit_literal = strtolower( trim( $field_data['unit'] ) );
							}
							if ( '' === $unit_literal ) {
								$unit_literal = $this->resolve_unit_from_wc_settings( $amazon_field_name, $wc_product );
							}

							$result[ $field_name ] = array(
								'value' => $numeric,
								'unit'  => $unit_literal,
							);
						} else {
							$result[ $field_name ] = $this->cast_value_by_schema( $raw_value, $field_schema );
						}
					}
				}
			}
		}

		return $result;
	}

	/**
	 * Cast a value based on schema type requirements.
	 *
	 * @param mixed $value Raw value from WooCommerce.
	 * @param array $schema Field schema definition.
	 * @return mixed Type-cast value.
	 */
	private function cast_value_by_schema( $value, array $schema ) {
		$type = $schema['type'] ?? 'string';

		switch ( $type ) {
			case 'array':
				// For array types, wrap single values in array if they aren't already arrays.
				if ( ! is_array( $value ) ) {
					return array( $value );
				}
				return $value;

			case 'number':
			case 'integer':
				// Convert to numeric - handle both int and float.
				if ( is_numeric( $value ) ) {
					return 'integer' === $type ? (int) $value : (float) $value;
				}
				// If not numeric, try to extract numeric from string.
				if ( is_string( $value ) ) {
					$numeric = filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION );
					if ( is_numeric( $numeric ) ) {
						return 'integer' === $type ? (int) $numeric : (float) $numeric;
					}
				}
				return 'integer' === $type ? 0 : 0.0;

			case 'boolean':
				// Convert to boolean.
				if ( is_bool( $value ) ) {
					return $value;
				}
				if ( is_string( $value ) ) {
					return in_array( strtolower( $value ), array( 'true', '1', 'yes', 'on' ), true );
				}
				return (bool) $value;

			case 'string':
			default:
				// Convert to string.
				if ( is_array( $value ) || is_object( $value ) ) {
					return wp_json_encode( $value );
				}
				$str_val = (string) $value;

				// Schema pattern constraint: if the pattern only allows digits, strip non-numeric chars.
				// This handles fields like recommended_browse_nodes whose schema has \A[0-9]*\z.
				if ( ! empty( $schema['pattern'] ) && is_string( $schema['pattern'] ) ) {
					$pat = $schema['pattern'];
					if ( preg_match( '/\[0-9\]/', $pat ) && ! preg_match( '/\[a-z\]/i', $pat ) ) {
						$digits = preg_replace( '/\D/', '', $str_val );
						if ( '' !== $digits ) {
							return $digits;
						}
					}
				}

				return $str_val;
		}
	}

	/**
	 * Get nested field schema for type casting by traversing deep schema structure.
	 *
	 * @param array  $schema Parent schema.
	 * @param string $field_name Field name to look up.
	 * @return array Field schema or empty array.
	 */
	private function get_nested_field_schema( array $schema, string $field_name ): array {
		// First check direct properties.
		if ( isset( $schema['properties'][ $field_name ] ) ) {
			return $schema['properties'][ $field_name ];
		}

		// For array types, look in items.properties.
		if ( isset( $schema['type'] ) && 'array' === $schema['type'] && isset( $schema['items']['properties'][ $field_name ] ) ) {
			return $schema['items']['properties'][ $field_name ];
		}

		// For deeply nested arrays, traverse through multiple levels.
		if ( isset( $schema['items']['properties'] ) ) {
			foreach ( $schema['items']['properties'] as $prop_name => $prop_schema ) {
				if ( $prop_name === $field_name ) {
					return $prop_schema;
				}

				// Recurse into nested array/object structures.
				if ( isset( $prop_schema['type'] ) && 'array' === $prop_schema['type'] && isset( $prop_schema['items']['properties'] ) ) {
					$nested_result = $this->get_nested_field_schema( $prop_schema, $field_name );
					if ( ! empty( $nested_result ) ) {
						return $nested_result;
					}
				}

				if ( isset( $prop_schema['properties'] ) ) {
					$nested_result = $this->get_nested_field_schema( $prop_schema, $field_name );
					if ( ! empty( $nested_result ) ) {
						return $nested_result;
					}
				}
			}
		}

		// If not found, return empty schema (will default to string type).
		return array();
	}

	/**
	 * Add required selectors (language_tag, marketplace_id) to item based on schema.
	 *
	 * @param array  &$item Reference to item array to modify.
	 * @param array  $schema Field schema definition.
	 * @param string $marketplace_id Marketplace ID.
	 * @param string $language_tag Language tag.
	 */
	private function add_required_selectors( array &$item, array $schema, string $marketplace_id, string $language_tag ): void {
		// Check selectors array.
		$selectors = $schema['selectors'] ?? array();

		// Also check required array (both at field level and items level).
		$required       = $schema['required'] ?? array();
		$items_required = $schema['items']['required'] ?? array();

			// Combine all required fields.
		$all_required = array_merge( $selectors, $required, $items_required );

		// Add language_tag if required.
		if ( in_array( 'language_tag', $all_required, true ) && ! isset( $item['language_tag'] ) ) {
			$item['language_tag'] = $language_tag;
		}

		// Add marketplace_id if required.
		if ( in_array( 'marketplace_id', $all_required, true ) && ! isset( $item['marketplace_id'] ) ) {
			$item['marketplace_id'] = $marketplace_id;
		}
	}

	/**
	 * Add required selectors to nested object fields based on their schema.
	 *
	 * @param array  &$payload_object Reference to object to modify.
	 * @param array  $schema Object schema definition.
	 * @param string $marketplace_id Marketplace ID.
	 * @param string $language_tag Language tag.
	 */
	private function add_required_selectors_to_object( array &$payload_object, array $schema, string $marketplace_id, string $language_tag ): void {
		// Check if this object's schema requires language_tag or marketplace_id.
		$required     = $schema['required'] ?? array();
		$selectors    = $schema['selectors'] ?? array();
		$all_required = array_merge( $required, $selectors );

		// Add language_tag if required by the object schema.
		if ( in_array( 'language_tag', $all_required, true ) && ! empty( $language_tag ) && ! isset( $payload_object['language_tag'] ) ) {
			$payload_object['language_tag'] = $language_tag;
		}

		// Add marketplace_id if required by the object schema.
		if ( in_array( 'marketplace_id', $all_required, true ) && ! empty( $marketplace_id ) && ! isset( $payload_object['marketplace_id'] ) ) {
			$payload_object['marketplace_id'] = $marketplace_id;
		}
	}

	/**
	 * Add merchant_suggested_asin for offer creation on existing ASIN.
	 *
	 * @param array       &$attributes Reference to attributes array.
	 * @param \WC_Product $wc_product WooCommerce product.
	 * @param string      $marketplace_id Marketplace ID.
	 * @param string      $asin ASIN.
	 */
	private function add_merchant_suggested_asin( array &$attributes, \WC_Product $wc_product, string $marketplace_id, string $asin ): void {
		$attributes['merchant_suggested_asin'] = array(
			array(
				'value'          => $asin,
				'marketplace_id' => $marketplace_id,
			),
		);
	}

	/**
	 * Build external product identifier structure.
	 *
	 * @param \WC_Product $product WooCommerce product.
	 * @param string      $marketplace_id Marketplace ID.
	 * @return array|null External product ID structure.
	 */
	private function build_external_product_id( \WC_Product $product, string $marketplace_id ): ?array {
		$id_type  = get_post_meta( $product->get_id(), '_momcs_amz_id_type', true );
		$id_value = get_post_meta( $product->get_id(), '_momcs_amz_id_value', true );

		if ( empty( $id_type ) || empty( $id_value ) ) {
			return null;
		}

		$type  = strtolower( (string) $id_type );
		$value = (string) $id_value;

		// Amazon SP-API: GTIN/UPC/EAN values are numeric and length-capped (max 14 for GTIN family).
		if ( in_array( $type, array( 'gtin', 'upc', 'ean', 'ean-13', 'ean13', 'upc-a', 'upca' ), true ) ) {
			$digits = preg_replace( '/\D/', '', $value );
			if ( '' === $digits ) {
				return null;
			}
			if ( strlen( $digits ) > 14 ) {
				$digits = substr( $digits, -14 );
			}
			$value = $digits;
			if ( 'ean-13' === $type || 'ean13' === $type ) {
				$type = 'ean';
			}
			if ( 'upc-a' === $type || 'upca' === $type ) {
				$type = 'upc';
			}
		}

		return array(
			array(
				'type'           => $type,
				'value'          => $value,
				'marketplace_id' => $marketplace_id,
			),
		);
	}

	/**
	 * Build condition type structure.
	 *
	 * @param \WC_Product $product WooCommerce product.
	 * @param string      $marketplace_id Marketplace ID.
	 * @return array|null Condition type structure.
	 */
	private function build_condition_type( \WC_Product $product, string $marketplace_id ): ?array {
		$condition = get_post_meta( $product->get_id(), '_momcs_amz_condition', true );

		if ( empty( $condition ) ) {
			$condition = 'new_new'; // Default condition.
		}

		return array(
			array(
				'value'          => $condition,
				'marketplace_id' => $marketplace_id,
			),
		);
	}

	/**
	 * Whether a schema node represents a measurement object ({value, unit}).
	 *
	 * Used by build_nested_structure_with_schema to decide when to build a
	 * {value, unit} pair instead of a plain scalar for leaf fields such as
	 * item_dimensions[].length, item_dimensions[].width, etc.
	 *
	 * @param array $schema Schema node to inspect.
	 * @return bool
	 */
	private function is_measurement_schema( array $schema ): bool {
		if ( 'object' !== ( $schema['type'] ?? '' ) ) {
			return false;
		}
		$props = $schema['properties'] ?? array();
		return isset( $props['value'] ) && isset( $props['unit'] );
	}

	/**
	 * Resolve the Amazon unit string for a given field using WooCommerce store settings.
	 *
	 * Resolution order:
	 *   1. Product-level meta (_momcs_amz_weight_unit / _momcs_amz_dimension_unit).
	 *   2. WooCommerce store setting (woocommerce_weight_unit / woocommerce_dimension_unit).
	 *   3. Sensible default (kilograms / centimeters).
	 *
	 * @param string      $amazon_field Top-level Amazon attribute name (e.g. 'item_weight').
	 * @param \WC_Product $wc_product   WooCommerce product instance.
	 * @return string Resolved Amazon unit string.
	 */
	private function resolve_unit_from_wc_settings( string $amazon_field, \WC_Product $wc_product ): string {
		$pid = $wc_product->get_id();

		// Weight fields.
		if ( in_array( $amazon_field, array( 'item_weight', 'item_package_weight' ), true ) ) {
			$meta_unit = get_post_meta( $pid, '_momcs_amz_weight_unit', true );
			if ( is_string( $meta_unit ) && '' !== trim( $meta_unit ) ) {
				return strtolower( trim( $meta_unit ) );
			}
			$wc_unit  = get_option( 'woocommerce_weight_unit', 'kg' );
			$unit_map = array(
				'kg'  => 'kilograms',
				'g'   => 'grams',
				'lbs' => 'pounds',
				'oz'  => 'ounces',
			);
			return $unit_map[ $wc_unit ] ?? 'kilograms';
		}

		// Dimension fields (and anything else).
		$meta_unit = get_post_meta( $pid, '_momcs_amz_dimension_unit', true );
		if ( is_string( $meta_unit ) && '' !== trim( $meta_unit ) ) {
			return strtolower( trim( $meta_unit ) );
		}
		$wc_unit  = get_option( 'woocommerce_dimension_unit', 'cm' );
		$unit_map = array(
			'cm' => 'centimeters',
			'm'  => 'meters',
			'in' => 'inches',
			'ft' => 'feet',
			'mm' => 'millimeters',
			'yd' => 'yards',
		);
		return $unit_map[ $wc_unit ] ?? 'centimeters';
	}

	/**
	 * Add schema-required companion fields to an already-built array-item object.
	 *
	 * Handles two classes of companion fields without hardcoding field names:
	 *
	 *   1. Scalar 'unit' property  — resolved from WooCommerce store/product settings.
	 *   2. Nested enum object (e.g. unit_count[].type) — resolved by reading the
	 *      schema's allowed enum values and matching against product meta.
	 *      Meta key pattern: _momcs_amz_{amazon_field}_{prop_name}  (e.g. _momcs_amz_unit_count_type).
	 *
	 * @param array       $item           Item array being built (by ref).
	 * @param array       $prop           Schema for the parent array field.
	 * @param string      $amazon_field   Top-level Amazon attribute name.
	 * @param \WC_Product $wc_product     WooCommerce product.
	 * @param array       $root_schema    Root JSON schema document (for $ref resolution).
	 * @param string      $marketplace_id Marketplace ID.
	 * @param string      $language_tag   Language tag.
	 * @return void
	 */
	private function add_schema_companion_fields(
		array &$item,
		array $prop,
		string $amazon_field,
		\WC_Product $wc_product,
		array $root_schema,
		string $marketplace_id,
		string $language_tag
	): void {
		$items_props = $prop['items']['properties'] ?? array();
		if ( empty( $items_props ) ) {
			return;
		}

		$selector_keys = array( 'value', 'language_tag', 'marketplace_id' );

		foreach ( $items_props as $prop_name => $prop_schema ) {
			// Already set or is a standard value / selector.
			if ( isset( $item[ $prop_name ] ) || in_array( $prop_name, $selector_keys, true ) ) {
				continue;
			}

			// 'unit' property: resolve dynamically from WC store/product settings.
			if ( 'unit' === $prop_name ) {
				$unit = $this->resolve_unit_from_wc_settings( $amazon_field, $wc_product );
				if ( '' !== $unit ) {
					$item['unit'] = $unit;
				}
				continue;
			}

			// Nested object with a 'value' sub-property that has an enum (e.g. unit_count[].type).
			$resolved_prop = $this->dereference_json_schema_fragment( $root_schema, $prop_schema );
			if ( 'object' === ( $resolved_prop['type'] ?? '' ) ) {
				$companion = $this->build_companion_enum_object(
					$resolved_prop,
					$amazon_field,
					$prop_name,
					$wc_product,
					$root_schema,
					$language_tag
				);
				if ( null !== $companion ) {
					$item[ $prop_name ] = $companion;
				}
			}
		}
	}

	/**
	 * Build a companion enum-object property from schema + product meta.
	 *
	 * Looks up allowed values from the schema and resolves the best match via:
	 *   1. Product meta key _momcs_amz_{amazon_field}_{prop_name}  (e.g. _momcs_amz_unit_count_type).
	 *   2. Common synonyms (count / each / unit / piece / item).
	 *   3. First enum value from the schema.
	 *
	 * @param array       $obj_schema    Dereferenced schema for the nested object.
	 * @param string      $amazon_field  Top-level Amazon field (e.g. 'unit_count').
	 * @param string      $prop_name     Nested property name (e.g. 'type').
	 * @param \WC_Product $wc_product    WooCommerce product.
	 * @param array       $root_schema   Root JSON schema document.
	 * @param string      $language_tag  Language tag.
	 * @return array|null Built object or null if no value could be resolved.
	 */
	private function build_companion_enum_object(
		array $obj_schema,
		string $amazon_field,
		string $prop_name,
		\WC_Product $wc_product,
		array $root_schema,
		string $language_tag
	): ?array {
		$obj_props = $obj_schema['properties'] ?? array();
		if ( ! isset( $obj_props['value'] ) ) {
			return null;
		}

		$value_schema   = $this->dereference_json_schema_fragment( $root_schema, $obj_props['value'] );
		$allowed_values = $this->collect_string_enums_from_schema_value( $root_schema, $value_schema );

		// Meta key: _momcs_amz_{amazon_field}_{prop_name} e.g. _momcs_amz_unit_count_type.
		$meta_key  = '_momcs_amz_' . $amazon_field . '_' . $prop_name;
		$meta_val  = get_post_meta( $wc_product->get_id(), $meta_key, true );
		$meta_val  = is_string( $meta_val ) ? trim( $meta_val ) : '';

		$resolved = $this->resolve_unit_count_type_value_for_schema( $allowed_values, $meta_val, '' );

		if ( '' === $resolved && array() !== $allowed_values ) {
			$resolved = (string) reset( $allowed_values );
		}

		if ( '' === $resolved ) {
			return null;
		}

		$obj = array( 'value' => $resolved );
		if ( isset( $obj_props['language_tag'] ) ) {
			$obj['language_tag'] = $language_tag;
		}
		return $obj;
	}

	/**
	 * Extract allowed strings for unit_count.items[].type.value from the product-type schema.
	 *
	 * @param array $schema Root JSON schema document.
	 * @return list<string>
	 */
	private function get_unit_count_type_value_enum_from_schema( array $schema ): array {
		if ( ! isset( $schema['properties']['unit_count'] ) || ! is_array( $schema['properties']['unit_count'] ) ) {
			return array();
		}

		$unit_count_prop = $this->dereference_json_schema_fragment( $schema, $schema['properties']['unit_count'] );
		if ( ! isset( $unit_count_prop['items'] ) || ! is_array( $unit_count_prop['items'] ) ) {
			return array();
		}

		$items = $this->dereference_json_schema_fragment( $schema, $unit_count_prop['items'] );
		if ( ! isset( $items['properties']['type'] ) || ! is_array( $items['properties']['type'] ) ) {
			return array();
		}

		$type_obj = $this->dereference_json_schema_fragment( $schema, $items['properties']['type'] );
		if ( ! isset( $type_obj['properties']['value'] ) || ! is_array( $type_obj['properties']['value'] ) ) {
			return array();
		}

		$value_schema = $this->dereference_json_schema_fragment( $schema, $type_obj['properties']['value'] );
		return $this->collect_string_enums_from_schema_value( $schema, $value_schema );
	}

	/**
	 * Follow a JSON Schema $ref within the same document (e.g. #/$defs/foo).
	 *
	 * @param array $root  Root schema.
	 * @param array $node  Node that may contain $ref.
	 * @param int   $depth Recursion guard.
	 * @return array Dereferenced node.
	 */
	private function dereference_json_schema_fragment( array $root, array $node, int $depth = 0 ): array {
		if ( $depth > 15 ) {
			return $node;
		}
		if ( ! isset( $node['$ref'] ) || ! is_string( $node['$ref'] ) ) {
			return $node;
		}

		$target = $this->follow_json_schema_ref( $root, $node['$ref'] );
		if ( null === $target ) {
			return $node;
		}

		unset( $node['$ref'] );
		$merged = array_merge( $target, $node );
		if ( isset( $merged['$ref'] ) ) {
			return $this->dereference_json_schema_fragment( $root, $merged, $depth + 1 );
		}

		return $merged;
	}

	/**
	 * Resolve #/... pointer from root schema.
	 *
	 * @param array  $root Root schema.
	 * @param string $ref  Reference string.
	 * @return array|null
	 */
	private function follow_json_schema_ref( array $root, string $ref ): ?array {
		if ( 0 !== strpos( $ref, '#/' ) ) {
			return null;
		}
		$parts = explode( '/', substr( $ref, 2 ) );
		$cur   = $root;
		foreach ( $parts as $segment ) {
			if ( ! is_array( $cur ) || ! array_key_exists( $segment, $cur ) ) {
				return null;
			}
			$cur = $cur[ $segment ];
		}
		return is_array( $cur ) ? $cur : null;
	}

	/**
	 * Collect enum / oneOf const string values from a schema fragment.
	 *
	 * @param array $root          Root schema (for $ref).
	 * @param array $value_schema Schema for the "value" property.
	 * @return list<string>
	 */
	private function collect_string_enums_from_schema_value( array $root, array $value_schema ): array {
		$value_schema = $this->dereference_json_schema_fragment( $root, $value_schema );
		$out          = array();

		if ( isset( $value_schema['enum'] ) && is_array( $value_schema['enum'] ) ) {
			foreach ( $value_schema['enum'] as $e ) {
				if ( is_string( $e ) || is_numeric( $e ) ) {
					$out[] = (string) $e;
				}
			}
		}

		foreach ( array( 'oneOf', 'anyOf' ) as $comb ) {
			if ( empty( $value_schema[ $comb ] ) || ! is_array( $value_schema[ $comb ] ) ) {
				continue;
			}
			foreach ( $value_schema[ $comb ] as $opt ) {
				if ( ! is_array( $opt ) ) {
					continue;
				}
				$opt = $this->dereference_json_schema_fragment( $root, $opt );
				if ( isset( $opt['const'] ) && ( is_string( $opt['const'] ) || is_numeric( $opt['const'] ) ) ) {
					$out[] = (string) $opt['const'];
				}
				if ( isset( $opt['enum'] ) && is_array( $opt['enum'] ) ) {
					foreach ( $opt['enum'] as $e ) {
						if ( is_string( $e ) || is_numeric( $e ) ) {
							$out[] = (string) $e;
						}
					}
				}
			}
		}

		$out = array_values( array_unique( $out ) );
		return $out;
	}

	/**
	 * Whether a unit_count type value exactly matches an allowed schema string.
	 *
	 * @param mixed $value        Candidate value.
	 * @param array $allowed_list Allowed strings from schema.
	 * @return bool
	 */
	private function unit_count_type_value_is_allowed( $value, array $allowed_list ): bool {
		if ( array() === $allowed_list ) {
			return true;
		}
		$v = (string) $value;
		foreach ( $allowed_list as $allowed ) {
			if ( $v === (string) $allowed ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Return canonical enum string matching $candidate case-insensitively, or empty string.
	 *
	 * @param string $candidate    Value to match.
	 * @param array  $allowed_list Allowed strings.
	 * @return string
	 */
	private function match_enum_canonical_string( string $candidate, array $allowed_list ): string {
		foreach ( $allowed_list as $allowed ) {
			if ( 0 === strcasecmp( $candidate, (string) $allowed ) ) {
				return (string) $allowed;
			}
		}
		return '';
	}

	/**
	 * Pick a valid unit_count.type.value: meta, then current payload, then common synonyms, then first enum.
	 *
	 * @param array  $allowed_list   Allowed strings from schema (may be empty).
	 * @param string $meta_preference Product meta _momcs_amz_unit_count_type.
	 * @param string $current_payload Current type.value from payload if any.
	 * @return string Canonical allowed value, or best-effort meta/current if enum unknown.
	 */
	private function resolve_unit_count_type_value_for_schema( array $allowed_list, string $meta_preference, string $current_payload ): string {
		$candidates = array();
		if ( '' !== $meta_preference ) {
			$candidates[] = $meta_preference;
		}
		if ( '' !== $current_payload ) {
			$candidates[] = $current_payload;
		}

		foreach ( $candidates as $cand ) {
			$hit = $this->match_enum_canonical_string( $cand, $allowed_list );
			if ( '' !== $hit ) {
				return $hit;
			}
		}

		if ( array() === $allowed_list ) {
			// No schema enum: return merchant meta or payload as-is (legacy).
			if ( '' !== $meta_preference ) {
				return $meta_preference;
			}
			if ( '' !== $current_payload ) {
				return $current_payload;
			}
			return '';
		}

		$synonyms = array(
			'count',
			'Count',
			'COUNT',
			'each',
			'Each',
			'1',
			'unit',
			'Unit',
			'piece',
			'Piece',
			'item',
			'Item',
		);
		foreach ( $synonyms as $syn ) {
			$hit = $this->match_enum_canonical_string( $syn, $allowed_list );
			if ( '' !== $hit ) {
				return $hit;
			}
		}

		foreach ( $allowed_list as $allowed ) {
			$a = (string) $allowed;
			if ( false !== stripos( $a, 'count' ) || '1' === $a ) {
				return $a;
			}
		}

		return '';
	}

	/**
	 * Apply fallbacks for item_name, brand, and India/contact fields when mappings omit them.
	 *
	 * @param array       $attributes      Attributes (by ref).
	 * @param \WC_Product $wc_product      Product.
	 * @param string      $marketplace_id  Marketplace id.
	 * @param string      $language_tag    Language tag.
	 * @param int         $product_id      Product id.
	 * @return void
	 */
	private function apply_item_name_brand_and_contact_fallbacks( array &$attributes, \WC_Product $wc_product, string $marketplace_id, string $language_tag, int $product_id ): void {
		if ( $this->is_amazon_array_attribute_empty( $attributes['item_name'] ?? null ) ) {
			$attributes['item_name'] = array(
				array(
					'value'          => $wc_product->get_name(),
					'language_tag'   => $language_tag,
					'marketplace_id' => $marketplace_id,
				),
			);
		}

		if ( $this->is_amazon_array_attribute_empty( $attributes['brand'] ?? null ) ) {
			$brand_str = get_post_meta( $product_id, '_brand', true );
			if ( ! is_string( $brand_str ) || '' === trim( $brand_str ) ) {
				$brand_str = $wc_product->get_attribute( 'brand' );
			}
			if ( ! is_string( $brand_str ) || '' === trim( $brand_str ) ) {
				$terms = get_the_terms( $product_id, 'product_brand' );
				if ( $terms && ! is_wp_error( $terms ) ) {
					$brand_str = $terms[0]->name;
				}
			}
			if ( is_string( $brand_str ) && '' !== trim( $brand_str ) ) {
				$attributes['brand'] = array(
					array(
						'value'          => trim( $brand_str ),
						'language_tag'   => $language_tag,
						'marketplace_id' => $marketplace_id,
					),
				);
			}
		}

		$contacts = array(
			'rtip_manufacturer_contact_information' => '_momcs_amz_rtip_manufacturer_contact_information',
			'packer_contact_information'            => '_momcs_amz_packer_contact_information',
			'importer_contact_information'          => '_momcs_amz_importer_contact_information',
		);
		foreach ( $contacts as $amazon_key => $meta_key ) {
			if ( isset( $attributes[ $amazon_key ] ) && is_array( $attributes[ $amazon_key ] ) && array() !== $attributes[ $amazon_key ] ) {
				continue;
			}
			$text = get_post_meta( $product_id, $meta_key, true );
			if ( ! is_string( $text ) || '' === trim( $text ) ) {
				continue;
			}
			$item = array( 'value' => trim( $text ) );
			if ( 'packer_contact_information' === $amazon_key || 'importer_contact_information' === $amazon_key ) {
				$item['language_tag']   = $language_tag;
				$item['marketplace_id'] = $marketplace_id;
			}
			$attributes[ $amazon_key ] = array( $item );
		}
	}

	/**
	 * Whether an Amazon array attribute is missing or has an empty primary value.
	 *
	 * @param mixed $attr Attribute value (usually list of row objects).
	 * @return bool
	 */
	private function is_amazon_array_attribute_empty( $attr ): bool {
		if ( ! is_array( $attr ) || array() === $attr ) {
			return true;
		}
		$first = $attr[0];
		if ( ! is_array( $first ) ) {
			return true;
		}
		if ( ! isset( $first['value'] ) ) {
			return false;
		}
		return '' === trim( (string) $first['value'] );
	}

	/**
	 * Process nested field mapping for complex structures (legacy method).
	 *
	 * @param array       &$attributes        Attributes array to modify.
	 * @param string      $field_name         Field name.
	 * @param array       $mapping_data       Nested mapping data.
	 * @param \WC_Product $wc_product         WooCommerce product.
	 * @param array       $prop               Schema property definition.
	 * @param string      $marketplace_id     Marketplace ID.
	 * @param string      $language_tag       Language tag.
	 */
	private function process_nested_field_mapping( array &$attributes, string $field_name, array $mapping_data, \WC_Product $wc_product, array $prop, string $marketplace_id, string $language_tag ): void {
		$wc_source     = $this->extract_wc_source_from_mapping( $mapping_data );
		$original_path = $mapping_data['original_path'] ?? '';

		if ( '' === $wc_source ) {
			return;
		}

		$value = $this->extract_wc_value( $wc_product, $wc_source );
		if ( null === $value || '' === $value ) {
			return;
		}

		// Use the assign_by_path method to handle nested assignment.
		$this->assign_by_path( $attributes, $original_path, $value );

		// Add required selectors if this is an array field.
		if ( isset( $prop['type'] ) && 'array' === $prop['type'] ) {
			$this->add_selectors_to_nested_field( $attributes, $field_name, $prop, $marketplace_id, $language_tag );
		}
	}

	/**
	 * Process nested schema mappings by looking for child field mappings.
	 * Works with both hierarchical structures and dot-notation mappings.
	 *
	 * @param array       &$attributes        Attributes array to modify.
	 * @param string      $parent_field       Parent field name.
	 * @param array       $prop               Schema property definition.
	 * @param array       $field_mapping      All field mappings.
	 * @param \WC_Product $wc_product         WooCommerce product.
	 * @param string      $marketplace_id     Marketplace ID.
	 * @param string      $language_tag       Language tag.
	 */
	private function process_nested_schema_mappings( array &$attributes, string $parent_field, array $prop, array $field_mapping, \WC_Product $wc_product, string $marketplace_id, string $language_tag ): void {
		// Check if the parent field exists in hierarchical format.
		if ( isset( $field_mapping[ $parent_field ] ) && is_array( $field_mapping[ $parent_field ] ) ) {
			// Already processed by process_hierarchical_field_mapping.
			return;
		}

		// Look for dot-notation nested field mappings like "item_dimensions.length".
		$nested_prefix = $parent_field . '.';
		foreach ( $field_mapping as $mapped_field => $mapping_data ) {
			if ( strpos( $mapped_field, $nested_prefix ) === 0 ) {
				$wc_source = $this->extract_wc_source_from_mapping( $mapping_data );
				if ( '' === $wc_source ) {
					continue;
				}

				$value = $this->extract_wc_value( $wc_product, $wc_source );
				if ( null === $value || '' === $value ) {
					continue;
				}

							// Build the nested structure using dot notation.
				$this->assign_by_path( $attributes, $mapped_field, $value );

				// Add selectors if this creates an array field.
				if ( isset( $prop['type'] ) && 'array' === $prop['type'] ) {
					$this->add_selectors_to_nested_field( $attributes, $parent_field, $prop, $marketplace_id, $language_tag );
				}
			}
		}
	}

	/**
	 * Add required selectors to nested field structures.
	 *
	 * @param array  &$attributes      Attributes array to modify.
	 * @param string $field_name       Field name.
	 * @param array  $prop             Schema property definition.
	 * @param string $marketplace_id   Marketplace ID.
	 * @param string $language_tag     Language tag.
	 */
	private function add_selectors_to_nested_field( array &$attributes, string $field_name, array $prop, string $marketplace_id, string $language_tag ): void {
		if ( ! isset( $attributes[ $field_name ] ) || ! is_array( $attributes[ $field_name ] ) ) {
			return;
		}

		// Add selectors to each array item.
		foreach ( $attributes[ $field_name ] as &$item ) {
			if ( is_array( $item ) ) {
				if ( in_array( 'language_tag', $prop['selectors'] ?? array(), true ) ) {
					$item['language_tag'] = $language_tag;
				}
				if ( in_array( 'marketplace_id', $prop['selectors'] ?? array(), true ) ) {
					$item['marketplace_id'] = $marketplace_id;
				}
			}
		}
	}

	/**
	 * Assign value into a nested array path like `foo[].bar` or `foo.bar`.
	 *
	 * @param array  &$payload Reference to payload array.
	 * @param string $path    Path to assign value to.
	 * @param mixed  $value   Value to assign.
	 * @return void
	 */
	private function assign_by_path( array &$payload, string $path, $value ): void {
		// Handle array notation with nested paths.
		if ( false !== strpos( $path, '[]' ) ) {
			$parts            = explode( '[]', $path, 2 );
			$head             = trim( $parts[0], '.' );
			$tail             = ltrim( $parts[1], '.' );
			$payload[ $head ] = isset( $payload[ $head ] ) ? $payload[ $head ] : array();
			$item             = array();
			if ( $tail ) {
				$this->assign_by_path( $item, $tail, $value );
			} else {
				$item = $value;
			}
			$payload[ $head ][] = $item;
			return;
		}

		// Handle dot notation for nested objects.
		if ( false !== strpos( $path, '.' ) ) {
			$parts            = explode( '.', $path, 2 );
			$head             = $parts[0];
			$tail             = $parts[1];
			$payload[ $head ] = isset( $payload[ $head ] ) ? $payload[ $head ] : array();
			$this->assign_by_path( $payload[ $head ], $tail, $value );
			return;
		}
		$payload[ $path ] = $value;
	}

	/**
	 * Apply price markup to a value.
	 *
	 * @param mixed  $value Value to apply markup to.
	 * @param string $channel_uuid Channel UUID.
	 * @return float Marked up value.
	 */
	private function apply_price_markup( $value, string $channel_uuid ): float {
		if ( ! is_numeric( $value ) ) {
			return $value;
		}
		$settings = apply_filters( 'momcs_get_sync_settings_for_channel', array(), $channel_uuid );
		if ( empty( $settings ) || empty( $settings['product_sync']['sync_settings']['price_markup'] ) ) {
			return round( (float) $value, 2 );
		}
		$cfg        = $settings['product_sync']['sync_settings']['price_markup'];
		$operator   = isset( $cfg['operator'] ) ? (string) $cfg['operator'] : '+';
		$percentage = isset( $cfg['percentage'] ) ? (float) $cfg['percentage'] : 0.0;
		$base       = (float) $value;
		$factor     = $percentage / 100.0;

		switch ( $operator ) {
			case '+':
				$base = $base + ( $base * $factor );
				break;
			case '-':
				$base = $base - ( $base * $factor );
				break;
			case '*':
				$base = $base * ( 1 + $factor );
				break;
			case '/':
				$div = ( 1 + $factor );
				if ( $div <= 0.0 ) {
					$div = 1.0;
				}
				$base = $base / $div;
				break;
			default:
				break;
		}

		if ( $base < 0 ) {
			$base = 0.0;
		}
		return round( $base, 2 );
	}

	/**
	 * Build schema-compliant payload for a variation product.
	 *
	 * This method builds the payload for a variation, automatically:
	 * - Sets parentage_level to 'child'.
	 * - Includes parent_sku reference in child_parent_sku_relationship.
	 * - Extracts variation-specific attribute values.
	 *
	 * @param int        $variation_id   WooCommerce variation product ID.
	 * @param string     $channel_uuid   Channel UUID.
	 * @param string     $product_type   Amazon product-type name.
	 * @param string     $marketplace_id Marketplace ID.
	 * @param string     $parent_sku     Parent product SKU.
	 * @param string     $asin           ASIN for existing catalog item (optional).
	 * @param array|null $template_config Pre-resolved template configuration.
	 * @return array<string,mixed> Payload ready to json_encode().
	 * @throws \RuntimeException When mapping is incomplete or validation fails.
	 */
	public function build_for_variation( int $variation_id, string $channel_uuid, string $product_type, string $marketplace_id, string $parent_sku, string $asin = '', ?array $template_config = null ): array {
		// Set variation context for attribute extraction.
		$this->variation_context = array(
			'is_variation' => true,
			'is_variable'  => false,
			'parent_sku'   => $parent_sku,
			'parent_id'    => Variation_Helper::get_parent_id( $variation_id ),
		);

		// Build base payload using the standard method.
		$payload = $this->build( $variation_id, $channel_uuid, $product_type, $marketplace_id, $asin, $template_config );

		// Inject parentage attributes for the variation (child).
		$payload = $this->inject_parentage_attributes( $payload, 'child', $parent_sku, $marketplace_id );

		// Clear context after build.
		$this->variation_context = array();

		return $payload;
	}

	/**
	 * Build schema-compliant payload for a variable product (parent).
	 *
	 * This method builds the payload for a variable product, automatically:
	 * - Sets parentage_level to 'parent'.
	 * - Does NOT include child_parent_sku_relationship (parent doesn't have one).
	 *
	 * @param int        $product_id     WooCommerce variable product ID.
	 * @param string     $channel_uuid   Channel UUID.
	 * @param string     $product_type   Amazon product-type name.
	 * @param string     $marketplace_id Marketplace ID.
	 * @param string     $asin           ASIN for existing catalog item (optional).
	 * @param array|null $template_config Pre-resolved template configuration.
	 * @return array<string,mixed> Payload ready to json_encode().
	 * @throws \RuntimeException When mapping is incomplete or validation fails.
	 */
	public function build_for_variable( int $product_id, string $channel_uuid, string $product_type, string $marketplace_id, string $asin = '', ?array $template_config = null ): array {
		// Set variable context.
		$this->variation_context = array(
			'is_variation' => false,
			'is_variable'  => true,
			'parent_sku'   => '',
			'parent_id'    => 0,
		);

		// Build base payload using the standard method.
		$payload = $this->build( $product_id, $channel_uuid, $product_type, $marketplace_id, $asin, $template_config );

		// Inject parentage attributes for the parent.
		$payload = $this->inject_parentage_attributes( $payload, 'parent', '', $marketplace_id );

		// Clear context after build.
		$this->variation_context = array();

		return $payload;
	}

	/**
	 * Inject parentage-related attributes into the payload.
	 *
	 * Dynamically adds:
	 * - parentage_level: 'parent' or 'child' based on product type.
	 * - child_parent_sku_relationship: For children, includes parent_sku reference.
	 *
	 * @param array  $payload         The payload to modify.
	 * @param string $parentage_level Either 'parent' or 'child'.
	 * @param string $parent_sku      Parent SKU (required for 'child', empty for 'parent').
	 * @param string $marketplace_id  Marketplace ID for structure requirements.
	 * @return array Modified payload with parentage attributes.
	 */
	public function inject_parentage_attributes( array $payload, string $parentage_level, string $parent_sku = '', string $marketplace_id = '' ): array {
		if ( ! isset( $payload['attributes'] ) ) {
			$payload['attributes'] = array();
		}

		// Set parentage_level attribute.
		$payload['attributes']['parentage_level'] = array(
			array(
				'value'          => $parentage_level,
				'marketplace_id' => $marketplace_id,
			),
		);

		// For child products, add the parent SKU relationship.
		if ( 'child' === $parentage_level && ! empty( $parent_sku ) ) {
			$payload['attributes']['child_parent_sku_relationship'] = array(
				array(
					'child_relationship_type' => 'variation',
					'parent_sku'              => $parent_sku,
					'marketplace_id'          => $marketplace_id,
				),
			);
		}

		return $payload;
	}

	/**
	 * Check if currently building a variation payload.
	 *
	 * @return bool True if building for a variation.
	 */
	private function is_building_variation(): bool {
		return ! empty( $this->variation_context['is_variation'] );
	}

	/**
	 * Check if currently building a variable (parent) payload.
	 *
	 * @return bool True if building for a variable product.
	 */
	private function is_building_variable(): bool {
		return ! empty( $this->variation_context['is_variable'] );
	}

	/**
	 * Check if an attribute is used for variations on a variable product.
	 *
	 * WooCommerce variable products have attributes that can be either:
	 * - Used for variations (e.g., color, size) - these should NOT appear in parent payload
	 * - Not used for variations (e.g., brand, material) - these CAN appear in parent payload
	 *
	 * @param \WC_Product_Variable $product        Variable product.
	 * @param string               $attribute_slug Attribute slug (e.g., 'pa_color').
	 * @return bool True if attribute is used for variations.
	 */
	private function is_variation_attribute( \WC_Product_Variable $product, string $attribute_slug ): bool {
		$attributes = $product->get_attributes();

		// Normalize the slug by removing pa_ prefix for comparison.
		$normalized_slug = $attribute_slug;
		if ( 0 === strpos( $attribute_slug, 'pa_' ) ) {
			$normalized_slug = substr( $attribute_slug, 3 );
		}

		foreach ( $attributes as $attr_key => $attribute ) {
			if ( ! $attribute instanceof \WC_Product_Attribute ) {
				continue;
			}

			// Match by taxonomy name (e.g., 'pa_color') or attribute key.
			$attr_name = $attribute->get_name();
			if ( $attribute_slug === $attr_name ||
				$attribute_slug === $attr_key ||
				$normalized_slug === $attr_key ||
				'pa_' . $normalized_slug === $attr_name ) {
				return $attribute->get_variation();
			}
		}

		return false;
	}

	/**
	 * Extract variation-specific WooCommerce value.
	 *
	 * For variation products building, this method extracts the actual variation
	 * attribute value when the source is a global attribute (pa_*).
	 *
	 * @param \WC_Product $product  WooCommerce product.
	 * @param string      $source   Source field name.
	 * @return mixed WooCommerce value or null if not found.
	 */
	private function extract_variation_wc_value( \WC_Product $product, string $source ) {
		// If building for variation and source is a global attribute, get variation-specific value.
		if ( $this->is_building_variation() && 0 === strpos( $source, 'pa_' ) ) {
			$variation_value = Variation_Helper::get_variation_attribute_value( $product, $source );
			if ( null !== $variation_value && '' !== $variation_value ) {
				return $variation_value;
			}
		}

		// Fall back to standard extraction.
		return $this->extract_wc_value( $product, $source );
	}
}
