<?php
/**
 * Feed Handler – orchestrates requests to Amazon Feeds API via the low-level
 * Feeds_API_Service and provides simplified helper methods for controllers,
 * cron jobs, and background processors.
 *
 * @package    MOMCS_Sync
 * @subpackage MOMCS_Sync\includes\common\handlers
 */

declare(strict_types=1);

namespace MOMCS_Sync\Includes\Common\Handlers;

use MOMCS_Sync\Includes\Common\Services\Amazon\Feeds_API_Service;
use MOMCS_Sync\Includes\Common\Utils\Logger;
use MOMCS_Sync\Includes\Common\Models\Multichannels_Model;
use MOMCS_Sync\Includes\Common\Handlers\Payload\Payload_Builder;
use MOMCS_Sync\Includes\Common\Utils\AS_Log_Helper;
use MOMCS_Sync\Includes\Common\Constants\Plugin_Constants;
use MOMCS_Sync\Includes\Common\Exceptions\Amazon_Api_Exception;
use MOMCS_Sync\Includes\Common\Exceptions\Sync_Exception;
use MOMCS_Sync\Includes\Common\Utils\Current_Action;
use MOMCS_Sync\Includes\Common\Utils\Asin_Helper;
use MOMCS_Sync\Includes\Common\Utils\Utils;
use MOMCS_Sync\Includes\Common\Utils\Variation_Helper;
use WP_Error;

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

/**
 * Class Feed_Handler
 */
class Feed_Handler {

	/**
	 * Feeds API service instance.
	 *
	 * @var Feeds_API_Service
	 */
	private Feeds_API_Service $feeds_api_service;
	/**
	 * Logger instance.
	 *
	 * @var Logger
	 */
	private Logger $logger;

	/**
	 * Payload builder instance.
	 *
	 * @var Payload_Builder
	 */
	private Payload_Builder $payload_builder;
	/**
	 * Multichannels model for channel lookups.
	 *
	 * @var Multichannels_Model
	 */
	private Multichannels_Model $multichannels_model;

	/**
	 * Constructor.
	 */
	public function __construct() {
		// Allow DI / easier testing.
		$this->feeds_api_service   = new Feeds_API_Service();
		$this->logger              = new Logger( 'feed-handler' );
		$this->payload_builder     = new Payload_Builder();
		$this->multichannels_model = new Multichannels_Model();
	}

	/**
	 * Feed Lifecycle Helpers.
	 */

	/**
	 * Create a feed document → upload payload → create feed.
	 * Returns the resulting feedId if successful.
	 *
	 * @param array       $payload         Feed payload (string or resource).
	 * @param array       $marketplace_ids Marketplace IDs.
	 * @param string|null $channel_id      Channel UUID.
	 *
	 * @return array{feedId:string}|WP_Error
	 */
	public function create_and_submit_feed( $payload, array $marketplace_ids, ?string $channel_id = null ) {
		$context = array( 'stage' => 'feed_create_document' );

		$document_result = $this->feeds_api_service->create_feed_document( $marketplace_ids, $context, $channel_id ?? '' );
		if ( is_wp_error( $document_result ) ) {
			return $document_result;
		}

		$feed_result = $this->feeds_api_service->create_feed( $document_result['feedDocumentId'], $marketplace_ids, array( 'stage' => 'feed_create' ) + $context, $channel_id ?? '' );
		if ( is_wp_error( $feed_result ) ) {
			return $feed_result;
		}

		return array( 'feedId' => $feed_result['feedId'] ?? '' );
	}

	/**
	 * Poll feed processing status.
	 *
	 * @param string      $feed_id    Feed ID.
	 * @param string|null $channel_id Channel UUID.
	 *
	 * @return array|WP_Error
	 */
	public function get_feed_status( string $feed_id, ?string $channel_id = null ) {
		return $this->feeds_api_service->get_feed_status( $feed_id, array( 'stage' => 'feed_status' ), $channel_id );
	}

	/**
	 * Download feed processing result (and decrypt if necessary).
	 *
	 * @param string      $feed_id    Feed ID.
	 * @param string|null $channel_id Channel UUID.
	 *
	 * @return array|WP_Error
	 */
	public function download_feed_result( string $feed_id, ?string $channel_id = null ) {
		$doc = $this->feeds_api_service->get_feed_document( $feed_id, array( 'stage' => 'feed_get_document' ), $channel_id );
		if ( is_wp_error( $doc ) ) {
			return $doc;
		}

		return $this->feeds_api_service->download_feed_result( $doc['url'], $doc['encryptionDetails'] ?? null, array( 'stage' => 'feed_download' ) );
	}

	/**
	 * Sync products using Feeds API (bulk path).
	 *
	 * For large product sets (exceeding CHUNKED_BUILD_THRESHOLD), the feed building
	 * is automatically split into smaller chunks to prevent PHP timeouts.
	 *
	 * @param array       $product_ids               WooCommerce product IDs.
	 * @param array       $marketplace_ids           Amazon marketplace IDs.
	 * @param string|null $channel_id                Channel UUID.
	 * @param array       $product_template_mappings Map of product_id => ['product_type' => string, 'template_config' => array].
	 * @return array                                 Result structure: success/error.
	 */
	public function sync_products_with_feeds_api( array $product_ids, array $marketplace_ids, ?string $channel_id = null, array $product_template_mappings = array() ) {
		// Use chunked building for large product sets to prevent timeouts.
		if ( count( $product_ids ) >= Plugin_Constants::CHUNKED_BUILD_THRESHOLD ) {
			return $this->start_chunked_feed_build( $product_ids, $marketplace_ids, $channel_id, $product_template_mappings );
		}

		$current_action_id = Current_Action::id();
		if ( $current_action_id ) {
			AS_Log_Helper::log(
				$current_action_id,
				'info',
				0,
				array(
					'stage'         => 'feed_upload_request',
					'product_count' => count( $product_ids ),
					'marketplaces'  => $marketplace_ids,
				)
			);
		}

		$feed_log_base = array( 'log_action_id' => $current_action_id );

		// 1) Create feed document.
		try {
			$document_result = $this->create_feed_document(
				$marketplace_ids,
				$feed_log_base + array( 'stage' => 'feed_create_document' ),
				$channel_id ?? ''
			);
		} catch ( Sync_Exception $e ) {
			$this->logger->error( 'Failed to create feed document: ' . $e->getMessage() );
			return array(
				'success' => array(),
				'error'   => array( 'feed_document' => $e->getMessage() ),
			);
		}

		// 2) Build payload.
		$payload_result = $this->build_feed_payload( $product_ids, $marketplace_ids, $channel_id, $product_template_mappings );
		if ( is_wp_error( $payload_result ) ) {
			$this->logger->error( 'Failed to build feed payload: ' . $payload_result->get_error_message() );
			return array(
				'success' => array(),
				'error'   => array( 'feed_payload' => $payload_result->get_error_message() ),
			);
		}
		$payload                   = $payload_result['payload'];
		$message_id_to_sku_mapping = $payload_result['message_id_to_sku_mapping'];

		// 3) Upload feed payload.
		try {
			$this->upload_feed_payload(
				$document_result['url'],
				$payload,
				$feed_log_base + array( 'stage' => 'feed_upload' )
			);
		} catch ( Amazon_Api_Exception $e ) {
			$this->logger->error( 'Failed to upload feed payload: ' . $e->getMessage() );
			return array(
				'success' => array(),
				'error'   => array( 'feed_upload' => $e->getMessage() ),
			);
		}

		// 4) Create feed.
		try {
			$feed_result = $this->create_feed(
				$document_result['feedDocumentId'],
				$marketplace_ids,
				$feed_log_base + array( 'stage' => 'feed_create' ),
				$channel_id
			);
		} catch ( Amazon_Api_Exception $e ) {
			$this->logger->error( 'Failed to create feed: ' . $e->getMessage() );
			return array(
				'success' => array(),
				'error'   => array( 'feed_creation' => $e->getMessage() ),
			);
		}

		// Audit summary.
		if ( $current_action_id ) {
			AS_Log_Helper::log(
				$current_action_id,
				'success',
				0,
				array(
					'stage'   => 'feed_created',
					'feed_id' => $feed_result['feedId'],
					'message' => 'Product Feed created',
				)
			);
		}

		// 5) Store mapping file for later result processing.
		$feed_id = $feed_result['feedId'];
		$this->store_feed_mapping_file( $feed_id, $payload, $message_id_to_sku_mapping );

		// 6) Schedule status polling.
		$this->schedule_feed_status_check( $feed_id, $current_action_id, $channel_id );

		if ( isset( $feed_id ) && $current_action_id ) {
			AS_Log_Helper::log(
				$current_action_id,
				'success',
				0,
				array(
					'stage'   => 'feed_uploaded',
					'feed_id' => $feed_id,
					'message' => 'Product Feed uploaded',
				)
			);
		}

		return array(
			'success' => array(
				'feed_id' => $feed_result['feedId'],
				'message' => 'Feed submitted successfully. The products will be processed by Amazon.',
			),
			'error'   => array(),
		);
	}

