<?php
/**
 * Simple Product object for sync.
 *
 * @package    Common
 * @subpackage Services\Product_Sync\SyncProducts
 * @author     miniOrange <info@miniorange.com>
 * @link       https://miniorange.com
 */

namespace MOWCPS\Common\Services\Product_Sync\SyncProducts;

use MOWCPS\Common\Utils\Utils;
use MOWCPS\Common\Exception\Exception;
use MOWCPS\Common\Utils\Sync_Utils;
use MOWCPS\Common\Constants\Constants;
use MOWCPS\Common\Models\Setting_Model;
use MOWCPS\Common\Models\Sync_Details_DAO;
use MOWCPS\Common\Constants\Plugin_Filters;
use MOWCPS\Common\Services\Logging\Supplier_Logger;
use MOWCPS\Common\Services\Auditing\Supplier_Audit_Logger;

/**
 * Trait for MO Product.
 */
trait MO_Product {

	/**
	 * Fields of the Product.
	 *
	 * @var array
	 */
	public $unique_identifier;

	/**
	 * Advanced config data for the current External Supplier.
	 *
	 * @var array
	 */
	public $advanced_config;

	/**
	 * Fields of the Product.
	 *
	 * @var string|int
	 */
	public $sku;

	/**
	 * Fields of the Variable product.
	 *
	 * @var string
	 */
	public $name;

	/**
	 * Fields of the Variable product.
	 *
	 * @var string
	 */
	public $description;

	/**
	 * Fields of the Variable product.
	 *
	 * @var float|int
	 */
	public $regular_price;

	/**
	 * Fields of the Variable product.
	 *
	 * @var bool
	 */
	public $manage_stock;

	/**
	 * Fields of the Variable product.
	 *
	 * @var string
	 */
	public $images;

	/**
	 * Config data for the current External Supplier.
	 *
	 * @var array
	 */
	public $config = array();

	/**
	 * Config data for the current External attribute.
	 *
	 * @var array
	 */
	public $attr_config = array();

	/**
	 * Structure of the product.
	 *
	 * @var string
	 */
	public $structure;

	/**
	 * Product data.
	 *
	 * @var array
	 */
	public $product_data;

	/**
	 * Product status.
	 *
	 * @var string
	 */
	public $product_status;

	/**
	 * Group the configuration by type.
	 *
	 * @param array $config array of config rows from db.
	 * @return array The grouped configuration.
	 */
	public function group_config( $config ) {
		$result       = array();
		$target_types = array(
			'attribute' => true,
			'custom'    => true,
			'taxonomy'  => true,
		);

		foreach ( $config as $item ) {
			$type = $item['type'] ?? '';
			$key  = $item['wc_attribute'] ?? null;
			if ( ! $key ) {
				continue;
			}
			if ( isset( $target_types[ $type ] ) ) {
				if ( ! isset( $result[ $type ] ) ) {
					// Initialize the group structure if it doesn't exist.
					$result[ $type ] = array(
						'wc_attribute'       => $type,
						'supplier_attribute' => array(),
					);
				}
				// Add to the nested array directly.
				$result[ $type ]['supplier_attribute'][ $key ] = $item;
			} else {
				$result[ $key ] = $item;
			}
		}

		return $result;
	}

	/**
	 * Get the unique identifier of the product.
	 *
	 * @param string|false $api_unique_identifier_key Key to map from product data.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 * @return array
	 */
	public function get_unique_identifier( $api_unique_identifier_key ) {
		$unique_identifier_value = '';

		try {
			$unique_identifier_value = Sync_Utils::map_particular_attr( $api_unique_identifier_key, $this->product_data );

			if ( ! ( is_string( $unique_identifier_value ) || is_int( $unique_identifier_value ) ) ) {
				throw new \UnexpectedValueException( 'Expected int or string from ' . __METHOD__ . ', got ' . gettype( $unique_identifier_value ) . ' with value: ' . var_export( $unique_identifier_value, true ) );
			}

			if ( false !== $api_unique_identifier_key && ! empty( $unique_identifier_value ) ) {
				$unique_identifier = array(
					$this->config['unique_identifier_fields']['key'] => $unique_identifier_value,
				);
			} else {
				$unique_identifier = array();
			}
		} catch ( \Throwable $e ) {
			Exception::handle( $e, 'unique identifier', $this->connection_id );
			$unique_identifier = array();
		} finally {
			return $unique_identifier;
		}
	}

	/**
	 * Get the SKU of the product.
	 *
	 * @param string|false $api_sku_key Key to map from product data.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 * @return string|int
	 */
	public function get_sku( $api_sku_key ) {
		$sku = '';

		try {
			$sku = Sync_Utils::map_particular_attr( $api_sku_key, $this->product_data );

			if ( ! ( is_string( $sku ) || is_int( $sku ) ) ) {
				throw new \UnexpectedValueException( 'Expected int or string from ' . __METHOD__ . ', got ' . gettype( $sku ) . ' with value: ' . var_export( $sku, true ) );
			}
		} catch ( \Throwable $e ) {
			Exception::handle( $e, 'sku', $this->connection_id );
			$sku = '';
		} finally {
			return $sku;
		}
	}

	/**
	 * Get the name of the product.
	 *
	 * @param string|false $api_name_key Key to map from product data.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 * @return string
	 */
	protected function get_name( $api_name_key ): string {
		$name = '';
		try {
			/**
			 * This filter position is to perform any operations before mapping the name attribute.
			 * This allows suppliers to enhance product data with additional API calls.
			 *
			 * Additional args/details should be sent as an in 2nd parameter of apply filter.
			 *
			 * $connection_name  => Current connection name number.
			 * $product_data     => API Product data for the current product.
			 * $api_name_key     => The key to map from product data.
			 * $product_data     => Enhanced product data after applying the filter.
			 */
			if ( has_filter( 'mowcps_data_alteration_filter' ) ) {
				$returned_data      = apply_filters(
					'mowcps_data_alteration_filter',
					array(
						'event_specifier' => 'update_product_data',
						'connection_id'   => $this->connection_id,
						'product_data'    => $this->product_data,
						'api_name_key'    => $api_name_key,
					)
				);
				$this->product_data = $returned_data['product_data'];
			}

			$name = Sync_Utils::map_particular_attr( $api_name_key, $this->product_data );

			if ( ! is_string( $name ) ) {
				throw new \UnexpectedValueException( 'Expected string from ' . __METHOD__ . ', got ' . gettype( $name ) . ' with value: ' . var_export( $name, true ) );
			}
		} catch ( \Throwable $e ) {
			Exception::handle( $e, 'name', $this->connection_id );
			$name = '';
		} finally {
			return $name;
		}
	}

	/**
	 * Get the description of the product.
	 *
	 * @param string|false $api_description_key Key to map from product data.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 * @return string
	 */
	protected function get_description( $api_description_key ): string {
		$description = '';
		try {
						/**
			 * This filter position is to perform any operations before mapping the description attribute.
			 * This allows suppliers to enhance product data with additional API calls.
			 *
			 * Additional args/details should be sent as an in 2nd parameter of apply filter.
			 *
			 * $connection_name     => Current connection name number.
			 * $product_data        => API Product data for the current product.
			 * $api_description_key => The key to map from product data.
			 * $product_data        => Enhanced product data after applying the filter.
			 */
			if ( has_filter( 'mowcps_data_alteration_filter' ) ) {
				$returned_data      = apply_filters(
					'mowcps_data_alteration_filter',
					array(
						'event_specifier'     => 'update_product_data',
						'connection_id'       => $this->connection_id,
						'product_data'        => $this->product_data,
						'api_description_key' => $api_description_key,
					)
				);
				$this->product_data = $returned_data['product_data'];
			}

			$description = Sync_Utils::map_particular_attr( $api_description_key, $this->product_data );

			/**
			 * This filter position is to perform any operations at the time of getting description.
			 * An array should be returned from the filter at the end which will eventually be synced as description.
			 *
			 * Additional args/details should be sent as an in 2nd parameter of apply filter.
			 *
			 * $connection_name  => Current connection name number.
			 * $product_data     => API Product data for the current product.
			 * $description      => Description value received from the API attribute.
			 * $description      => Description value after applying the filter.
			 */
			if ( has_filter( 'mowcps_data_alteration_filter' ) ) {
				$returned_data = apply_filters(
					'mowcps_data_alteration_filter',
					array(
						'event_specifier' => 'get_description',
						'connection_id'   => $this->connection_id,
						'product_data'    => $this->product_data,
						'description'     => $description,
					)
				);
				$description   = $returned_data['description'];
			}

			if ( Utils::is_flat_array( $description ) ) {
				$description = implode( ', ', $description );
			}

			if ( ! is_string( $description ) ) {
				throw new \UnexpectedValueException( 'Expected string from ' . __METHOD__ . ', got ' . gettype( $description ) . ' with value: ' . var_export( $description, true ) );
			}
		} catch ( \Throwable $e ) {
			Exception::handle( $e, 'description', $this->connection_id );
			$description = '';
		} finally {
			return $description;
		}
	}

	/**
	 * Get the regular price of the product.
	 *
	 * @param string|false $api_regular_price_key Key to map from product data.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 * @return int|float|string
	 */
	public function get_regular_price( $api_regular_price_key ) {
		$regular_price = '';
		try {
			$regular_price = Sync_Utils::map_particular_attr( $api_regular_price_key, $this->product_data );

			/**
			 * This filter position is to perform any operations at the time of getting regular price.
			 * An array should be returned from the filter at the end which will eventually be synced as regular price.
			 *
			 * Additional args/details should be sent as an in 2nd parameter of apply filter.
			 *
			 * $connection_name  => Current connection name number.
			 * $product_data     => API Product data for the current product.
			 * $regular_price    => Regular price value received from the API attribute.
			 * $regular_price    => Regular price value after applying the filter.
			 */
			if ( has_filter( 'mowcps_data_alteration_filter' ) ) {
				$returned_data = apply_filters(
					'mowcps_data_alteration_filter',
					array(
						'event_specifier' => 'get_regular_price',
						'connection_name' => $this->connection_id,
						'product_data'    => $this->product_data,
						'regular_price'   => $regular_price,
					)
				);
				$regular_price = $returned_data['regular_price'];
			}

			/**
			 * This filter position is to perform any operations after pricing margin is applied to regular price.
			 * An array should be returned from the filter at the end which will eventually be synced as regular price.
			 *
			 * Additional args/details should be sent as an in 2nd parameter of apply filter.
			 *
			 * $connection_name  => Current connection name number.
			 * $product_data     => API Product data for the current product.
			 * $regular_price    => Regular price value after applying pricing margin.
			 * $regular_price    => Regular price value after applying this filter.
			 */
			if ( has_filter( 'mowcps_data_alteration_filter' ) ) {
				$returned_data = apply_filters(
					'mowcps_data_alteration_filter',
					array(
						'event_specifier' => 'get_regular_price_after_margin',
						'connection_name' => $this->connection_id,
						'product_data'    => $this->product_data,
						'regular_price'   => $regular_price,
					)
				);
				$regular_price = $returned_data['regular_price'];
			}

			// Convert string numbers to actual numbers.
			if ( is_string( $regular_price ) && is_numeric( $regular_price ) ) {
				$regular_price = strpos( $regular_price, '.' ) !== false ? (float) $regular_price : (int) $regular_price;
			}

			$is_valid_price = Sync_Utils::validate_price_format( $regular_price );

			if ( ! empty( $regular_price ) && ! $is_valid_price ) {
				throw new \UnexpectedValueException( 'Expected numeric value from ' . __METHOD__ . ', got ' . gettype( $regular_price ) . ' with value: ' . var_export( $regular_price, true ) );
			}
		} catch ( \Throwable $e ) {
			Exception::handle( $e, 'regular price', $this->connection_id );
			$regular_price = '';
		} finally {
			return $regular_price;
		}
	}