	/**
	 * Build product feed payload for Feeds API.
	 *
	 * This method now supports variable products and variations:
	 * - Variable products are expanded to parent + all variations.
	 * - Parent messages are created first with parentage_level: parent.
	 * - Variation messages follow with parentage_level: child and parent_sku reference.
	 *
	 * @param array       $product_ids               WooCommerce product IDs.
	 * @param array       $marketplace_ids           Amazon marketplace IDs.
	 * @param string|null $channel_id                Channel UUID.
	 * @param array       $product_template_mappings Map of product_id => ['product_type' => string, 'template_config' => array].
	 * @return array|WP_Error
	 */
	public function build_feed_payload( array $product_ids, array $marketplace_ids, $channel_id = null, array $product_template_mappings = array() ) {
		$channel_details = $this->get_channel_details( (string) $channel_id );
		if ( is_wp_error( $channel_details ) ) {
			return new WP_Error( 'channel_details', $channel_details->get_error_message() );
		}

		// Expand product IDs to include variations (parent-first ordering).
		$expanded_product_ids = Variation_Helper::expand_product_ids_with_variations( $product_ids );

		$all_products = $this->get_products_by_ids( $expanded_product_ids );
		if ( empty( $all_products ) ) {
			return new WP_Error( 'no_products', 'No valid products supplied' );
		}

		$messages                  = array();
		$message_id_counter        = 1;
		$message_id_to_sku_mapping = array();

		foreach ( $expanded_product_ids as $product_id ) {
			$product = isset( $all_products[ $product_id ] ) ? $all_products[ $product_id ] : null;
			if ( ! $product ) {
				$this->logger->error( 'Product not found: ' . $product_id );
				continue;
			}

			// Resolve product type and template config from mappings (use parent's for variations).
			$resolved        = $this->resolve_product_template_for_feed( $product, $product_template_mappings );
			$product_type    = $resolved['product_type'];
			$template_config = $resolved['template_config'];

			// Build message based on product type (variable/variation/simple).
			$msg = $this->build_product_message( $product, $marketplace_ids, $channel_id, $product_type, $message_id_counter, $template_config );
			if ( ! is_wp_error( $msg ) ) {
				$messages[]                                       = $msg;
				$message_id_to_sku_mapping[ $message_id_counter ] = isset( $msg['sku'] ) ? $msg['sku'] : '';
				++$message_id_counter;
			}
		}

		if ( empty( $messages ) ) {
			return new WP_Error( 'no_valid_products', 'No valid products to include in the feed' );
		}

		$payload = array(
			'header'   => array(
				'sellerId' => $channel_details['seller_id'],
				'version'  => '2.0',
			),
			'messages' => $messages,
		);

		return array(
			'payload'                   => $payload,
			'message_id_to_sku_mapping' => $message_id_to_sku_mapping,
		);
	}

	/**
	 * Resolve product type and template config for feed message, using parent's for variations.
	 *
	 * @param \WC_Product $product                   WooCommerce product.
	 * @param array       $product_template_mappings Product template mappings (product_id => ['product_type', 'template_config']).
	 * @return array ['product_type' => string, 'template_config' => array|null].
	 */
	private function resolve_product_template_for_feed( $product, array $product_template_mappings ): array {
		$product_id = $product->get_id();

		// Direct mapping for this product.
		if ( isset( $product_template_mappings[ $product_id ] ) ) {
			return array(
				'product_type'    => $product_template_mappings[ $product_id ]['product_type'] ?? 'PRODUCT',
				'template_config' => $product_template_mappings[ $product_id ]['template_config'] ?? null,
			);
		}

		// For variations, use parent's template.
		if ( Variation_Helper::is_variation( $product ) ) {
			$parent_id = Variation_Helper::get_parent_id( $product );
			if ( $parent_id && isset( $product_template_mappings[ $parent_id ] ) ) {
				return array(
					'product_type'    => $product_template_mappings[ $parent_id ]['product_type'] ?? 'PRODUCT',
					'template_config' => $product_template_mappings[ $parent_id ]['template_config'] ?? null,
				);
			}
		}

		return array(
			'product_type'    => 'PRODUCT',
			'template_config' => null,
		);
	}

	/**
	 * Build a Feeds API message for a product (supports simple, variable, and variation).
	 *
	 * @param \WC_Product $product          WC_Product object.
	 * @param array       $marketplace_ids  Marketplaces.
	 * @param string|null $channel_id       Channel UUID.
	 * @param string      $product_type     Amazon product type.
	 * @param int         $message_id       Positive message id.
	 * @param array|null  $template_config  Pre-resolved template configuration.
	 * @return array|WP_Error
	 */
	private function build_product_message( $product, array $marketplace_ids, $channel_id = null, $product_type = 'PRODUCT', $message_id = 1, ?array $template_config = null ) {
		$sku = $product->get_sku();
		if ( empty( $sku ) ) {
			return new WP_Error( 'missing_sku', 'Product SKU is required: ' . $product->get_id() );
		}

		// Get marketplace-aware ASIN.
		$marketplace_id = null;
		if ( Utils::is_premium_plan() && $channel_id ) {
			$marketplace_id = $this->get_marketplace_id_from_channel( $channel_id );
		}
		$asin = Asin_Helper::get_product_asin( $product->get_id(), $marketplace_id );

		// Determine if this is a variable product or variation.
		$is_variable  = Variation_Helper::is_variable_product( $product );
		$is_variation = Variation_Helper::is_variation( $product );

		$message = array(
			'messageId'     => $message_id,
			'sku'           => $sku,
			'operationType' => 'UPDATE',
			'productType'   => $product_type,
			'requirements'  => $asin ? 'LISTING_OFFER_ONLY' : 'LISTING',
			'attributes'    => $this->get_product_attributes_for_feed( $product, $marketplace_ids, $channel_id, $product_type, $asin, $is_variable, $is_variation, $template_config ),
		);

		return $message;
	}

	/**
	 * Build a Feeds API message for a single product (simple variant only).
	 *
	 * @deprecated Use build_product_message() instead for variable product support.
	 *
	 * @param mixed       $product          WC_Product object.
	 * @param array       $marketplace_ids  Marketplaces.
	 * @param string|null $channel_id       Channel UUID.
	 * @param string      $product_type     Amazon product type.
	 * @param int         $message_id       Positive message id.
	 * @return array|WP_Error
	 */
	private function build_simple_product_message( $product, array $marketplace_ids, $channel_id = null, $product_type = 'PRODUCT', $message_id = 1 ) {
		return $this->build_product_message( $product, $marketplace_ids, $channel_id, $product_type, $message_id );
	}

	/**
	 * Build product attribute payload via Payload_Builder and adapt for Feeds API.
	 *
	 * @deprecated Use get_product_attributes_for_feed() instead for variable product support.
	 *
	 * @param mixed       $product          WC_Product object.
	 * @param array       $marketplace_ids  Marketplaces.
	 * @param string|null $channel_id       Channel UUID.
	 * @param string      $product_type     Amazon product type.
	 * @param bool|string $asin             ASIN.
	 *
	 * @return array
	 */
	private function get_product_attributes( $product, array $marketplace_ids, $channel_id = null, $product_type = 'PRODUCT', $asin = false ) {
		return $this->get_product_attributes_for_feed( $product, $marketplace_ids, $channel_id, $product_type, $asin, false, false );
	}

	/**
	 * Build product attribute payload via Payload_Builder with variation support.
	 *
	 * For variable products (parents):
	 * - Uses build_for_variable() to include parentage_level: parent.
	 *
	 * For variations (children):
	 * - Uses build_for_variation() to include parentage_level: child and parent_sku.
	 *
	 * For simple products:
	 * - Uses standard build() method.
	 *
	 * @param \WC_Product $product          WC_Product object.
	 * @param array       $marketplace_ids  Marketplaces.
	 * @param string|null $channel_id       Channel UUID.
	 * @param string      $product_type     Amazon product type.
	 * @param bool|string $asin             ASIN.
	 * @param bool        $is_variable      Whether product is a variable (parent).
	 * @param bool        $is_variation     Whether product is a variation (child).
	 * @param array|null  $template_config  Pre-resolved template configuration.
	 *
	 * @return array Product attributes adapted for Feeds API.
	 */
	private function get_product_attributes_for_feed( $product, array $marketplace_ids, $channel_id = null, $product_type = 'PRODUCT', $asin = false, bool $is_variable = false, bool $is_variation = false, ?array $template_config = null ): array {
		try {
			$primary_marketplace_id = isset( $marketplace_ids[0] ) ? $marketplace_ids[0] : 'ATVPDKIKX0DER';
			$payload                = array();

			if ( $is_variable ) {
				// Variable product (parent) - use build_for_variable.
				$payload = $this->payload_builder->build_for_variable(
					$product->get_id(),
					$channel_id,
					$product_type,
					$primary_marketplace_id,
					$asin ?: '',
					$template_config
				);
			} elseif ( $is_variation ) {
				// Variation (child) - use build_for_variation with parent SKU.
				$parent_sku = Variation_Helper::get_parent_sku( $product );
				if ( empty( $parent_sku ) ) {
					$this->logger->warning( 'Variation ' . $product->get_id() . ' has no parent SKU, using empty string' );
				}
				$payload = $this->payload_builder->build_for_variation(
					$product->get_id(),
					$channel_id,
					$product_type,
					$primary_marketplace_id,
					$parent_sku,
					$asin ?: '',
					$template_config
				);
			} else {
				// Simple product - use standard build.
				$payload = $this->payload_builder->build(
					$product->get_id(),
					$channel_id,
					$product_type,
					$primary_marketplace_id,
					$asin ?: '',
					$template_config
				);
			}

			$attributes = isset( $payload['attributes'] ) ? $payload['attributes'] : array();
			$attributes = $this->adapt_attributes_for_feeds_api( $attributes, $marketplace_ids );

			return $attributes;
		} catch ( \Exception $e ) {
			$this->logger->error( 'Failed to build attributes with Payload_Builder: ' . $e->getMessage() );
			return array();
		}
	}

	/**
	 * Adapt Payload_Builder attributes to Feeds API expectations.
	 *
	 * @param array $attributes       Attributes.
	 * @param array $marketplace_ids  Marketplaces.
	 *
	 * @return array
	 */
	private function adapt_attributes_for_feeds_api( $attributes, $marketplace_ids ) {
		$adapted_attributes = $attributes;

		if ( ! isset( $adapted_attributes['item_name'] ) && isset( $adapted_attributes['product_title'] ) ) {
			$adapted_attributes['item_name'] = $adapted_attributes['product_title'];
		}

		foreach ( $adapted_attributes as $field_name => &$field_value ) {
			if ( is_array( $field_value ) && ! empty( $field_value ) ) {
				$first_item = reset( $field_value );
				if ( is_array( $first_item ) && isset( $first_item['marketplace_id'] ) && isset( $first_item['value'] ) ) {
					$this->ensure_marketplace_coverage( $field_value, $marketplace_ids );
				}
			}
		}

		return $adapted_attributes;
	}

	/**
	 * Ensure marketplace-specific attribute arrays include all marketplaces.
	 *
	 * @param array &$field_value      Field value.
	 * @param array $marketplace_ids   Marketplaces.
	 *
	 * @return void
	 */
	private function ensure_marketplace_coverage( &$field_value, $marketplace_ids ) {
		$covered_marketplaces = array();
		foreach ( $field_value as $item ) {
			if ( isset( $item['marketplace_id'] ) ) {
				$covered_marketplaces[] = $item['marketplace_id'];
			}
		}
		$template_item = reset( $field_value );
		foreach ( $marketplace_ids as $marketplace_id ) {
			if ( ! in_array( $marketplace_id, $covered_marketplaces, true ) ) {
				$new_item                   = $template_item;
				$new_item['marketplace_id'] = $marketplace_id;
				$field_value[]              = $new_item;
			}
		}
	}

	/**
	 * Fetch seller_id and marketplace_id for a given channel.
	 *
	 * @param string $channel_id Channel UUID.
	 *
	 * @return array|WP_Error
	 */
	private function get_channel_details( string $channel_id ) {
		$channel = $this->multichannels_model->get_channel_by_uuid( $channel_id );
		if ( ! $channel ) {
			return new WP_Error( 'invalid_channel', 'Channel not found' );
		}
		$marketplace_id = ! empty( $channel['region'] ) ? $channel['region'] : '';
		if ( empty( $marketplace_id ) ) {
			return new WP_Error( 'invalid_marketplace_id', 'Marketplace ID not found' );
		}
		$seller_id = isset( $channel['seller_id'] ) ? $channel['seller_id'] : $channel_id;
		return array(
			'seller_id'      => $seller_id,
			'marketplace_id' => $marketplace_id,
		);
	}