	/**
	 * Get the stock management of the product.
	 *
	 * @param string|false $api_manage_stock Key to map from product data.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 * @return int
	 */
	protected function get_manage_stock( $api_manage_stock ): int {
		try {
			$manage_stock = Sync_Utils::map_particular_attr( $api_manage_stock, $this->product_data );

			if ( ! ( is_int( $manage_stock ) || is_bool( $manage_stock ) || is_string( $manage_stock ) ) ) {
				throw new \UnexpectedValueException( 'Expected int, boolean, or string from ' . __METHOD__ . ', got ' . gettype( $manage_stock ) . ' with value: ' . var_export( $manage_stock, true ) );
			}

			$manage_stock = (int) $manage_stock;
		} catch ( \Throwable $e ) {
			Exception::handle( $e, 'stock management', $this->connection_id );
		} finally {
			return $manage_stock;
		}
	}

	/**
	 * Get the image URLs of the product.
	 *
	 * @param string|false $api_image_key Key to map from product data.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 * @return array
	 */
	protected function get_images( $api_image_key ): array {
		$images = array();

		try {
			$images = Sync_Utils::fetch_data_from_multiple_keys( $api_image_key, $this->product_data, array() );
			$images = $this->convert_image_array( $images );
			$images = Sync_Utils::perform_additional_operations_on_value( $images, $this->product_data );

			if ( has_filter( 'mowcps_data_alteration_filter' ) ) {
				$returned_data = apply_filters(
					'mowcps_data_alteration_filter',
					array(
						'event_specifier' => 'alter_product_images',
						'images'          => $images,
						'product_data'    => $this->product_data,
					)
				);

				$images = $returned_data['images'];

			}
			if ( false !== strpos( Constants::MOWCPS_PLAN_NAME, 'standard' ) && is_array( $images ) && count( $images ) > 2 ) {
				$images = array_slice( $images, 0, 2 );
			}

			if ( ! is_array( $images ) ) {
				throw new \UnexpectedValueException( 'Expected array from ' . __METHOD__ . ', got ' . gettype( $images ) . ' with value: ' . var_export( $images, true ) );
			}
		} catch ( \Throwable $e ) {
			Exception::handle( $e, 'images', $this->connection_id );
			$images = array();
		} finally {
			return $images;
		}
	}

	/**
	 * Convert images list to array.
	 *
	 * @param array|string $image_list List of images.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 *
	 * @return array
	 */
	public function convert_image_array( $image_list ) {
		$internal_key = ! empty( $this->advanced_config['image_src_attr'] ) ? $this->advanced_config['image_src_attr'] : '';
		$pre_path     = ! empty( $this->advanced_config['image_path_attr'] ) ? $this->advanced_config['image_path_attr'] : '';
		if ( '' !== $internal_key ) {
			$return_array = array();
			foreach ( $image_list as $image ) {
				$image_url = Sync_Utils::map_particular_attr( $internal_key, $image );
				if ( false !== $image_url ) {
					array_push( $return_array, $image_url );
				}
			}
			$image_list = $return_array;
		}

		// Split comma-separated entries across the entire list.
		if ( is_array( $image_list ) && ! empty( $image_list ) ) {
			$flattened = array();
			foreach ( $image_list as $image_value ) {
				if ( is_string( $image_value ) && ( false !== strpos( $image_value, ',https://' ) || false !== strpos( $image_value, ', https://' ) || false !== strpos( $image_value, ',http://' ) || false !== strpos( $image_value, ', http://' ) || ( ! empty( $pre_path ) && false !== strpos( $image_value, ',' ) ) ) ) {
					$parts = array_map( 'trim', explode( ',', $image_value ) );
					foreach ( $parts as $part ) {
						if ( '' !== $part ) {
							$flattened[] = $part;
						}
					}
				} elseif ( is_string( $image_value ) ) {
					$image_value = trim( $image_value );
					if ( '' !== $image_value ) {
						$flattened[] = $image_value;
					}
				}
			}
			if ( ! empty( $flattened ) ) {
				$image_list = $flattened;
			}
		}

		if ( '' !== $pre_path ) {
			$return_array = array();
			foreach ( $image_list as $image ) {
				if ( ! empty( $image ) ) {
					array_push( $return_array, $pre_path . $image );
				}
			}
			$image_list = $return_array;
		}

		return $image_list;
	}