	/**
	 * Bulk fetch WooCommerce products by IDs.
	 *
	 * Supports fetching all product types including variations.
	 * Uses WC_Product_Query for batch fetching with a fallback to
	 * wc_get_product() for any missing products (typically variations).
	 *
	 * @param int[] $ids Product IDs.
	 *
	 * @return array<int, \WC_Product>
	 */
	private function get_products_by_ids( array $ids ) {
		if ( empty( $ids ) ) {
			return array();
		}

		$map = array();

		// First, try batch query with all product types including variations.
		$query    = new \WC_Product_Query(
			array(
				'limit'   => count( $ids ),
				'return'  => 'objects',
				'orderby' => 'post__in',
				'include' => $ids,
				'type'    => array( 'simple', 'variable', 'variation', 'grouped', 'external' ),
			)
		);
		$products = $query->get_products();

		foreach ( $products as $prod ) {
			$map[ $prod->get_id() ] = $prod;
		}

		// Fallback: fetch any missing products individually (handles edge cases with variations).
		$missing_ids = array_diff( $ids, array_keys( $map ) );
		if ( ! empty( $missing_ids ) ) {
			foreach ( $missing_ids as $missing_id ) {
				$product = wc_get_product( $missing_id );
				if ( $product ) {
					$map[ $product->get_id() ] = $product;
				}
			}
		}

		return $map;
	}

	/**
	 * Maximum number of polling attempts before giving up.
	 */
	private const MAX_POLL_ATTEMPTS = 10;

	/**
	 * Base delay in seconds for polling (used for exponential backoff).
	 */
	private const BASE_POLL_DELAY = 30;

	/**
	 * Maximum delay in seconds for polling (cap for exponential backoff).
	 */
	private const MAX_POLL_DELAY = 600;

	/**
	 * Queue a one-off WP-Cron event to poll Amazon for feed status.
	 *
	 * Uses exponential backoff: delay = min(BASE_DELAY * 2^(attempt-1), MAX_DELAY)
	 * - Attempt 1: 30s
	 * - Attempt 2: 60s
	 * - Attempt 3: 120s
	 * - Attempt 4: 240s
	 * - Attempt 5+: 480s, 600s (capped)
	 *
	 * @param string      $feed_id    The Amazon feed identifier.
	 * @param int         $action_id  Action Scheduler job that created the feed.
	 * @param string|null $channel_id Channel UUID.
	 * @param int         $attempt    Current polling attempt number (1-based).
	 *
	 * @return void
	 */
	public function schedule_feed_status_check( $feed_id, int $action_id = 0, $channel_id = null, int $attempt = 1 ) {
		// Check if max attempts reached.
		if ( $attempt > self::MAX_POLL_ATTEMPTS ) {
			$this->logger->error( 'Max polling attempts (' . self::MAX_POLL_ATTEMPTS . ') reached for feed ID: ' . $feed_id . '. Marking as failed.' );
			if ( $action_id ) {
				AS_Log_Helper::log(
					$action_id,
					'error',
					0,
					array(
						'stage'    => 'feed_poll_max_attempts',
						'feed_id'  => $feed_id,
						'attempts' => $attempt - 1,
						'message'  => 'Feed status polling exceeded maximum attempts. Please check manually.',
					)
				);
			}
			return;
		}

		$transient_key = Plugin_Constants::TRANSIENT_FEED_STATUS_CRON_PREFIX . $feed_id;
		if ( false !== get_transient( $transient_key ) ) {
			$this->logger->debug( 'Feed status cron already scheduled for feed ID: ' . $feed_id . '; skipping duplicate.' );
			return;
		}

		// Calculate exponential backoff delay: BASE_DELAY * 2^(attempt-1), capped at MAX_DELAY.
		$delay = min( self::BASE_POLL_DELAY * pow( 2, $attempt - 1 ), self::MAX_POLL_DELAY );

		$args = array( $feed_id, $action_id, $channel_id, $attempt );
		if ( ! wp_next_scheduled( Plugin_Constants::CRON_CHECK_FEED_STATUS, $args ) ) {
			wp_schedule_single_event( time() + $delay, Plugin_Constants::CRON_CHECK_FEED_STATUS, $args );
			set_transient( $transient_key, 1, $delay );
			$this->logger->info( 'Scheduled feed status check for feed ID: ' . $feed_id . ' in ' . $delay . ' seconds (attempt ' . $attempt . '/' . self::MAX_POLL_ATTEMPTS . ')' );
		}
	}

	/**
	 * Store feed mapping file in WordPress uploads folder.
	 *
	 * @param string $feed_id  Feed ID from Amazon.
	 * @param array  $payload  Payload sent.
	 * @param array  $message_id_to_sku_mapping Mapping of messageId to SKU.
	 * @return void
	 */
	public function store_feed_mapping_file( $feed_id, $payload, $message_id_to_sku_mapping ) {
		$upload_dir = wp_upload_dir();
		if ( ! $upload_dir || ! empty( $upload_dir['error'] ) ) {
			$this->logger->error( 'Failed to get WordPress uploads directory: ' . ( $upload_dir['error'] ?? 'Unknown error' ) );
			return;
		}

		$feed_data = array(
			'feed_id'                   => $feed_id,
			'upload_time'               => current_time( 'c' ),
			'payload'                   => $payload,
			'message_id_to_sku_mapping' => $message_id_to_sku_mapping,
		);

		$file_path = trailingslashit( $upload_dir['basedir'] ) . $feed_id . '.json';
		$json_data = wp_json_encode( $feed_data, JSON_PRETTY_PRINT );
		if ( false === $json_data ) {
			$this->logger->error( 'Failed to encode feed mapping data to JSON for feed ID: ' . $feed_id );
			return;
		}

		global $wp_filesystem;
		WP_Filesystem();

		if ( ! $wp_filesystem->put_contents( $file_path, $json_data, FS_CHMOD_FILE ) ) {
			$this->logger->error( 'Failed to write feed mapping file: ' . $file_path );
			return;
		}

		$this->logger->info( 'Successfully stored feed mapping file: ' . $file_path );
	}

	/**
	 * Load feed mapping file.
	 *
	 * @param string $feed_id Feed ID.
	 * @return array|null
	 */
	public function load_feed_mapping_file( $feed_id ) {
		$upload_dir = wp_upload_dir();
		if ( ! $upload_dir || ! empty( $upload_dir['error'] ) ) {
			$this->logger->warning( 'Failed to get WordPress uploads directory: ' . ( $upload_dir['error'] ?? 'Unknown error' ) );
			return null;
		}

		$file_path = trailingslashit( $upload_dir['basedir'] ) . $feed_id . '.json';
		if ( ! file_exists( $file_path ) ) {
			$this->logger->info( 'Feed mapping file not found: ' . $file_path . ' - processing feed response normally' );
			return null;
		}

		global $wp_filesystem;
		WP_Filesystem();

		$json_data = $wp_filesystem->get_contents( $file_path );
		if ( false === $json_data ) {
			$this->logger->warning( 'Failed to read feed mapping file: ' . $file_path );
			return null;
		}

		$feed_data = json_decode( $json_data, true );
		if ( JSON_ERROR_NONE !== json_last_error() ) {
			$this->logger->warning( 'Failed to decode feed mapping JSON for feed ID: ' . $feed_id . ' - Error: ' . json_last_error_msg() );
			return null;
		}
		if ( ! isset( $feed_data['message_id_to_sku_mapping'] ) ) {
			$this->logger->warning( 'Feed mapping file missing message_id_to_sku_mapping key for feed ID: ' . $feed_id );
			return null;
		}
		$this->logger->info( 'Successfully loaded feed mapping file for feed ID: ' . $feed_id . ' with ' . count( $feed_data['message_id_to_sku_mapping'] ) . ' mappings' );
		return $feed_data['message_id_to_sku_mapping'];
	}