	/**
	 * Convert images list to array.
	 *
	 * @param array|string $image_list List of images.
	 * @throws \UnexpectedValueException If unexpected datatype is returned.
	 *
	 * @return array
	 */
	public function convert_downloadable_file_array( $downloadable_file_list ) {
		$internal_key = ! empty( $this->advanced_config['downloadable_file_src_attr'] ) ? $this->advanced_config['downloadable_file_src_attr'] : '';
		$pre_path     = ! empty( $this->advanced_config['downloadable_file_path_attr'] ) ? $this->advanced_config['downloadable_file_path_attr'] : '';
		if ( '' !== $internal_key ) {
			$return_array = array();
			foreach ( $downloadable_file_list as $downloadable_file ) {
				$downloadable_file_url = Sync_Utils::map_particular_attr( $internal_key, $downloadable_file );
				if ( false !== $downloadable_file_url ) {
					array_push( $return_array, $downloadable_file_url );
				}
			}
			$downloadable_file_list = $return_array;
		}

		// Split comma-separated entries across the entire list.
		if ( is_array( $downloadable_file_list ) && ! empty( $downloadable_file_list ) ) {
			$flattened = array();
			foreach ( $downloadable_file_list as $downloadable_file_value ) {
				if ( is_string( $downloadable_file_value ) && false !== strpos( $downloadable_file_value, ',' ) ) {
					$parts = array_map( 'trim', explode( ',', $downloadable_file_value ) );
					foreach ( $parts as $part ) {
						if ( '' !== $part ) {
							$flattened[] = $part;
						}
					}
				} elseif ( is_string( $downloadable_file_value ) ) {
					$downloadable_file_value = trim( $downloadable_file_value );
					if ( '' !== $downloadable_file_value ) {
						$flattened[] = $downloadable_file_value;
					}
				}
			}
			if ( ! empty( $flattened ) ) {
				$downloadable_file_list = $flattened;
			}
		}

		if ( '' !== $pre_path ) {
			$return_array = array();
			foreach ( $downloadable_file_list as $downloadable_file ) {
				if ( ! empty( $downloadable_file ) ) {
					array_push( $return_array, $pre_path . $downloadable_file );
				}
			}
			$downloadable_file_list = $return_array;
		}

		return $downloadable_file_list;
	}

	/**
	 * Determine the operation (add or update) based on whether product exists.
	 *
	 * @param int    $connection_id The connection ID.
	 * @param array  $product_data  The product data.
	 * @param string $product_type  The product type ('main' or 'variation').
	 * @return string Operation type: 'add' or 'update'.
	 */
	public function determine_operation( $connection_id, $product_data, $product_type ) {
		$supplier_id              = Sync_Details_DAO::get_instance()->get_supplier_id();
		$supplier_details         = Sync_Details_DAO::get_instance()->get_supplier_details( $supplier_id );
		$connection_details       = Sync_Details_DAO::get_instance()->get_connection_details( $connection_id );
		$supplier_unique_id_key   = $supplier_details['unique_product_identifier'] ?? 'mo_uid';
		$connection_unique_id_key = ! empty( $connection_details['configuration']['prerequisites'][ $product_type . '_product_unique_identifier' ] ) ? $connection_details['configuration']['prerequisites'][ $product_type . '_product_unique_identifier' ] : '';

		$unique_identifier_value                            = Sync_Utils::map_particular_attr( $connection_unique_id_key, $product_data );
		$this->unique_identifier[ $supplier_unique_id_key ] = $unique_identifier_value;

		if ( empty( $unique_identifier_value ) ) {
			// If no unique identifier, default to 'add'.
			return Constants::ADD;
		}

		$unique_identifier = array(
			$supplier_unique_id_key => $unique_identifier_value,
		);

		$product_id = Sync_Utils::get_product_by_meta( $unique_identifier );
		$product_id = (int) $product_id;

		return ( ! empty( $product_id ) && 0 !== $product_id ) ? Constants::UPDATE : Constants::ADD;
	}

	/**
	 * Check if a field should be processed based on operation and sync flags.
	 *
	 * @param array  $field     Field configuration array.
	 * @param string $operation Operation to be performed. Can be 'add' or 'update'.
	 * @return bool True if field should be processed, false otherwise.
	 */
	protected function should_process_field( $field, $operation ) {
		$advanced_config = $field['advanced_configuration'] ?? array();

		// If advanced_configuration doesn't exist or doesn't have sync flags, process the field (backward compatibility).
		if ( empty( $advanced_config ) || ( ! isset( $advanced_config['add_during_sync'] ) && ! isset( $advanced_config['update_during_sync'] ) ) ) {
			return true;
		}

		// Check add_during_sync flag for 'add' operation.
		if ( Constants::ADD === $operation ) {
			$add_during_sync = isset( $advanced_config['add_during_sync'] ) ? (bool) $advanced_config['add_during_sync'] : true;
			return $add_during_sync;
		}

		// Check update_during_sync flag for 'update' operation.
		if ( Constants::UPDATE === $operation ) {
			$update_during_sync = isset( $advanced_config['update_during_sync'] ) ? (bool) $advanced_config['update_during_sync'] : true;
			return $update_during_sync;
		}

		return true;
	}