	/**
	 * Poll Amazon for the current processing status of a feed.
	 *
	 * Uses exponential backoff for polling with attempt tracking.
	 *
	 * @param string      $feed_id    Feed ID returned by create_feed().
	 * @param int         $action_id  Original Action Scheduler job ID for consolidated logging.
	 * @param string|null $channel_id Channel UUID.
	 * @param int         $attempt    Current polling attempt number (1-based).
	 *
	 * @return void
	 */
	public function check_feed_status( $feed_id, int $action_id = 0, $channel_id = null, int $attempt = 1 ) {
		$this->logger->info( 'Checking status for feed ID: ' . $feed_id . ' (attempt ' . $attempt . '/' . self::MAX_POLL_ATTEMPTS . ')' );

		$poll_log_base = array( 'log_action_id' => $action_id );
		if ( $action_id ) {
			AS_Log_Helper::log(
				$action_id,
				'info',
				0,
				array(
					'stage'   => 'feed_poll_attempt_request',
					'feed_id' => $feed_id,
					'attempt' => $attempt,
				)
			);
		}

		$status_result = $this->get_feed_status( $feed_id, $channel_id );
		if ( is_wp_error( $status_result ) ) {
			$this->logger->error( 'Failed to get feed status: ' . $status_result->get_error_message() );
			// Increment attempt on error and reschedule with backoff.
			$this->schedule_feed_status_check( $feed_id, $action_id, $channel_id, $attempt + 1 );
			return;
		}

		$status = $status_result['processingStatus'];
		if ( $action_id ) {
			AS_Log_Helper::log(
				$action_id,
				'info',
				0,
				array(
					'stage'   => 'getting_status',
					'feed_id' => $feed_id,
					'status'  => $status,
					'attempt' => $attempt,
					'message' => 'Getting Status: ' . $status,
				)
			);
		}

		if ( 'DONE' === $status || 'CANCELLED' === $status || 'FATAL' === $status ) {
			$this->logger->info( 'Feed processing completed for feed ID: ' . $feed_id . ' with status: ' . $status . ' after ' . $attempt . ' poll attempts' );
			if ( isset( $status_result['resultFeedDocumentId'] ) ) {
				$this->process_feed_result( $feed_id, $status_result['resultFeedDocumentId'], $action_id, $channel_id, $poll_log_base );
			} else {
				$this->logger->info( 'No result document available for feed ID: ' . $feed_id );
			}
		} elseif ( 'IN_PROGRESS' === $status || 'IN_QUEUE' === $status ) {
			$this->logger->info( 'Feed still processing for feed ID: ' . $feed_id . ', status: ' . $status );
			// Increment attempt for next poll with exponential backoff.
			$this->schedule_feed_status_check( $feed_id, $action_id, $channel_id, $attempt + 1 );
		} else {
			$this->logger->error( 'Feed processing failed for feed ID: ' . $feed_id . ', status: ' . $status );
		}

		if ( $action_id ) {
			AS_Log_Helper::log(
				$action_id,
				'DONE' === $status ? 'success' : 'error',
				0,
				array(
					'stage'         => 'DONE' === $status ? 'feed_processing_completed' : 'feed_failed',
					'feed_id'       => $feed_id,
					'status'        => $status,
					'poll_attempts' => $attempt,
					'message'       => 'DONE' === $status ? 'Processing Feed result completed.' : 'Feed processing failed.',
				)
			);
		}
	}

	/**
	 * Download and process feed result document.
	 *
	 * @param string      $feed_id Feed ID.
	 * @param string      $result_document_id Document ID.
	 * @param int         $action_id Action log id.
	 * @param string|null $channel_id Channel UUID.
	 * @param array       $poll_log_base Context.
	 * @return void
	 */
	private function process_feed_result( $feed_id, $result_document_id, int $action_id = 0, $channel_id = null, $poll_log_base = array() ) {
		$document_result = $this->download_feed_result( $result_document_id, $channel_id );
		if ( is_wp_error( $document_result ) ) {
			$this->logger->error( 'Failed to download feed result: ' . $document_result->get_error_message() );
			return;
		}

		$this->process_feed_response( $feed_id, $document_result, $action_id );
	}

	/**
	 * Process feed JSON response.
	 * Uses the mapping file to resolve messageId→SKU.
	 *
	 * @param string $feed_id    Feed ID.
	 * @param array  $response   Feed response.
	 * @param int    $action_id  Action ID.
	 *
	 * @return void
	 */
	private function process_feed_response( $feed_id, $response, int $action_id = 0 ) {
		$message_id_to_sku_mapping = $this->load_feed_mapping_file( $feed_id );

		if ( empty( $response ) || ! isset( $response['header'] ) ) {
			$this->logger->error( 'Invalid feed response format for feed ID: ' . $feed_id );
			return;
		}

		$error_skus        = array();
		$issues_by_product = array();

		if ( isset( $response['issues'] ) && ! empty( $response['issues'] ) ) {
			foreach ( $response['issues'] as $issue ) {
				$message_id = isset( $issue['messageId'] ) ? $issue['messageId'] : null;
				$sku        = null;
				if ( $message_id && ! empty( $message_id_to_sku_mapping ) && isset( $message_id_to_sku_mapping[ $message_id ] ) ) {
					$sku = $message_id_to_sku_mapping[ $message_id ];
				} else {
					$sku = isset( $issue['sku'] ) ? $issue['sku'] : 'Unknown SKU';
					if ( $message_id ) {
						$this->logger->warning( 'MessageId ' . $message_id . ' not found in mapping file for feed ' . $feed_id . ', using fallback SKU: ' . $sku );
					}
				}

				$code     = isset( $issue['code'] ) ? $issue['code'] : 'Unknown Code';
				$severity = isset( $issue['severity'] ) ? strtoupper( $issue['severity'] ) : 'WARNING';
				$message  = isset( $issue['message'] ) ? $issue['message'] : 'Unknown issue';

				if ( ! in_array( $sku, $error_skus, true ) ) {
					$error_skus[] = $sku;
				}

				if ( ! isset( $issues_by_product[ $sku ] ) ) {
					$issues_by_product[ $sku ] = array(
						'ERROR'   => array(),
						'WARNING' => array(),
					);
				}
				$issues_by_product[ $sku ][ $severity ][] = $issue;

				$this->logger->warning( 'Feed issue for SKU ' . $sku . ': [' . $severity . '] ' . $code . ' - ' . $message );

				if ( $action_id ) {
					$product_id = wc_get_product_id_by_sku( $sku ) ? wc_get_product_id_by_sku( $sku ) : 0;
					AS_Log_Helper::log(
						$action_id,
						strtolower( 'ERROR' === $severity ? 'error' : ( 'WARNING' === $severity ? 'warning' : 'info' ) ),
						$product_id,
						array(
							'stage'   => 'feed_result_error',
							'sku'     => $sku,
							'code'    => $code,
							'message' => $message,
						)
					);
				}
			}
		}

		foreach ( $issues_by_product as $sku => $grouped_issues ) {
			if ( empty( $sku ) || 'Unknown SKU' === $sku ) {
				continue;
			}
			$product_id = wc_get_product_id_by_sku( $sku );
			if ( ! $product_id ) {
				$this->logger->warning( 'Product not found for SKU: ' . $sku . ', cannot store issues' );
				continue;
			}
			update_post_meta( $product_id, '_momcs_amz_listing_issues', wp_json_encode( $grouped_issues ) );
			$this->logger->info( 'Stored ' . ( count( $grouped_issues['ERROR'] ) + count( $grouped_issues['WARNING'] ) ) . ' issues for SKU: ' . $sku );
		}

		if ( ! empty( $message_id_to_sku_mapping ) ) {
			$feed_skus    = array_values( $message_id_to_sku_mapping );
			$success_skus = array_diff( $feed_skus, $error_skus );
			foreach ( $success_skus as $sku ) {
				if ( empty( $sku ) || 'Unknown SKU' === $sku ) {
					continue;
				}
				$product_id = wc_get_product_id_by_sku( $sku );
				if ( ! $product_id ) {
					continue;
				}
				update_post_meta( $product_id, '_momcs_amz_listing_status', 'processing' );
				$this->logger->info( "Set listing status to 'processing' for SKU: {$sku}" );
				if ( $action_id ) {
					AS_Log_Helper::log(
						$action_id,
						'success',
						(int) $product_id,
						array(
							'stage'   => 'feed_result_processing',
							'sku'     => $sku,
							'message' => 'Product marked as processing - no errors found in feed response',
						)
					);
				}
			}
		} else {
			$this->logger->warning( 'No messageId mapping available for feed ' . $feed_id . ' - cannot determine success SKUs' );
		}
	}

	/**
	 * Build a minimal delete feed payload for the provided WooCommerce product IDs.
	 *
	 * @param array $product_ids Product IDs.
	 *
	 * @return array|WP_Error
	 */
	private function build_delete_feed_payload( array $product_ids ) {
		$messages = array();
		foreach ( $product_ids as $product_id ) {
			$product = wc_get_product( $product_id );
			if ( ! $product ) {
				continue;
			}
			$sku = $product->get_sku();
			if ( empty( $sku ) ) {
				$sku = (string) $product_id;
			}
			$messages[] = array(
				'sku'           => $sku,
				'operationType' => 'DELETE',
				'productType'   => 'PRODUCT',
			);
		}
		if ( empty( $messages ) ) {
			return new WP_Error( 'empty_payload', 'No valid products to unlist' );
		}
		return array( 'messages' => $messages );
	}

	/**
	 * Unlist products using Feeds API by sending DELETE messages.
	 *
	 * @param array       $product_ids      Product IDs.
	 * @param array       $marketplace_ids  Marketplaces.
	 * @param int         $action_id        Action ID.
	 * @param string|null $channel_id       Channel UUID.
	 *
	 * @return array
	 */
	public function unlist_products_with_feeds_api( array $product_ids, array $marketplace_ids, int $action_id = 0, $channel_id = null ) {
		if ( empty( $marketplace_ids ) ) {
			return array(
				'success' => array(),
				'error'   => array( 'marketplace_ids' => 'Marketplace IDs are required' ),
			);
		}

		if ( $action_id ) {
			AS_Log_Helper::log(
				$action_id,
				'info',
				0,
				array(
					'stage'         => 'feed_unlist_request',
					'product_count' => count( $product_ids ),
					'marketplaces'  => $marketplace_ids,
				)
			);
		}

		$feed_log_base   = array( 'log_action_id' => $action_id );
		$document_result = $this->create_feed_document( $marketplace_ids, $feed_log_base + array( 'stage' => 'feed_unlist_create_document' ), $channel_id ?? '' );
		if ( is_wp_error( $document_result ) ) {
			return array(
				'success' => array(),
				'error'   => array( 'feed_document' => $document_result->get_error_message() ),
			);
		}

		$payload = $this->build_delete_feed_payload( $product_ids );
		if ( is_wp_error( $payload ) ) {
			return array(
				'success' => array(),
				'error'   => array( 'feed_payload' => $payload->get_error_message() ),
			);
		}

		$upload_result = $this->upload_feed_payload( $document_result['url'], $payload, $feed_log_base + array( 'stage' => 'feed_unlist_upload' ) );
		if ( is_wp_error( $upload_result ) ) {
			return array(
				'success' => array(),
				'error'   => array( 'feed_upload' => $upload_result->get_error_message() ),
			);
		}

		$feed_result = $this->create_feed( $document_result['feedDocumentId'], $marketplace_ids, $feed_log_base + array( 'stage' => 'feed_unlist_create_feed' ), $channel_id );
		if ( is_wp_error( $feed_result ) ) {
			return array(
				'success' => array(),
				'error'   => array( 'feed_creation' => $feed_result->get_error_message() ),
			);
		}

		if ( $action_id ) {
			AS_Log_Helper::log(
				$action_id,
				'success',
				0,
				array(
					'stage'   => 'feed_unlist_created',
					'feed_id' => $feed_result['feedId'],
				)
			);
		}

		$this->schedule_feed_status_check( $feed_result['feedId'], $action_id, $channel_id );
		return array(
			'success' => array( 'feed_id' => $feed_result['feedId'] ),
			'error'   => array(),
		);
	}

	/**
	 * Proxy for Feeds_API_Service::create_feed_document().
	 *
	 * @param array  $marketplace_ids Marketplace IDs.
	 * @param array  $log_context     Log context for logging purposes.
	 * @param string $channel_id      Channel UUID.
	 *
	 * @return array|\WP_Error
	 *
	 * @throws \MOMCS_Sync\Includes\Common\Exceptions\Sync_Exception Feed exception.
	 */
	public function create_feed_document( array $marketplace_ids = array(), array $log_context = array(), string $channel_id = '' ) {
		return $this->feeds_api_service->create_feed_document( $marketplace_ids, $log_context, $channel_id );
	}

	/**
	 * Proxy for Feeds_API_Service::upload_feed_payload().
	 *
	 * @param string $upload_url  Pre-signed S3 upload URL.
	 * @param mixed  $payload     Feed payload (string|resource|array).
	 * @param array  $log_context Optional log context.
	 *
	 * @return array|\WP_Error
	 *
	 * @throws \MOMCS_Sync\Includes\Common\Exceptions\Amazon_Api_Exception Amazon API exception.
	 */
	public function upload_feed_payload( string $upload_url, $payload, array $log_context = array() ) {
		return $this->feeds_api_service->upload_feed_payload( $upload_url, $payload, $log_context );
	}

	/**
	 * Proxy for Feeds_API_Service::create_feed().
	 *
	 * @param string      $feed_document_id Feed document ID.
	 * @param array       $marketplace_ids  Marketplace IDs.
	 * @param array       $log_context      Log context.
	 * @param string|null $channel_id       Channel UUID.
	 *
	 * @return array|\WP_Error
	 *
	 * @throws \MOMCS_Sync\Includes\Common\Exceptions\Amazon_Api_Exception Amazon API exception.
	 */
	public function create_feed( string $feed_document_id, array $marketplace_ids, array $log_context = array(), ?string $channel_id = null ) {
		return $this->feeds_api_service->create_feed( $feed_document_id, $marketplace_ids, $log_context, $channel_id ?? '' );
	}