	/**
	 * Add/update the product in WooCommerce.
	 *
	 * @param string $connection_id The connection ID.
	 * @param array  $product_data  The product data.
	 * @param string $product_type   The product type ('main' or 'variation').
	 *
	 * @return int
	 */
	public function save( $connection_id, $product_data, $product_type ) {
		$supplier_id              = Sync_Details_DAO::get_instance()->get_supplier_id();
		$supplier_details         = Sync_Details_DAO::get_instance()->get_supplier_details( $supplier_id );
		$connection_details       = Sync_Details_DAO::get_instance()->get_connection_details( $connection_id );
		$supplier_unique_id_key   = $supplier_details['unique_product_identifier'] ?? 'mo_uid';
		$connection_unique_id_key = $connection_details['configuration']['prerequisites'][ $product_type . '_product_unique_identifier' ];

		// Use unique identifier if already set (from determine_operation), otherwise get it from product data.
		if ( ! isset( $this->unique_identifier[ $supplier_unique_id_key ] ) || empty( $this->unique_identifier[ $supplier_unique_id_key ] ) ) {
			$this->unique_identifier[ $supplier_unique_id_key ] = Sync_Utils::map_particular_attr( $connection_unique_id_key, $product_data );
		}

		// Validate unique identifier.
		if ( empty( $this->unique_identifier[ $supplier_unique_id_key ] ) ) {
			Supplier_Logger::debug( $supplier_id, 'Product sync failed - Missing unique identifier (' . $supplier_unique_id_key . ')' );
			Supplier_Logger::debug( $supplier_id, 'Check your attribute mapping configuration for the unique identifier field' );
			$wc_structure = isset( $this->structure ) ? (string) $this->structure : 'simple';
			Supplier_Audit_Logger::log_product(
				$supplier_id,
				Sync_Details_DAO::get_instance()->get_schedule_id(),
				Supplier_Audit_Logger::extract_mo_product_fields_for_audit( $this ),
				'failed',
				false,
				'Missing unique identifier',
				0,
				$wc_structure,
				Supplier_Audit_Logger::get_sync_role_meta( $this )
			);
			return 0;
		}

		if ( has_filter( Plugin_Filters::FILTER_ALTER_PRODUCT_BEFORE_SAVE ) ) {
			apply_filters(
				Plugin_Filters::FILTER_ALTER_PRODUCT_BEFORE_SAVE,
				array(
					'event_specifier'   => Plugin_Filters::EVENT_TRACK_UNIQUE_IDENTIFIER,
					'supplier_id'       => $supplier_id,
					'unique_identifier' => $this->unique_identifier[ $supplier_unique_id_key ],
					'product_data'      => $product_data,
				)
			);
		}

		$setting_config        = Setting_Model::get_by_connection_and_operation( $connection_id, 'product-operations', array( 'configuration' => true ) );
		$this->product_status  = $setting_config['configuration']['available_products']['product_status'];
		$add_setting_config    = $setting_config['configuration']['available_products']['add'];
		$update_setting_config = $setting_config['configuration']['available_products']['update'];

		// Apply per-schedule add/update override when running a scheduled sync.
		$schedule_id = Sync_Details_DAO::get_instance()->get_schedule_id();
		if ( ! empty( $schedule_id ) && class_exists( 'MOWCPS\\' . MOWCPS_PLAN_NAMESPACE . '\\Models\\Sync_Schedule_Model' ) ) {
			$schedule_model_class = 'MOWCPS\\' . MOWCPS_PLAN_NAMESPACE . '\\Models\\Sync_Schedule_Model';
			$schedule_row         = $schedule_model_class::get_by_id( $schedule_id );
			if ( ! empty( $schedule_row['override_add_update'] ) ) {
				$add_setting_config    = ! empty( $schedule_row['allow_add'] ) ? Constants::CONFIGURATION_ACTIVE : false;
				$update_setting_config = ! empty( $schedule_row['allow_update'] ) ? Constants::CONFIGURATION_ACTIVE : false;
				Supplier_Logger::debug( $supplier_id, 'Schedule override applied — add: ' . ( $add_setting_config ? 'enabled' : 'disabled' ) . ', update: ' . ( $update_setting_config ? 'enabled' : 'disabled' ) );
			}
		}

		$product_id = Sync_Utils::get_product_by_meta( $this->unique_identifier );
		$product_id = (int) $product_id;

		if ( empty( $product_id ) && has_filter( Plugin_Filters::FILTER_RESOLVE_PRODUCT_ID_AFTER_META_LOOKUP ) ) {
			$product_id = (int) apply_filters(
				Plugin_Filters::FILTER_RESOLVE_PRODUCT_ID_AFTER_META_LOOKUP,
				$product_id,
				array(
					'supplier_id'            => $supplier_id,
					'supplier_details'       => $supplier_details,
					'connection_id'          => $connection_id,
					'connection_details'     => $connection_details,
					'unique_identifier'      => $this->unique_identifier,
					'supplier_unique_id_key' => $supplier_unique_id_key,
					'product_data'           => $product_data,
					'product_type'           => $product_type,
				)
			);
		}

		// Filter after getting product_id. Could be replaced by the above filter if the above is not used previously.
		if ( has_filter( Plugin_Filters::FILTER_ALTER_PRODUCT_BEFORE_SAVE ) ) {
			$returned_data = apply_filters(
				Plugin_Filters::FILTER_ALTER_PRODUCT_BEFORE_SAVE,
				array(
					'event_specifier'    => Plugin_Filters::EVENT_ALTER_PRODUCT_BEFORE_SAVE,
					'supplier_details'   => $supplier_details,
					'connection_details' => $connection_details,
					'unique_identifier'  => $this->unique_identifier[ $supplier_unique_id_key ],
					'product_data'       => $product_data,
					'product_id'         => $product_id,
					'regular_price'      => $this->regular_price,
				)
			);
			$this->regular_price = $returned_data['regular_price'];
		}

		if ( ! empty( $product_id ) && 0 !== $product_id ) {
			if ( Constants::CONFIGURATION_ACTIVE === $update_setting_config ) {
				Supplier_Logger::debug( $supplier_id, 'Updating existing product (ID: ' . $product_id . ') with unique identifier: ' . $this->unique_identifier[ $supplier_unique_id_key ] );
				return Sync_Utils::update_product( $product_id, $this );
			} else {
				Supplier_Logger::debug( $supplier_id, 'Product exists (ID: ' . $product_id . ') but updates are disabled in settings' );
				$product = wc_get_product( $product_id );
				Supplier_Audit_Logger::log_handler_save_short_circuit( $this, $product_id, 'Updates disabled in settings' );
				return array(
					'id'  => $product_id,
					'obj' => $product,
				);
			}
		} elseif ( Constants::CONFIGURATION_ACTIVE === $add_setting_config ) {
			Supplier_Logger::debug( $supplier_id, 'Creating new product with unique identifier: ' . $this->unique_identifier[ $supplier_unique_id_key ] );
			return Sync_Utils::add_product( $this );
		} elseif ( Constants::VARIABLE === $product_type && Constants::CONFIGURATION_ACTIVE === $update_setting_config && Sync_Utils::is_variation_with_existing_parent( $this ) ) {
			Supplier_Logger::debug( $supplier_id, 'Adding Variation whose parent product exists with unique identifier ' . $this->unique_identifier[ $supplier_unique_id_key ] );
			return Sync_Utils::add_product( $this );
		} else {
			Supplier_Logger::debug( $supplier_id, 'Product creation is disabled in settings for unique identifier: ' . $this->unique_identifier[ $supplier_unique_id_key ] );
			Supplier_Audit_Logger::log_handler_save_short_circuit( $this, 0, 'Product creation disabled in settings' );
		}
		return array();
	}