	/**
	 * Get marketplace ID from channel configuration.
	 *
	 * @param string $channel_id The channel UUID.
	 * @return string|null The marketplace ID or null if not found.
	 */
	private function get_marketplace_id_from_channel( string $channel_id ): ?string {
		if ( empty( $channel_id ) ) {
			return null;
		}

		$channel = $this->multichannels_model->get_channel_by_uuid( $channel_id );

		if ( empty( $channel ) || 'amazon' !== $channel['marketplace'] ) {
			return null;
		}

		return $channel['region'] ?? null;
	}

	/**
	 * =========================================================================
	 * CHUNKED FEED BUILDING
	 * =========================================================================
	 * When syncing 1000+ products, the feed building is split into smaller
	 * chunks processed by separate Action Scheduler actions. This prevents
	 * PHP timeouts and allows automatic resume if a chunk fails.
	 * =========================================================================
	 */

	/**
	 * Store chunked feed build progress to transient.
	 *
	 * @param string $job_id   Unique job identifier.
	 * @param array  $progress Progress data to store.
	 * @return bool True on success, false on failure.
	 */
	private function store_feed_build_progress( string $job_id, array $progress ): bool {
		$transient_key            = Plugin_Constants::TRANSIENT_FEED_BUILD_PREFIX . $job_id;
		$progress['last_updated'] = current_time( 'c' );

		// Store for 24 hours - should be plenty for even slow builds.
		return set_transient( $transient_key, $progress, DAY_IN_SECONDS );
	}

	/**
	 * Retrieve chunked feed build progress from transient.
	 *
	 * @param string $job_id Unique job identifier.
	 * @return array|false Progress data or false if not found.
	 */
	private function get_feed_build_progress( string $job_id ) {
		$transient_key = Plugin_Constants::TRANSIENT_FEED_BUILD_PREFIX . $job_id;
		return get_transient( $transient_key );
	}

	/**
	 * Delete chunked feed build progress transient.
	 *
	 * @param string $job_id Unique job identifier.
	 * @return bool True on success, false on failure.
	 */
	private function delete_feed_build_progress( string $job_id ): bool {
		$transient_key = Plugin_Constants::TRANSIENT_FEED_BUILD_PREFIX . $job_id;
		return delete_transient( $transient_key );
	}

	/**
	 * Start chunked feed building for large product sets.
	 *
	 * Creates initial job state, persists it, and schedules the first chunk
	 * processing action via Action Scheduler.
	 *
	 * @param array       $product_ids               WooCommerce product IDs.
	 * @param array       $marketplace_ids           Amazon marketplace IDs.
	 * @param string|null $channel_id                Channel UUID.
	 * @param array       $product_template_mappings Map of product_id => ['product_type' => string, 'template_config' => array].
	 * @return array Result structure with job_id or error.
	 */
	public function start_chunked_feed_build( array $product_ids, array $marketplace_ids, ?string $channel_id = null, array $product_template_mappings = array() ): array {
		$current_action_id = Current_Action::id();
		$job_id            = wp_generate_uuid4();

		$this->logger->info(
			'Starting chunked feed build',
			array(
				'job_id'        => $job_id,
				'product_count' => count( $product_ids ),
				'chunk_size'    => Plugin_Constants::FEED_BUILD_CHUNK_SIZE,
			)
		);

		if ( $current_action_id ) {
			AS_Log_Helper::log(
				$current_action_id,
				'info',
				0,
				array(
					'stage'         => 'chunked_feed_build_start',
					'job_id'        => $job_id,
					'product_count' => count( $product_ids ),
					'chunk_size'    => Plugin_Constants::FEED_BUILD_CHUNK_SIZE,
					'message'       => 'Starting chunked feed build for ' . count( $product_ids ) . ' products',
				)
			);
		}

		// Initialize job progress state.
		$progress = array(
			'job_id'                    => $job_id,
			'product_ids'               => $product_ids,
			'total_products'            => count( $product_ids ),
			'channel_id'                => $channel_id,
			'marketplace_ids'           => $marketplace_ids,
			'action_id'                 => $current_action_id,
			'status'                    => 'building',
			'current_index'             => 0,
			'messages'                  => array(),
			'sku_mapping'               => array(),
			'message_counter'           => 1,
			'product_template_mappings' => $product_template_mappings,
			'started_at'                => current_time( 'c' ),
		);

		// Store initial progress.
		if ( ! $this->store_feed_build_progress( $job_id, $progress ) ) {
			$this->logger->error( 'Failed to store initial feed build progress for job: ' . $job_id );
			return array(
				'success' => array(),
				'error'   => array( 'progress_store' => 'Failed to initialize chunked feed build' ),
			);
		}

		// Schedule first chunk via Action Scheduler.
		as_enqueue_async_action(
			Plugin_Constants::CRON_BUILD_FEED_CHUNK,
			array( $job_id ),
			'momcs_feed_build'
		);

		$this->logger->info( 'Scheduled first chunk for job: ' . $job_id );

		return array(
			'success' => array(
				'job_id'  => $job_id,
				'message' => 'Chunked feed build started. Processing ' . count( $product_ids ) . ' products in chunks of ' . Plugin_Constants::FEED_BUILD_CHUNK_SIZE,
			),
			'error'   => array(),
		);
	}

	/**
	 * Process a single chunk of products in the feed build.
	 *
	 * Loads progress from transient, processes CHUNK_SIZE products,
	 * saves progress back, and either schedules next chunk or finalizes.
	 *
	 * @param string $job_id Unique job identifier.
	 * @return void
	 */
	public function process_feed_build_chunk( string $job_id ): void {
		$progress = $this->get_feed_build_progress( $job_id );

		if ( false === $progress ) {
			$this->logger->error( 'Feed build progress not found for job: ' . $job_id );
			return;
		}

		if ( 'building' !== $progress['status'] ) {
			$this->logger->warning( 'Feed build job ' . $job_id . ' is not in building status: ' . $progress['status'] );
			return;
		}

		$action_id     = $progress['action_id'] ?? 0;
		$product_ids   = $progress['product_ids'];
		$current_index = $progress['current_index'];
		$total         = $progress['total_products'];
		$chunk_size    = Plugin_Constants::FEED_BUILD_CHUNK_SIZE;

		$chunk_start = $current_index;
		$chunk_end   = min( $current_index + $chunk_size, $total );

		$this->logger->info(
			'Processing chunk',
			array(
				'job_id'      => $job_id,
				'chunk_start' => $chunk_start,
				'chunk_end'   => $chunk_end,
				'total'       => $total,
			)
		);

		if ( $action_id ) {
			AS_Log_Helper::log(
				$action_id,
				'info',
				0,
				array(
					'stage'       => 'chunked_feed_build_chunk',
					'job_id'      => $job_id,
					'chunk_start' => $chunk_start,
					'chunk_end'   => $chunk_end,
					'total'       => $total,
					'message'     => sprintf( 'Processing products %d-%d of %d', $chunk_start + 1, $chunk_end, $total ),
				)
			);
		}

		// Get channel details for seller_id (needed for messages).
		$channel_id      = $progress['channel_id'];
		$marketplace_ids = $progress['marketplace_ids'];
		$channel_details = $this->get_channel_details( (string) $channel_id );

		if ( is_wp_error( $channel_details ) ) {
			$this->logger->error( 'Failed to get channel details: ' . $channel_details->get_error_message() );
			$progress['status'] = 'failed';
			$progress['error']  = $channel_details->get_error_message();
			$this->store_feed_build_progress( $job_id, $progress );
			return;
		}

		// Get product IDs for this chunk.
		$chunk_product_ids = array_slice( $product_ids, $chunk_start, $chunk_size );

		// Fetch products in bulk.
		$products = $this->get_products_by_ids( $chunk_product_ids );

		// Load existing state.
		$messages          = $progress['messages'];
		$sku_mapping       = $progress['sku_mapping'];
		$message_counter   = $progress['message_counter'];
		$template_mappings = $progress['product_template_mappings'] ?? array();

		// Process each product in the chunk.
		foreach ( $chunk_product_ids as $product_id ) {
			$product = isset( $products[ $product_id ] ) ? $products[ $product_id ] : null;

			if ( ! $product ) {
				$this->logger->warning( 'Product not found during chunk build: ' . $product_id );
				continue;
			}

			// Resolve product type and template config from mappings (use parent's for variations).
			$resolved        = $this->resolve_product_template_for_feed( $product, $template_mappings );
			$product_type    = $resolved['product_type'];
			$template_config = $resolved['template_config'];

			$msg = $this->build_product_message(
				$product,
				$marketplace_ids,
				$channel_id,
				$product_type,
				$message_counter,
				$template_config
			);

			if ( ! is_wp_error( $msg ) ) {
				$messages[]                        = $msg;
				$sku_mapping[ $message_counter ]   = isset( $msg['sku'] ) ? $msg['sku'] : '';
				++$message_counter;
			} else {
				$this->logger->warning( 'Failed to build message for product ' . $product_id . ': ' . $msg->get_error_message() );
			}
		}

		// Update progress.
		$progress['current_index']   = $chunk_end;
		$progress['messages']        = $messages;
		$progress['sku_mapping']     = $sku_mapping;
		$progress['message_counter'] = $message_counter;

		// Check if we're done building.
		if ( $chunk_end >= $total ) {
			$this->logger->info( 'All chunks processed for job: ' . $job_id . ', proceeding to upload' );

			if ( empty( $messages ) ) {
				$this->logger->error( 'No valid products built for job: ' . $job_id );
				$progress['status'] = 'failed';
				$progress['error']  = 'No valid products to include in the feed';
				$this->store_feed_build_progress( $job_id, $progress );
				return;
			}

			// Store progress with uploading status.
			$progress['status'] = 'uploading';
			$this->store_feed_build_progress( $job_id, $progress );

			// Finalize and upload.
			$this->finalize_and_upload_feed( $job_id, $channel_details );
		} else {
			// Save progress and schedule next chunk.
			$this->store_feed_build_progress( $job_id, $progress );

			as_enqueue_async_action(
				Plugin_Constants::CRON_BUILD_FEED_CHUNK,
				array( $job_id ),
				'momcs_feed_build'
			);

			$this->logger->info( 'Scheduled next chunk for job: ' . $job_id );
		}
	}

	/**
	 * Finalize chunked feed build and upload to Amazon.
	 *
	 * Assembles the final payload from stored messages and uploads
	 * to Amazon Feeds API.
	 *
	 * @param string $job_id         Unique job identifier.
	 * @param array  $channel_details Channel details with seller_id.
	 * @return void
	 */
	private function finalize_and_upload_feed( string $job_id, array $channel_details ): void {
		$progress = $this->get_feed_build_progress( $job_id );

		if ( false === $progress || 'uploading' !== $progress['status'] ) {
			$this->logger->error( 'Invalid progress state for finalization: ' . $job_id );
			return;
		}

		$action_id       = $progress['action_id'] ?? 0;
		$messages        = $progress['messages'];
		$sku_mapping     = $progress['sku_mapping'];
		$marketplace_ids = $progress['marketplace_ids'];
		$channel_id      = $progress['channel_id'];

		$this->logger->info(
			'Finalizing feed upload',
			array(
				'job_id'        => $job_id,
				'message_count' => count( $messages ),
			)
		);

		if ( $action_id ) {
			AS_Log_Helper::log(
				$action_id,
				'info',
				0,
				array(
					'stage'         => 'chunked_feed_build_finalizing',
					'job_id'        => $job_id,
					'message_count' => count( $messages ),
					'message'       => 'Finalizing feed with ' . count( $messages ) . ' product messages',
				)
			);
		}

		// Assemble final payload.
		$payload = array(
			'header'   => array(
				'sellerId' => $channel_details['seller_id'],
				'version'  => '2.0',
			),
			'messages' => $messages,
		);

		$feed_log_base = array( 'log_action_id' => $action_id );

		// Create feed document.
		try {
			$document_result = $this->create_feed_document(
				$marketplace_ids,
				$feed_log_base + array( 'stage' => 'chunked_feed_create_document' ),
				$channel_id ?? ''
			);
		} catch ( Sync_Exception $e ) {
			$this->logger->error( 'Failed to create feed document for chunked build: ' . $e->getMessage() );
			$progress['status'] = 'failed';
			$progress['error']  = $e->getMessage();
			$this->store_feed_build_progress( $job_id, $progress );
			return;
		}

		// Upload feed payload.
		try {
			$this->upload_feed_payload(
				$document_result['url'],
				$payload,
				$feed_log_base + array( 'stage' => 'chunked_feed_upload' )
			);
		} catch ( Amazon_Api_Exception $e ) {
			$this->logger->error( 'Failed to upload chunked feed payload: ' . $e->getMessage() );
			$progress['status'] = 'failed';
			$progress['error']  = $e->getMessage();
			$this->store_feed_build_progress( $job_id, $progress );
			return;
		}

		// Create feed.
		try {
			$feed_result = $this->create_feed(
				$document_result['feedDocumentId'],
				$marketplace_ids,
				$feed_log_base + array( 'stage' => 'chunked_feed_create' ),
				$channel_id
			);
		} catch ( Amazon_Api_Exception $e ) {
			$this->logger->error( 'Failed to create feed for chunked build: ' . $e->getMessage() );
			$progress['status'] = 'failed';
			$progress['error']  = $e->getMessage();
			$this->store_feed_build_progress( $job_id, $progress );
			return;
		}

		$feed_id = $feed_result['feedId'];

		$this->logger->info( 'Chunked feed upload complete', array( 'job_id' => $job_id, 'feed_id' => $feed_id ) );

		if ( $action_id ) {
			AS_Log_Helper::log(
				$action_id,
				'success',
				0,
				array(
					'stage'   => 'chunked_feed_created',
					'job_id'  => $job_id,
					'feed_id' => $feed_id,
					'message' => 'Chunked feed build completed and uploaded',
				)
			);
		}

		// Store mapping file for result processing.
		$this->store_feed_mapping_file( $feed_id, $payload, $sku_mapping );

		// Schedule status polling.
		$this->schedule_feed_status_check( $feed_id, $action_id, $channel_id );

		// Update progress to completed.
		$progress['status']       = 'completed';
		$progress['feed_id']      = $feed_id;
		$progress['completed_at'] = current_time( 'c' );
		$this->store_feed_build_progress( $job_id, $progress );

		// Clean up transient after successful completion (delay cleanup to allow status checks).
		wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'momcs_cleanup_feed_build_progress', array( $job_id ) );
	}
}