	/**
	 * Sync product images.
	 *
	 * @param MOWCPS\Common\Functionality\mowcps_product $mowcps_product Product object.
	 * @param integer                                    $product_id ID of the product.
	 * @param array                                      $arguments  Additional arguments required.
	 * Additional arguments:
	 * 1. connection_name => Current connection name number.
	 */
	public static function set_image( $mowcps_product, $product_id, $arguments ) {

		$img_ids = array();

		global $wpdb;

		foreach ( $mowcps_product->images as $key => $value ) {

			$image_url = is_string( $value ) ? $value : '';

			if ( self::check_if_base64( $image_url ) ) {
				$image_name = $mowcps_product->sku . '-' . $key;
				$image_name = sanitize_file_name( $image_name );
				$query      = '%' . $image_name . '%';
				$image_id   = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_wp_attached_file' AND meta_value LIKE %s;", $query ) ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery -- Caching is not required here since we always need the latest data from the database.
			} else {
				$image_index = ( strrpos( $image_url, '/' ) );
				$image_name  = '';
				if ( (int) $image_index ) {
					$image_name = substr( $image_url, $image_index + 1 );
					if ( false !== strpos( $image_name, '.' ) ) {
						$image_name = substr( $image_name, 0, strpos( $image_name, '.' ) );
					} else {
						$image_url = $image_url . '?ext=.jpeg';
					}
				}
				$query    = '%' . $image_url . '%';
				$image_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_source_url' AND meta_value LIKE %s;", $query ) ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery -- Caching is not required here since we always need the latest data from the database.
			}

			if ( null === $image_id ) {
				if ( self::check_if_base64( $image_url ) ) {
					$image_id = self::upload_image_string( $image_url, $image_name );
				} else {
					$image_id = media_sideload_image( $image_url, 0, null, 'id' );
					// If checksum mismatch occurs, use custom download method.
					if ( is_wp_error( $image_id ) && strpos( $image_id->get_error_message(), 'checksum' ) !== false ) {
						$supplier_id = Sync_Details_DAO::get_instance()->get_supplier_id();
						Supplier_Logger::debug( $supplier_id, 'Image download checksum mismatch, trying alternative method' );
						Supplier_Logger::debug( $supplier_id, 'Image URL: ' . $image_url );
						$image_id = self::custom_download_image( $image_url, $image_name );
					} elseif ( is_wp_error( $image_id ) ) {
						$supplier_id = Sync_Details_DAO::get_instance()->get_supplier_id();
						Supplier_Logger::debug( $supplier_id, 'Failed to download product image' );
						Supplier_Logger::debug( $supplier_id, 'Image URL: ' . $image_url );
						Supplier_Logger::debug( $supplier_id, 'Error: ' . $image_id->get_error_message() );
					}
				}

				// Set identifier for media uploaded by our plugin.
				if ( ! is_wp_error( $image_id ) ) {
					update_post_meta( $image_id, 'MOWCPS_connection_name_media', $arguments['connection_id'] );
				}
			}

			if ( ! is_wp_error( $image_id ) ) {
				array_push( $img_ids, $image_id );
			}
		}
		// Check if Variation Gallery plugin is active.
		if ( ! function_exists( 'is_plugin_active' ) ) {
			include_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$is_plugin_active = is_plugin_active( 'woo-variation-gallery/woo-variation-gallery.php' ) ? true : false;

		// Find images configuration and check for variation gallery settings.
		$variation_gallery_enable = Constants::SYNC_RUNNING_STATUS_OFF;
		$variation_gallery_config = array();

		if ( is_array( $mowcps_product->config ) && $is_plugin_active ) {
			foreach ( $mowcps_product->config as $config_item ) {
				if ( isset( $config_item['wc_attribute'] ) && 'images' === $config_item['wc_attribute'] ) {
					$advanced_config = $config_item['advanced_configuration'] ?? array();

					if ( isset( $advanced_config['variation_gallery'] ) && is_array( $advanced_config['variation_gallery'] ) ) {
						$variation_gallery_settings = $advanced_config['variation_gallery'];

						// Check if variation gallery is enabled.
						if ( isset( $variation_gallery_settings['is_active'] ) && true === $variation_gallery_settings['is_active'] ) {
							$variation_gallery_enable = Constants::SYNC_RUNNING_STATUS_ON;
						}
						$variation_gallery_config = $variation_gallery_settings;
					}
					break;
				}
			}
		}

		if ( Constants::SYNC_RUNNING_STATUS_ON === $variation_gallery_enable && $is_plugin_active ) {
			self::upload_all_images_to_product_with_variation_gallery( $product_id, $img_ids, $mowcps_product, $variation_gallery_config );
		} else {
			self::upload_all_images_to_product( $product_id, $img_ids );
			if ( isset( $mowcps_product->parent_product_id ) ) {
				self::upload_all_images_to_product( $mowcps_product->parent_product_id, $img_ids );
			}
		}

		return $img_ids;
	}

	/**
	 * Custom download method that bypasses WordPress checksum validation.
	 *
	 * @param string $image_url   URL of the image to download.
	 * @param string $image_name  Optional name for the image file.
	 * @return int|null Attachment ID or null on failure.
	 */
	public static function custom_download_image( $image_url, $image_name = '' ) {
		// Get upload directory
		$upload_dir  = wp_upload_dir();
		$upload_path = $upload_dir['path'];
		$upload_url  = $upload_dir['url'];

		// Generate unique filename.
		$filename = basename( $image_url );
		if ( empty( $filename ) ) {
			$filename = 'image_' . time() . '.jpg';
		}

		// Ensure unique filename.
		$original_filename = $filename;
		while ( file_exists( $upload_path . '/' . $filename ) ) {
			$path_parts    = pathinfo( $original_filename );
			$unique_suffix = substr( md5( uniqid( wp_rand(), true ) ), 0, 6 );
			$filename      = $path_parts['filename'] . '_' . $unique_suffix . '.' . $path_parts['extension'];
		}

		// Download image.
		$response = wp_remote_get( $image_url, array( 'timeout' => 60 ) );

		if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
			return null;
		}

		$image_data = wp_remote_retrieve_body( $response );
		if ( empty( $image_data ) ) {
			return null;
		}

		$file_path = $upload_path . '/' . $filename;
		if ( @file_put_contents( $file_path, $image_data ) === false ) {
			return null;
		}

		// Validate MIME type.
		$finfo     = finfo_open( FILEINFO_MIME_TYPE );
		$mime_type = finfo_file( $finfo, $file_path );
		finfo_close( $finfo );

		if ( ! $mime_type || strpos( $mime_type, 'image/' ) !== 0 ) {
			wp_delete_file( $file_path );
			return null;
		}

		// Create attachment.
		$attachment = array(
			'post_mime_type' => $mime_type,
			'post_title'     => preg_replace( '/\.[^.]+$/', '', $filename ),
			'post_content'   => '',
			'post_status'    => 'inherit',
			'guid'           => $upload_url . '/' . $filename,
		);

		$attach_id = wp_insert_attachment( $attachment, $file_path );

		if ( is_wp_error( $attach_id ) ) {
			wp_delete_file( $file_path );
			return null;
		}

		// Generate metadata.
		$attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
		wp_update_attachment_metadata( $attach_id, $attach_data );

		// Store the original attachment source in meta (CRITICAL FIX).
		add_post_meta( $attach_id, '_source_url', $image_url );

		return $attach_id;
	}

	/**
	 * Check if the given string is base64 encoded.
	 *
	 * @param string $image Image string.
	 *
	 * @return bool
	 */
	public static function check_if_base64( $image ) {
		// Decode the string and check for errors.
		$decoded = base64_decode( $image, true ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Not used for code obfuscation.

		/**
		 * If decoding was successful and the decoded string is not empty
		 * and the decoded string is the same as the input string when re-encoded.
		 */
		return ( false !== $decoded && base64_encode( $decoded ) === $image ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Not used for code obfuscation.
	}

	/**
	 * Upload image string to the media library.
	 *
	 * @param string $image    Image string.
	 * @param string $filename Filename.
	 *
	 * @return int
	 */
	public static function upload_image_string( $image, $filename ) {
		$filename = $filename . '.jpg';

		$upload_dir = wp_upload_dir();

		$upload_path = str_replace( '/', '/', $upload_dir['path'] ) . '/';

		$decoded = base64_decode( $image ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Not used for code obfuscation.

		file_put_contents( $upload_path . $filename, $decoded ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Direct file writing is required for performance in this specific case.

		$attachment = array(
			'post_mime_type' => 'image/jpeg',
			'post_title'     => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ),
			'post_content'   => '',
			'post_status'    => 'inherit',
			'guid'           => $upload_dir['url'] . '/' . basename( $filename ),
		);

		$attach_id = wp_insert_attachment( $attachment, $upload_dir['path'] . '/' . $filename );
		return $attach_id;
	}

	/**
	 * Upload all the images to the product.
	 *
	 * @param integer $product_id     ID of the product.
	 * @param array   $image_id_array Array of image IDs.
	 */
	private static function upload_all_images_to_product( $product_id, $image_id_array ) {
		if ( count( $image_id_array ) ) {
			if ( ! has_post_thumbnail( $product_id ) ) {
				set_post_thumbnail( $product_id, $image_id_array[0] );
				array_shift( $image_id_array );
			} elseif ( (int) get_post_thumbnail_id( $product_id ) === (int) $image_id_array[0] ) {
				// Thumbnail already set (e.g. replace-images); skip duplicating it into _product_image_gallery.
				array_shift( $image_id_array );
			}

			if ( ! empty( $image_id_array ) ) {
				$existing_gallery       = get_post_meta( $product_id, '_product_image_gallery', true );
				$existing_gallery_array = ! empty( $existing_gallery ) ? explode( ',', $existing_gallery ) : array();
				$merged_gallery_array   = array_merge( $existing_gallery_array, $image_id_array );
				$unique_gallery_array   = array_unique( $merged_gallery_array );

				update_post_meta( $product_id, '_product_image_gallery', implode( ',', $unique_gallery_array ) ); // set the images id's left over after the array shift as the gallery images.
			}
		}
	}



	private static function upload_all_images_to_product_with_variation_gallery( $product_id, $image_id_array, $mo_product, $variation_gallery_config ) {
		// Config settings.
		$variation_gallery_db_key         = 'woo_variation_gallery_images';
		$variation_gallery_to_main_enable = ( ! empty( $variation_gallery_config['sync_to_main'] ) && true === $variation_gallery_config['sync_to_main'] ) ? 'on' : 'off';

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

		$post_type    = get_post_type( $product_id );
		$is_variation = $post_type === 'product_variation';

		// if variation gallery is enabled and connection is allowed, then upload the images to the variation gallery.
		if ( $is_variation ) {
			// Set product thumbnail if not set.
			if ( ! has_post_thumbnail( $product_id ) && isset( $image_id_array[0] ) ) {
				set_post_thumbnail( $product_id, $image_id_array[0] );
				// Remove the first image from the array (already set as featured image).
				array_shift( $image_id_array );
			}
			// Save variation gallery.
			update_post_meta( $product_id, $variation_gallery_db_key, $image_id_array );
			// Also upload to parent/main product if allowed.
			if ( $variation_gallery_to_main_enable === Constants::SYNC_RUNNING_STATUS_ON && isset( $mo_product->parent_product_id ) && $mo_product->parent_product_id !== $product_id ) {
				self::upload_all_images_to_product( $mo_product->parent_product_id, $image_id_array );
			}
		} elseif ( ! $is_variation ) {
			if ( $variation_gallery_to_main_enable === Constants::SYNC_RUNNING_STATUS_ON ) {
				self::upload_all_images_to_product( $product_id, $image_id_array );
			} else {
				// Set thumbnail.
				set_post_thumbnail( $product_id, $image_id_array[0] );
				array_shift( $image_id_array );

				// Delete existing gallery images before adding new ones.
				delete_post_meta( $product_id, '_product_image_gallery' );

				// Save product image gallery.
				update_post_meta( $product_id, '_product_image_gallery', implode( ',', $image_id_array ) );
			}
		}
	}

	/**
	 * Set attributes values to the Parent Variable product and create attributes for child variation.
	 *
	 * @param array   $custom_fields Custom fields of the product.
	 * @param integer $post_id       ID of the product.
	 */
	public static function set_custom_fields( $custom_fields, $post_id ) {
		global $wpdb;
		/**
		 * This filter position is to perform any operations at the time of syncing custom fields.
		 * An array should be returned from the filter at the end which will eventually be synced as custom fields.
		 *
		 * 2nd parameter in apply_filter should be event identifier.
		 *
		 * Additional args/details should be sent as an in 3rd parameter of apply filter.
		 *
		 * $custom_fields Same as function doc.
		 * $post_id       Same as function doc.
		 */
		if ( has_filter( 'mowcps_data_alteration_filter' ) ) {
			$returned_data = apply_filters(
				'mowcps_data_alteration_filter',
				array(
					'event_specifier' => 'set_custom_fields',
					'custom_fields'   => $custom_fields,
					'post_id'         => $post_id,
				)
			);
			$custom_fields = $returned_data['custom_fields'];

		}

		foreach ( $custom_fields as $key => $val ) {
			if ( '' === $key || '' === $val ) {
				continue;
			}

			$values[] = sprintf(
				"(%d, '%s', '%s')",
				(int) $post_id,
				esc_sql( $key ),
				esc_sql( maybe_serialize( $val ) )
			);
		}

		if ( ! empty( $values ) ) {
			$wpdb->query(
				"INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value)
				VALUES " . implode( ',', $values ) . '
				ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value)'
			);
		}
	}
}
