<?php
/**
 * This is a util file for syncing product details from API to WooCommerce.
 *
 * @package    Common
 * @subpackage Utils
 * @author     miniOrange <info@miniorange.com>
 * @link       https://miniorange.com
 */

namespace MOWCPS\Common\Utils;

use MOWCPS\Common\Services\Logging\Supplier_Logger;
use MOWCPS\Common\Services\Auditing\Supplier_Audit_Logger;
use MOWCPS\Common\Models\Sync_Details_DAO;
use MOWCPS\Common\Models\Connection_Model;
use MOWCPS\Common\Models\Setting_Model;
use MOWCPS\Common\Services\Product_Sync\SyncProducts\MO_Product;
use MOWCPS\Common\Constants\Plugin_Filters;
use MOWCPS\Common\Constants\Constants;

/**
 * This will be used to handle all the sync related operations.
 */
class Sync_Utils {
	use MO_Product;

	/**
	 * Convert the API response to array.
	 *
	 * @param string $api_response    API response.
	 * @param int    $connection_id   Connection ID for which the sync is initiated.
	 *
	 * @return array $api_response
	 */
	public static function get_main_response_array( $api_response, $connection_id ) {
		$connection_details = Sync_Details_DAO::get_instance()->get_connection_details( $connection_id );
		$response_attr      = $connection_details['configuration']['prerequisites']['product_response'];
		if ( '' !== $response_attr ) {
			$api_response = self::prepare_product_object( $api_response, $response_attr );
		}

		// To handle the case where there is only single product data and is given directly where as our code expect it to be given inside an array of size 1.
		$api_response_check = wp_json_encode( $api_response );
		if ( '{' === $api_response_check[0] ) {
			$api_response = array( $api_response );
		}
		return $api_response;
	}

	/**
	 * Prepare the product object for the given API response.
	 *
	 * @param \stdClass $data          Product data from which the desired API response is to be extracted.
	 * @param string    $response_attr Key associated with product data array.
	 *
	 * @return array
	 */
	public static function prepare_product_object( $data, $response_attr ) {
		$products_data = self::map_particular_attr( $response_attr, $data, array() );

		if ( has_filter( 'mowcps_api_call_alteration_filter' ) ) {
				$api_request   = apply_filters(
					'mowcps_api_call_alteration_filter',
					array(
						'event_specifier' => 'alter_api_data',
						'products_data'   => $products_data,
					)
				);
				$products_data = $api_request['products_data'];
		}

		$products_data = is_array( $products_data ) ? $products_data : (array) $products_data;
		return $products_data;
	}

	/**
	 * Fetch field value from the given object.
	 *
	 * @param string $attr_name      Field name.
	 * @param object $product_object Product data from API.
	 * @param mixed  $default_value  Default value.
	 * @return mixed
	 */
	public static function map_particular_attr( $attr_name, $product_object, $default_value = false ) {
		$product_object = is_array( $product_object ) ? $product_object : (array) $product_object;

		// Handle multiple attributes combined with '+'.
		$attr_name = html_entity_decode( $attr_name );
		if ( strpos( $attr_name, '+' ) !== false ) {
			$attributes = Utils::split_to_array( $attr_name );
			$values     = array_map(
				function ( $attribute ) use ( $product_object, $default_value ) {
					return self::extract_attribute_value( $attribute, $product_object, $default_value );
				},
				$attributes
			);

			$values = self::transform_array_values( $values );

			return implode( '', array_filter( $values, array( static::class, 'is_valid_value' ) ) );
		}

		$value = self::extract_attribute_value( $attr_name, $product_object, $default_value );
		// Removing extra double quotes from the beginning and end of the value, if present (e.g., """value""" => value).
		if ( is_string( $value ) ) {
			$value = preg_replace( '/^"+|"+$/', '', $value );
		}
		return $value;
	}

	/**
	 * Extracts the value of a given attribute from the product object.
	 * Supports attribute name prefixes (##) and nested attributes using '->'.
	 *
	 * @param string       $attribute      Attribute name.
	 * @param array|object $product_object Product data.
	 * @param mixed        $default_value  Default value.
	 *
	 * @return mixed
	 */
	private static function extract_attribute_value( $attribute, $product_object, $default_value ) {
		// Remove spaces from the attribute name.
		$attribute = trim( $attribute );

		if ( strpos( $attribute, '##' ) === 0 ) {
			return trim( $attribute, '##' );
		}

		if ( strpos( $attribute, '->' ) !== false ) {
			return self::extract_nested_attribute( $attribute, $product_object, $default_value );
		}

		return $product_object[ $attribute ] ?? $default_value;
	}

	/**
	 * Extracts a nested attribute value by traversing the product object.
	 *
	 * @param string       $attribute_path Attribute path using '->' notation.
	 * @param array|object $product_object Product data.
	 * @param mixed        $default_value  Default value.
	 *
	 * @return mixed
	 */
	private static function extract_nested_attribute( $attribute_path, $product_object, $default_value ) {
		$keys = explode( '->', $attribute_path );
		foreach ( $keys as $index => $key ) {
			if ( ! is_array( $product_object ) || ! array_key_exists( $key, $product_object ) ) {
				return $default_value;
			}

			$product_object = is_object( $product_object[ $key ] ) ? (array) $product_object[ $key ] : $product_object[ $key ];

			if ( count( $keys ) - 1 === $index ) {
				return self::is_valid_value( $product_object ) ? $product_object : $default_value;
			}
		}
		return $default_value;
	}

	/**
	 * Transform the array values into a comma separated string.
	 *
	 * @param array $values Array of values to transform.
	 * @return array
	 */
	private static function transform_array_values( $values ) {
		$new_values = array();
		foreach ( $values as $value ) {
			if ( is_array( $value ) ) {
				$value = implode( ', ', $value );
			}
			$new_values[] = $value;
		}
		return $new_values;
	}

	/**
	 * Add product into WC
	 *
	 * @param MOWCPS\Common\Functionality\MOWCPS_Simple_Product | MOWCPS\Common\Functionality\mowcps_product $mowcps_product Product object.
	 *
	 * @return mixed
	 */
	public static function add_product( $mowcps_product ) {
		try {
			if ( Constants::VARIABLE === $mowcps_product->structure && has_filter( Plugin_Filters::FILTER_PRODUCT_SAVE_FILTER ) ) {
				return apply_filters( Plugin_Filters::FILTER_PRODUCT_SAVE_FILTER, $mowcps_product, Constants::ADD, '' );
			} elseif ( class_exists( 'WC_Product_Simple' ) ) {
				if ( has_filter( Plugin_Filters::FILTER_PRODUCT_SAVE_FILTER ) ) {
					$product = apply_filters( Plugin_Filters::FILTER_PRODUCT_SAVE_FILTER, $mowcps_product, Constants::ADD, '' );
				} else {
					$product = new \WC_Product_Simple();

					if ( has_filter( Plugin_Filters::FILTER_UPDATE_PRO_PRODUCT ) ) {
						return apply_filters( Plugin_Filters::FILTER_UPDATE_PRO_PRODUCT, $product, $mowcps_product, Constants::UPDATE );
					} else {
						return self::set_product_fields( $product, $mowcps_product, Constants::ADD );
					}
				}

				return $product;
			} else {
				$supplier_id = Sync_Details_DAO::get_instance()->get_supplier_id();
				Supplier_Logger::debug( $supplier_id, 'WooCommerce not active or WC_Product_Simple class not found' );
				return 'WooCommerce not active or WC_Product_Simple class not found';
			}
		} catch ( \Exception $e ) {
			$supplier_id = Sync_Details_DAO::get_instance()->get_supplier_id();
			Supplier_Logger::debug( $supplier_id, 'Exception during product creation: ' . $e->getMessage() );
			return $e->getMessage();
		}
	}

	/**
	 * Update product into WC
	 *
	 * @param integer                                           $id Product ID.
	 * @param MOWCPS\Common\Functionality\MOWCPS_Simple_Product $mowcps_product Product object.
	 *
	 * @return mixed
	 */
	public static function update_product( $id, $mowcps_product ) {
		try {
			$product = function_exists( 'wc_get_product' ) ? wc_get_product( $id ) : false;
			if ( Constants::VARIABLE === $mowcps_product->structure && has_filter( Plugin_Filters::FILTER_PRODUCT_UPDATE_FILTER ) ) {
				return apply_filters( Plugin_Filters::FILTER_PRODUCT_UPDATE_FILTER, $mowcps_product, Constants::UPDATE, $product );
			} elseif ( class_exists( 'WC_Product_Simple' ) ) {
				if ( ! $product ) {
					$product = new \WC_Product_Simple();
				}

				if ( has_filter( Plugin_Filters::FILTER_UPDATE_PRO_PRODUCT ) ) {
					return apply_filters( Plugin_Filters::FILTER_UPDATE_PRO_PRODUCT, $product, $mowcps_product, Constants::UPDATE );
				} else {
					return self::set_product_fields( $product, $mowcps_product, Constants::UPDATE );
				}
			} else {
				$supplier_id = Sync_Details_DAO::get_instance()->get_supplier_id();
				Supplier_Logger::debug( $supplier_id, 'WooCommerce not active or WC_Product_Simple class not found during update' );
				return 'WooCommerce not active or WC_Product_Simple class not found';
			}
		} catch ( \Exception $e ) {
			$supplier_id = Sync_Details_DAO::get_instance()->get_supplier_id();
			Supplier_Logger::debug( $supplier_id, 'Exception during product update: ' . $e->getMessage() );
			return $e->getMessage();
		}
	}

	/**
	 * Set the fields to the WC product.
	 *
	 * @param \WC_Product                                       $product    WC Product object.
	 * @param MOWCPS\Common\Functionality\MOWCPS_Simple_Product $mowcps_product Product object.
	 * @param string                                            $operation  Operation to be performed. Can be 'update' or 'add'.
	 * @param bool                                              $audit_skip_success_when_premium_pipeline When true, Common skips duplicate success audit rows (Premium wraps this call).
	 *
	 * @return array|boolean
	 */
	public static function set_product_fields( $product, $mowcps_product, $operation = '', $audit_skip_success_when_premium_pipeline = false ) {
		try {
			$product_created_array = array();

			// *Below operations requires WC Product object*.

			// Set SKU.
			! empty( $mowcps_product->sku ) && 'update' !== $operation ? $product->set_sku( $mowcps_product->sku ) : '';

			// Set Name.
			! empty( $mowcps_product->name ) ? $product->set_name( $mowcps_product->name ) : '';

			// Set Description.
			! empty( $mowcps_product->description ) ? $product->set_description( $mowcps_product->description ) : '';

			// Set Regular Price.
			! empty( $mowcps_product->regular_price ) ? $product->set_regular_price( $mowcps_product->regular_price ) : '';

			// Set discard products flag from option based on supplier_id.
			$supplier_id           = Sync_Details_DAO::get_instance()->get_supplier_id();
			$option_key_current    = 'mowcps_' . $supplier_id . '_current_discard_products';
			$current_discard_value = get_option( $option_key_current, Constants::LEFTOVER_PRODUCT_DISCARD_NO );
			$product->update_meta_data( 'mowcps_discard_products', $current_discard_value );
			$product->update_meta_data( 'mowcps_connection_name_product_updated', Sync_Details_DAO::get_instance()->get_supplier_id() );
			'update' !== $operation ? $product->update_meta_data( 'mowcps_connection_name_product_created', Sync_Details_DAO::get_instance()->get_supplier_id() ) : '';
			'update' !== $operation ? $product->update_meta_data( 'mowcps_supplier_name_product_created', Sync_Details_DAO::get_instance()->get_supplier_details( Sync_Details_DAO::get_instance()->get_supplier_id() )['name'] ) : '';

			// Set left fields.
			if ( has_filter( Plugin_Filters::FILTER_SET_LEFT_FIELDS ) ) {
				$product = apply_filters( Plugin_Filters::FILTER_SET_LEFT_FIELDS, $product, $mowcps_product, $operation );
			}

			$product_id = self::get_product_by_meta( $mowcps_product->unique_identifier );

			// If product is already created, then return early fetching product details.
			if ( $product_id > 0 && 'add' === $operation ) {
				$product_created_array['id']                  = $product_id;
				$product                                      = wc_get_product( $product_id );
				$product_created_array['obj']                 = $product;
				$product_created_array['audit_duplicate_add'] = true;
				Supplier_Audit_Logger::log_from_common_set_product_fields( $mowcps_product, 'add', $product_created_array, $audit_skip_success_when_premium_pipeline );
				return $product_created_array;
			}

			// Save Product.
			$id = $product->save();

			if ( empty( $id ) ) {
				$supplier_id = Sync_Details_DAO::get_instance()->get_supplier_id();
				Supplier_Logger::debug( $supplier_id, 'Product save failed - WordPress returned empty ID for SKU: ' . $mowcps_product->sku );
				Supplier_Logger::debug( $supplier_id, 'Possible reasons: Invalid product data, database error, or WooCommerce validation failure' );
				Supplier_Audit_Logger::log_from_common_set_product_fields( $mowcps_product, $operation, false, $audit_skip_success_when_premium_pipeline );
				return false;
			}

			// Set Unique Identifier.
			! empty( $mowcps_product->unique_identifier ) && 'update' !== $operation ? self::set_custom_fields( $mowcps_product->unique_identifier, $id ) : '';

			// Set manage_stock .
			if ( 1 === $mowcps_product->manage_stock ) {
				$product->set_manage_stock( true );
			} elseif ( (int) ( -1 === $mowcps_product->manage_stock && ( isset( $mowcps_product->stock_quantity ) && $mowcps_product->stock_quantity >= 0 && 'MOWCPS\Premium\Services\ProductSync\SyncProducts\MO_Variable_ParentProduct' !== get_class( $mowcps_product ) ) ) ) {
				$product->set_manage_stock( true );
			} elseif ( isset( $mowcps_product->manage_stock ) ) {
				$product->set_manage_stock( false );
			}

			// Set Image.
			! empty( $mowcps_product->images ) ? self::set_image( $mowcps_product, $id, array( 'connection_id' => $mowcps_product->connection_id ) ) : '';

			// Send Created WC Product Object and ID.
			$product_created_array['id']  = $id;
			$product_created_array['obj'] = $product;
			Supplier_Audit_Logger::log_from_common_set_product_fields( $mowcps_product, $operation, $product_created_array, $audit_skip_success_when_premium_pipeline );
			return $product_created_array;
		} catch ( \Error $e ) {
			$supplier_id = Sync_Details_DAO::get_instance()->get_supplier_id();
			Supplier_Logger::debug( $supplier_id, 'Fatal error during product sync: ' . $e->getMessage() );
			Supplier_Logger::debug( $supplier_id, 'Error location: ' . $e->getFile() . ':' . $e->getLine() );
			Supplier_Logger::debug( $supplier_id, 'This is usually caused by invalid product data or PHP compatibility issues' );
			if ( isset( $mowcps_product ) && is_object( $mowcps_product ) ) {
				Supplier_Audit_Logger::log_from_common_set_product_fields( $mowcps_product, $operation, false, $audit_skip_success_when_premium_pipeline );
			}
			return false;
		}
	}

	/**
	 * Apply a pricing margin to a given price.
	 * This static function takes a price, an operator, and a margin value as input and applies the margin to the price based on the operator.
	 *
	 * @param  float  $price    The original price to which the margin should be applied.
	 * @param  string $operator The operator to determine how the margin should be applied. Valid operators are '+', '-', '*', and '/'.
	 * @param  mixed  $margin   The margin value to be applied. If numeric, it can be either an integer or a float. If not numeric, a default value of 0 will be used.
	 * @return float The adjusted price after applying the margin.
	 */
	public static function apply_pricing_margin( $price, $operator, $margin ) {
		if ( is_numeric( $margin ) ) {
			if ( strpos( $margin, '.' ) === false ) {
				$margin = intval( $margin ); // Convert to integer.
			} else {
				$margin = floatval( $margin ); // Convert to float.
			}
		} else {
			$margin = 0; // Default value if margin is not numeric.
		}

		switch ( $operator ) {
			case '+':
				return $price + $margin;
			case '-':
				return $price - $margin;
			case '*':
				return $price * $margin;
			case '/':
				return $price / $margin;
			default:
				return $price;
		}
	}

	/**
	 * Validate if a price value is in correct format and can be converted to a valid numeric value.
	 *
	 * @param mixed $price_value The price value to validate, can be string or numeric.
	 * @return bool Returns true if price is valid, false otherwise.
	 */
	public static function validate_price_format( $price_value ) {
		$is_valid_price = false;
		if ( is_string( $price_value ) ) {
			$sanitized_price = str_replace( ',', '', $price_value );
			if ( is_numeric( $sanitized_price ) ) {
				$is_valid_price = true;
			}
		} elseif ( is_numeric( $price_value ) ) {
			$is_valid_price = true;
		}
		return $is_valid_price;
	}

	/**
	 * Normalize price value by handling different decimal formats.
	 * Uses WooCommerce's separator settings to parse price strings correctly.
	 * Falls back to custom logic if WooCommerce functions are unavailable.
	 *
	 * @param mixed $value The price value to normalize.
	 * @return float The normalized price value as a float.
	 */
	public static function normalize_price_value( $value ) {
		if ( null === $value || '' === $value || ( ! is_scalar( $value ) && ! is_numeric( $value ) ) ) {
			return 0.0;
		}

		// Convert to string first to preserve format information and handle European formats.
		$value = (string) $value;
		$value = trim( $value );
		$value = preg_replace( '/[^\d.,-]/', '', $value );

		if ( '' === $value ) {
			return 0.0;
		}

		// Use WooCommerce separator settings if available.
		if ( function_exists( 'wc_get_price_decimal_separator' ) && function_exists( 'wc_get_price_thousand_separator' ) ) {
			$decimal  = wc_get_price_decimal_separator();
			$thousand = wc_get_price_thousand_separator();

			// Remove thousand separator first.
			if ( $thousand ) {
				$value = str_replace( $thousand, '', $value );
			}

			// Normalize decimal separator to dot.
			if ( $decimal && '.' !== $decimal ) {
				$value = str_replace( $decimal, '.', $value );
			}

			$normalized = floatval( $value );

			if ( ! is_finite( $normalized ) ) {
				return 0.0;
			}

			return $normalized;
		}
		return floatval( $value );
	}

	/**
	 * Fetch field value from multiple objects.
	 *
	 * @param string $attr_names      Field name.
	 * @param object $product_object Product data from API.
	 * @param object $default_value  Default value.
	 * @return string
	 */
	public static function fetch_data_from_multiple_keys( $attr_names, $product_object, $default_value = false ) {
		if ( ! is_array( $product_object ) ) {
			$product_object = (array) $product_object;
		}
		if ( ! is_array( $attr_names ) ) {
			$attr_names = explode( ',', $attr_names );
		}
		$combined_attributes = array();
		foreach ( $attr_names as $key ) {
			$key             = sanitize_text_field( trim( $key ) );
			$attribute_value = self::map_particular_attr( $key, $product_object, array() );
			if ( ! empty( is_array( $attribute_value ) ) ) {
				$combined_attributes = array_merge( $combined_attributes, $attribute_value );
			} else {
				$combined_attributes[] = $attribute_value;
			}
		}
		return ! empty( array_filter( $combined_attributes ) ) ? $combined_attributes : $default_value;
	}

	/**
	 * Performs operations on values to replace with values.
	 *
	 * @param array|string $value        Value to replace.
	 * @param mixed        $dynamic_data Data to replace value in.
	 * @return array|string
	 */
	public static function perform_additional_operations_on_value( $value, $dynamic_data ) {
		$return_value = $value;
		if ( is_array( $value ) ) {
			$return_value = array();
			foreach ( $value as $details ) {
				$pattern = '/{{(.*?)}}/';

				// Use preg_match_all to find all matches.
				preg_match_all( $pattern, $details, $matches );

				// Extracted substrings will be in $matches[1].
				$extracted_substrings = $matches[1];

				foreach ( $extracted_substrings as $match ) {
					$new_val = self::map_particular_attr( $match, $dynamic_data );
					$new_val = false === $new_val ? '' : $new_val;
					$details = str_replace( '{{' . $match . '}}', $new_val, $details );
				}
				$return_value[] = $details;
			}
		} else {
			$pattern = '/{{(.*?)}}/';

			// Use preg_match_all to find all matches.
			preg_match_all( $pattern, $value, $matches );

			// Extracted substrings will be in $matches[1].
			$extracted_substrings = $matches[1];

			foreach ( $extracted_substrings as $match ) {
				$new_val = self::map_particular_attr( $match, $dynamic_data );
				$new_val = false === $new_val ? '' : $new_val;
				$value   = str_replace( '{{' . $match . '}}', $new_val, $value );
			}
			$return_value = $value;
		}
		return $return_value;
	}

	/**
	 * Check if supplied value if valid.
	 *
	 * @param mixed $value Value to check.
	 * @return boolean
	 */
	public static function is_valid_value( $value ) {
		return ! is_null( $value ) && ! empty( $value ) && false !== $value && array() !== $value;
	}

	/**
	 * Function to create global attributes in WooCommerce.
	 * It will only create the attribute title/label and not add any values.
	 * If the attribute is already created, it will not be updated.
	 *
	 * @param array|string $attribute_list Array of all the attribute names or a single attribute name.
	 *
	 * @return array
	 */
	public static function create_wc_attributes( $attribute_list ) {

		$attribute_list = is_string( $attribute_list ) ? (array) $attribute_list : $attribute_list;

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

		$return_ids = array();
		foreach ( $attribute_list as $attribute ) {
			// Set your attribute details.
			$attribute_name    = $attribute;
			$attribute_type    = 'select';
			$attribute_orderby = 'name';

			// Create an array of attribute data.
			$attribute_data = array(
				'name'         => $attribute_name,
				'type'         => $attribute_type,
				'order_by'     => $attribute_orderby,
				'has_archives' => true, // Set true if you want to create archive/filter pages for values of this attribute.
			);

			// Check if the attribute already exists.
			$attribute_slug = 'pa_' . sanitize_title( $attribute );
			// If the attribute doesn't exist, create it.
			if ( ! taxonomy_exists( $attribute_slug ) ) {
				$return_ids[] = wc_create_attribute( $attribute_data );
				self::add_taxonomy_for_wc_attribute( $attribute_slug );
			} else {
				$return_ids[] = random_int( 1, 15 );
			}
		}

		return $return_ids;
	}

	/**
	 * To handle missed out flow because of continuous init.
	 *
	 * @param string $attribute_slug Slug of the attribute.
	 * @return void
	 */
	private static function add_taxonomy_for_wc_attribute( $attribute_slug ) {
		global $wc_product_attributes;

		$wc_product_attributes = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- This is a global variable that is used to store the product attributes.
		$attribute_taxonomies  = wc_get_attribute_taxonomies();
		$permalinks            = wc_get_permalink_structure();
		if ( $attribute_taxonomies ) {
			foreach ( $attribute_taxonomies as $tax ) {
				$name = wc_attribute_taxonomy_name( $tax->attribute_name );

				if ( $name ) {
					$tax->attribute_public          = absint( isset( $tax->attribute_public ) ? $tax->attribute_public : 1 );
					$label                          = ! empty( $tax->attribute_label ) ? $tax->attribute_label : $tax->attribute_name;
					$wc_product_attributes[ $name ] = $tax; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- This is a global variable that is used to store the product attributes.
					$taxonomy_data                  = array(
						'hierarchical'          => false,
						'update_count_callback' => '_update_post_term_count',
						'labels'                => array(
							/* translators: %s: attribute name */
							'name'              => sprintf( _x( 'Product %s', 'Product Attribute', 'products-sync-for-woocommerce' ), $label ),
							'singular_name'     => $label,
							/* translators: %s: attribute name */
							'search_items'      => sprintf( __( 'Search %s', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'all_items'         => sprintf( __( 'All %s', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'parent_item'       => sprintf( __( 'Parent %s', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'parent_item_colon' => sprintf( __( 'Parent %s:', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'edit_item'         => sprintf( __( 'Edit %s', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'update_item'       => sprintf( __( 'Update %s', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'add_new_item'      => sprintf( __( 'Add new %s', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'new_item_name'     => sprintf( __( 'New %s', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'not_found'         => sprintf( __( 'No &quot;%s&quot; found', 'products-sync-for-woocommerce' ), $label ),
							/* translators: %s: attribute name */
							'back_to_items'     => sprintf( __( '&larr; Back to "%s" attributes', 'products-sync-for-woocommerce' ), $label ),
						),
						'show_ui'               => true,
						'show_in_quick_edit'    => false,
						'show_in_menu'          => false,
						'meta_box_cb'           => false,
						'query_var'             => 1 === $tax->attribute_public,
						'rewrite'               => false,
						'sort'                  => false,
						'public'                => 1 === $tax->attribute_public,
						'show_in_nav_menus'     => 1 === $tax->attribute_public && apply_filters( 'woocommerce_attribute_show_in_nav_menus', false, $name ),
						'capabilities'          => array(
							'manage_terms' => 'manage_product_terms',
							'edit_terms'   => 'edit_product_terms',
							'delete_terms' => 'delete_product_terms',
							'assign_terms' => 'assign_product_terms',
						),
					);

					if ( 1 === $tax->attribute_public && sanitize_title( $tax->attribute_name ) ) {
						$taxonomy_data['rewrite'] = array(
							'slug'         => trailingslashit( $permalinks['attribute_rewrite_slug'] ) . urldecode( sanitize_title( $tax->attribute_name ) ),
							'with_front'   => false,
							'hierarchical' => true,
						);
					}

					register_taxonomy( $name, apply_filters( "woocommerce_taxonomy_objects_{$name}", array( 'product' ) ), apply_filters( "woocommerce_taxonomy_args_{$name}", $taxonomy_data ) );
				}
			}
		}
	}

	/**
	 * Create the attribute object for the product.
	 *
	 * @param string $name    name of the attribute to be created in WC Product.
	 * @param mixed  $options values of the attribute to be stored in WC Product.
	 *
	 * @return WC_Product_Attribute
	 */
	public static function create_attributes( $name, $options ) {
		$attribute = new \WC_Product_Attribute();

		if ( 'pa_' === substr( $name, 0, 3 ) ) {
			$id = wc_attribute_taxonomy_id_by_name( $name );

		} else {
			$id = 0;
		}
		// Set Attribute ID.
		$attribute->set_id( $id );

		// Set Attribute Name.
		! empty( $name ) ? $attribute->set_name( $name ) : '';

		// Set Attribute Options.
		! empty( $options ) ? $attribute->set_options( $options ) : '';

		// Set Attribute Visible.
		$attribute->set_visible( true );

		// Set Attribute Variation.
		$attribute->set_variation( true );

		return $attribute;
	}

	/**
	 * Identify a product uniquely using meta field.
	 *
	 * @param array $unique_identifier_array The unique identifier array for the product.
	 * @return string The product ID or an empty string if not found.
	 */
	public static function get_product_by_meta( $unique_identifier_array ) {
		global $wpdb;

		$product_id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT post_id 
				FROM {$wpdb->postmeta} pm
				INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
				WHERE pm.meta_key = %s
				AND pm.meta_value = %s
				AND p.post_type IN ('product', 'product_variation')
				LIMIT 1",
				array_keys( $unique_identifier_array )[0],
				$unique_identifier_array[ array_keys( $unique_identifier_array )[0] ],
			)
		);

		// Return the product ID or an empty string if not found.
		$product_id = $product_id ? $product_id : '';

		return $product_id;
	}

	/**
	 * Store product data in batch-wise files.
	 *
	 * @param array  $product_data         The complete product data array to be stored.
	 * @param string $file_directory_path  The directory path where batch files will be stored.
	 * @param int    $connection_id        Connection ID.
	 * @param int    $batch_size           Number of products per batch file.
	 * @param int    $supplier_id          Optional. Supplier ID. If not provided, will be retrieved from connection details.
	 * @param int    $starting_batch_number Optional. The batch number to start from. Default 1.
	 * @return bool True on success, false on failure.
	 */
	public static function store_data_in_batch_files( $product_data, $file_directory_path, $connection_id, $batch_size, $supplier_id, $starting_batch_number = 1 ) {
		if ( empty( $product_data ) || ! is_array( $product_data ) ) {
			return false;
		}
		// Split products into chunks.
		$product_chunks = array_chunk( $product_data, $batch_size );
		$total_batches  = count( $product_chunks );
		$success_count  = 0;

		// Clean up old batch files only if starting from batch 1 (fresh start).
		if ( 1 === $starting_batch_number ) {
			self::cleanup_batch_files( $file_directory_path, $connection_id, $supplier_id );
		}

		// Store each chunk in a separate batch file.
		foreach ( $product_chunks as $batch_index => $batch_products ) {
			$batch_number = $starting_batch_number + $batch_index;
			$file_name    = self::get_batch_file_name( $supplier_id, $connection_id, $batch_number );
			$file_path    = $file_directory_path . DIRECTORY_SEPARATOR . $file_name;

			$serialized_data = serialize( $batch_products );
			$write_result    = Utils::store_data_in_file( $file_name, $file_directory_path, $serialized_data );

			if ( $write_result ) {
				++$success_count;
			} else {
				Supplier_Logger::debug( $supplier_id, sprintf( '[MOWCPS] Connection ID: %s - Failed to write batch file: %s', $connection_id, $file_name ) );
			}
		}
		$end_batch_num = $starting_batch_number + $total_batches - 1;

		if ( 1 === $starting_batch_number ) {
			update_option( 'mowcps_' . $connection_id . '_total_batches', $end_batch_num );
		} else {
			$current_total = (int) get_option( 'mowcps_' . $connection_id . '_total_batches', 0 );
			if ( $end_batch_num > $current_total ) {
				update_option( 'mowcps_' . $connection_id . '_total_batches', $end_batch_num );
			}
		}

		return $success_count === $total_batches;
	}

	/**
	 * Retrieve product data from a specific batch file.
	 *
	 * @param string $file_directory_path The directory path where batch files are stored.
	 * @param int    $connection_id       Connection ID.
	 * @param int    $batch_number        The batch number to retrieve.
	 * @param int    $supplier_id         Optional. Supplier ID. If not provided, will be retrieved from connection details.
	 * @return array The product data array from the batch file, or empty array if not found.
	 */
	public static function retrieve_data_from_batch_file( $file_directory_path, $connection_id, $batch_number, $supplier_id ) {

		$file_name = self::get_batch_file_name( $supplier_id, $connection_id, $batch_number );
		$file_path = $file_directory_path . DIRECTORY_SEPARATOR . $file_name;

		if ( ! file_exists( $file_path ) ) {
			return array();
		}

		$serialized_data = Utils::retrieve_data_from_file( $file_path );
		$product_data    = unserialize( $serialized_data );

		if ( false === $product_data ) {
			return array();
		}
		return is_array( $product_data ) ? $product_data : array();
	}

	/**
	 * Generate batch file name for a specific batch number.
	 *
	 * @param int $supplier_id   Supplier ID.
	 * @param int $connection_id Connection ID.
	 * @param int $batch_number  The batch number.
	 * @return string The generated batch file name.
	 */
	public static function get_batch_file_name( $supplier_id, $connection_id, $batch_number ) {
		return sprintf( 'sync_%s_%s_batch_%d.json', $supplier_id, $connection_id, $batch_number );
	}

	/**
	 * Clean up old batch files for the connection.
	 *
	 * @param string $file_directory_path The directory path containing batch files.
	 * @param int    $connection_id       Connection ID.
	 * @param int    $supplier_id         Supplier ID.
	 * @return void
	 */
	public static function cleanup_batch_files( $file_directory_path, $connection_id, $supplier_id ) {
		if ( ! is_dir( $file_directory_path ) ) {
			return;
		}

		$pattern       = sprintf( 'sync_%s_%s_batch_*.json', $supplier_id, $connection_id );
		$deleted_count = 0;

		// Scan directory and delete matching batch files.
		$files = glob( $file_directory_path . DIRECTORY_SEPARATOR . $pattern );
		if ( false !== $files && ! empty( $files ) ) {
			foreach ( $files as $file ) {
				if ( is_file( $file ) && wp_delete_file( $file ) ) {
					++$deleted_count;
				}
			}
		}

		// Clean up outbound SKU cache file.
		$sku_cache_file = $file_directory_path . DIRECTORY_SEPARATOR . sprintf( 'outbound_sku_cache_%s_%s.json', $supplier_id, $connection_id );
		if ( is_file( $sku_cache_file ) && wp_delete_file( $sku_cache_file ) ) {
			++$deleted_count;
		}

		if ( $deleted_count > 0 ) {
			Supplier_Logger::debug( $supplier_id, sprintf( '[MOWCPS] Connection ID: %s - Cleaned up %d old batch files and SKU cache files.', $connection_id, $deleted_count ) );
		}
	}

	/**
	 * Clean up all batch files for all connections of a supplier.
	 *
	 * Removes all batch files for all connections belonging to a supplier.
	 * This is called when sync is aborted or completed to clean up temporary files.
	 * Only cleans up batch files for connections where batch processing is enabled.
	 *
	 * @since 1.0.0
	 *
	 * @param int $supplier_id Supplier ID.
	 * @return void
	 */
	public static function cleanup_all_batch_files_for_supplier( $supplier_id ) {
		if ( empty( $supplier_id ) ) {
			return;
		}
		$connections = Connection_Model::get_by_supplier_id( $supplier_id );
		if ( empty( $connections ) || ! is_array( $connections ) ) {
			return;
		}
		$file_directory_path = trailingslashit( MOWCPS_PLUGIN_DIR ) . 'data' . DIRECTORY_SEPARATOR . 'external-supplier' . DIRECTORY_SEPARATOR . 'api-response';
		$connections_cleaned = 0;

		foreach ( $connections as $connection ) {
			$connection_id = isset( $connection['id'] ) ? (int) $connection['id'] : 0;
			if ( empty( $connection_id ) ) {
				continue;
			}

			$batch_flag_setting = Setting_Model::get_by_connection_and_operation( $connection_id, 'batch', array( 'configuration' => true ) );
			$is_batch_enabled   = isset( $batch_flag_setting['is_active'] ) && '1' === $batch_flag_setting['is_active'];

			if ( $is_batch_enabled ) {
				self::cleanup_batch_files( $file_directory_path, $connection_id, $supplier_id );
				++$connections_cleaned;
			}
		}

		if ( $connections_cleaned > 0 ) {
			Supplier_Logger::debug( $supplier_id, sprintf( '[MOWCPS] Supplier ID: %s - Cleaned up batch files for %d connection(s) with batch processing enabled.', $supplier_id, $connections_cleaned ) );
		}
	}

	/**
	 * Clean up memory and caches to prevent memory accumulation during sync operations.
	 *
	 * @param bool $force_cleanup Whether to force aggressive cleanup with multiple GC passes.
	 * @return void
	 */
	public static function cleanup_memory( $force_cleanup = false ) {
		wp_cache_flush();
		if ( function_exists( 'wc_delete_product_transients' ) ) {
			wc_delete_product_transients();
		}
		clean_post_cache( 0 );

		if ( $force_cleanup && function_exists( 'gc_collect_cycles' ) ) {
			for ( $i = 0; $i < 3; $i++ ) {
				gc_collect_cycles();
			}
		}
	}

	/**
	 * Trigger garbage collection if available.
	 *
	 * @return void
	 */
	public static function trigger_garbage_collection() {
		if ( function_exists( 'gc_collect_cycles' ) ) {
			gc_collect_cycles();
		}
	}

	/**
	 * Handle sync status transitions (PAUSED/STOPPED/COMPLETED).
	 *
	 * @param int    $supplier_id     The supplier ID.
	 * @param int    $schedule_id     The schedule ID.
	 * @param object $sync_analytics  The sync analytics object.
	 * @return string|false            Returns status constant if sync should stop, false to continue.
	 */
	public static function handle_sync_status( $supplier_id, $schedule_id, $sync_analytics ) {
		$running_status = Model_Utils::sql_get_option( 'mowcps_' . $supplier_id . '_' . $schedule_id . '_sync_running_status' );

		if ( Constants::PAUSED_SYNC_RUNNING_STATUS === $running_status ) {
			$sync_analytics->sync_status_message = Constants::SYNC_PAUSED;
			$sync_analytics->sync_status_code    = Constants::SYNC_PAUSED;
			$sync_analytics->sync_end_time       = time();
			$sync_analytics->save();
			return Constants::SYNC_PAUSED;
		} elseif ( Constants::STOPPED_SYNC_RUNNING_STATUS === $running_status ) {
			$sync_analytics->sync_status_message = Constants::SYNC_ABORTED;
			$sync_analytics->sync_status_code    = Constants::SYNC_ABORTED;
			$sync_analytics->sync_end_time       = time();
			$sync_analytics->save();
			self::cleanup_all_batch_files_for_supplier( $supplier_id );
			return Constants::SYNC_ABORTED;
		} elseif ( Constants::SYNC_RUNNING_STATUS_OFF === $running_status ) {
			$sync_analytics->sync_status_message = Constants::COMPLETED_SYNC_RUNNING_STATUS;
			if ( Constants::FILE_DOWNLOAD_FAILED !== $sync_analytics->sync_status_code ) {
				$sync_analytics->sync_status_code = Constants::COMPLETED_SYNC_RUNNING_STATUS;
			}
			$sync_analytics->sync_end_time = time();
			$sync_analytics->save();
			self::cleanup_all_batch_files_for_supplier( $supplier_id );
			return Constants::COMPLETED_SYNC_RUNNING_STATUS;
		}

		return false; // Continue sync.
	}

	/**
	 * Increment analytics counters for successful product save.
	 *
	 * @param object $sync_analytics            Batch analytics object.
	 * @param object $supplier_sync_analytics   Supplier analytics object.
	 * @param object $connection_sync_analytics Connection analytics object.
	 * @return void
	 */
	public static function increment_success_counters( $sync_analytics, $supplier_sync_analytics, $connection_sync_analytics ) {
		$sync_analytics->product_saved_success            = ( $sync_analytics->product_saved_success ?? 0 ) + 1;
		$supplier_sync_analytics->product_saved_success   = ( $supplier_sync_analytics->product_saved_success ?? 0 ) + 1;
		$connection_sync_analytics->product_saved_success = ( $connection_sync_analytics->product_saved_success ?? 0 ) + 1;
	}

	/**
	 * Increment analytics counters for failed product save.
	 *
	 * @param object $sync_analytics            Batch analytics object.
	 * @param object $supplier_sync_analytics   Supplier analytics object.
	 * @param object $connection_sync_analytics Connection analytics object.
	 * @return void
	 */
	public static function increment_failure_counters( $sync_analytics, $supplier_sync_analytics, $connection_sync_analytics ) {
		$sync_analytics->product_saved_fail            = ( $sync_analytics->product_saved_fail ?? 0 ) + 1;
		$supplier_sync_analytics->product_saved_fail   = ( $supplier_sync_analytics->product_saved_fail ?? 0 ) + 1;
		$connection_sync_analytics->product_saved_fail = ( $connection_sync_analytics->product_saved_fail ?? 0 ) + 1;
	}

	/**
	 * Increment analytics counters for successful variable variation save.
	 *
	 * @param object $sync_analytics            Batch analytics object.
	 * @param object $supplier_sync_analytics   Supplier analytics object.
	 * @param object $connection_sync_analytics Connection analytics object.
	 * @return void
	 */
	public static function increment_variation_success_counters( $sync_analytics, $supplier_sync_analytics, $connection_sync_analytics ) {
		$sync_analytics->variation_success_parsed            = (int) ( $sync_analytics->variation_success_parsed ?? 0 ) + 1;
		$supplier_sync_analytics->variation_success_parsed   = (int) ( $supplier_sync_analytics->variation_success_parsed ?? 0 ) + 1;
		$connection_sync_analytics->variation_success_parsed = (int) ( $connection_sync_analytics->variation_success_parsed ?? 0 ) + 1;
	}

	/**
	 * Increment analytics counters for product parsed.
	 *
	 * @param object $sync_analytics            Batch analytics object.
	 * @param object $supplier_sync_analytics   Supplier analytics object.
	 * @param object $connection_sync_analytics Connection analytics object.
	 * @return void
	 */
	public static function increment_parsed_counters( $sync_analytics, $supplier_sync_analytics, $connection_sync_analytics ) {
		$sync_analytics->total_product_parsed            = ( $sync_analytics->total_product_parsed ?? 0 ) + 1;
		$supplier_sync_analytics->total_product_parsed   = ( $supplier_sync_analytics->total_product_parsed ?? 0 ) + 1;
		$connection_sync_analytics->total_product_parsed = ( $connection_sync_analytics->total_product_parsed ?? 0 ) + 1;
	}

	/**
	 * Save all analytics objects.
	 *
	 * @param object $sync_analytics            Batch analytics object.
	 * @param object $supplier_sync_analytics   Supplier analytics object.
	 * @param object $connection_sync_analytics Connection analytics object.
	 * @return void
	 */
	public static function save_analytics( $sync_analytics, $supplier_sync_analytics, $connection_sync_analytics ) {
		$sync_analytics->save();
		$supplier_sync_analytics->save();
		$connection_sync_analytics->save();
	}

	/**
	 * Complete sync with final status and cleanup.
	 *
	 * @param object $sync_analytics The sync analytics object.
	 * @param int    $supplier_id    Optional. Supplier ID. If not provided, will be retrieved from sync_analytics.
	 * @return string                The completion status constant.
	 */
	public static function complete_sync( $sync_analytics, $supplier_id = null ) {
		$sync_analytics->sync_status_message = Constants::COMPLETED_SYNC_RUNNING_STATUS;
		if ( Constants::FILE_DOWNLOAD_FAILED !== $sync_analytics->sync_status_code ) {
			$sync_analytics->sync_status_code = Constants::COMPLETED_SYNC_RUNNING_STATUS;
		}
		$sync_analytics->sync_end_time = time();
		$sync_analytics->save();
		return Constants::COMPLETED_SYNC_RUNNING_STATUS;
	}

	/**
	 * Delete all scheduled cron events for a specific supplier.
	 *
	 * This function removes all WordPress cron jobs associated with a supplier ID.
	 * It handles multiple cron job types including:
	 * - mowcps_sync_scheduler (with supplier_id as first argument)
	 * - mowcps_product_batch_sync_cron (with supplier_id in cron_arguments array)
	 * - mowcps_product_sync_backup_cron (with supplier_id in cron_arguments array)
	 * - Any other mowcps-prefixed hooks that contain the supplier_id
	 *
	 * @param int $supplier_id The supplier ID for which to clear scheduled events.
	 * @return void
	 */
	public static function clear_supplier_scheduled_events( $supplier_id ) {
		$cron_array = _get_cron_array();

		if ( ! is_array( $cron_array ) || empty( $supplier_id ) ) {
			return 0;
		}

		foreach ( $cron_array as $timestamp => $cron_jobs ) {
			foreach ( $cron_jobs as $hook_name => $hook_data ) {
				// Only process hooks that start with 'mowcps'.
				if ( 0 !== strpos( $hook_name, 'mowcps' ) ) {
					continue;
				}

				foreach ( $hook_data as $hash => $cron_job ) {
					$args         = isset( $cron_job['args'] ) ? $cron_job['args'] : array();
					$should_clear = false;

					// Check if supplier_id matches in different argument structures.
					// Case 1: Direct supplier_id in args array (e.g., mowcps_sync_scheduler).
					if ( isset( $args[0] ) && is_numeric( $args[0] ) && intval( $args[0] ) === intval( $supplier_id ) ) {
						$should_clear = true;
					}

					// Case 2: supplier_id in nested cron_arguments array (e.g., batch and backup crons).
					if ( isset( $args[0]['supplier_id'] ) && intval( $args[0]['supplier_id'] ) === intval( $supplier_id ) ) {
						$should_clear = true;
					}

					if ( $should_clear ) {
						wp_clear_scheduled_hook( $hook_name, $args );
					}
				}
			}
		}
	}

	/**
	 * Check if the given product object is a variation with an existing parent product.
	 * 
	 * @param object $product_object MO_Variable_Product or similar product object.
	 * @return bool True if it's a variation with existing parent, false otherwise.
	 */
	public static function is_variation_with_existing_parent( $product_object ) {

		if ( ! isset( $product_object->parent_product_id ) ) {
			return false;
		}
		
		$parent_id = $product_object->parent_product_id;
		
		if ( empty( $parent_id ) ) {
			return false;
		}
		
		$parent_product = wc_get_product( $parent_id );
		
		if ( ! $parent_product ) {
			return false;
		}
		
		if ( ! $parent_product->is_type( 'variable' ) ) {
			return false;
		}
		
		return true;
	}

	/**
	 * Validate that selected filter connections are not already used by other
	 * filters for the same supplier.
	 *
	 * In non-breaking mode, when $existing_connection_uuids are provided, only
	 * newly added connections are validated. Existing unchanged connections are ignored.
	 *
	 * @param int         $supplier_id Supplier ID.
	 * @param array       $selected_connection_uuids Selected connection UUIDs.
	 * @param string      $exclude_setting_uuid Optional setting UUID to exclude from check.
	 * @param array       $existing_connection_uuids Optional existing connection UUIDs.
	 * @return true|\WP_Error
	 */
	public static function validate_filter_connections_not_used_in_other_filters( $supplier_id, $selected_connection_uuids, $exclude_setting_uuid = '', $existing_connection_uuids = array() ) {
		$supplier_id = intval( $supplier_id );
		if ( $supplier_id <= 0 ) {
			return true;
		}

		$selected_connection_uuids = self::normalize_connection_uuid_list( $selected_connection_uuids );
		$existing_connection_uuids = self::normalize_connection_uuid_list( $existing_connection_uuids );

		if ( empty( $selected_connection_uuids ) ) {
			return true;
		}

		$connections_to_validate = empty( $existing_connection_uuids )
			? $selected_connection_uuids
			: array_values( array_diff( $selected_connection_uuids, $existing_connection_uuids ) );

		if ( empty( $connections_to_validate ) ) {
			return true;
		}

		$all_filter_settings = Setting_Model::get_all(
			array(
				'supplier_id' => $supplier_id,
				'type'        => 'filter',
			)
		);

		$used_connection_uuids = array();
		foreach ( $all_filter_settings as $filter_setting ) {
			$setting_uuid = $filter_setting['uuid'] ?? '';
			if ( ! empty( $exclude_setting_uuid ) && $setting_uuid === $exclude_setting_uuid ) {
				continue;
			}

			if ( empty( $filter_setting['connections'] ) || ! is_array( $filter_setting['connections'] ) ) {
				continue;
			}

			foreach ( $filter_setting['connections'] as $connection ) {
				$connection_uuid = sanitize_text_field( strval( $connection['uuid'] ?? '' ) );
				if ( ! empty( $connection_uuid ) ) {
					$used_connection_uuids[ $connection_uuid ] = true;
				}
			}
		}

		$duplicate_connections = array_values(
			array_intersect( $connections_to_validate, array_keys( $used_connection_uuids ) )
		);

		if ( ! empty( $duplicate_connections ) ) {
			return new \WP_Error(
				Constants::ERROR_INVALID_CONFIGURATION,
				'One or more selected API connections are already configured in another filter for this supplier.',
				array( 'status' => Constants::HTTP_BAD_REQUEST )
			);
		}

		return true;
	}

	/**
	 * Normalize and sanitize connection UUID list.
	 *
	 * @param mixed $connection_uuids Connection UUID list.
	 * @return array
	 */
	private static function normalize_connection_uuid_list( $connection_uuids ) {
		if ( ! is_array( $connection_uuids ) ) {
			return array();
		}

		$connection_uuids = array_map(
			function ( $connection_uuid ) {
				return sanitize_text_field( strval( $connection_uuid ) );
			},
			$connection_uuids
		);

		$connection_uuids = array_filter( $connection_uuids );

		return array_values( array_unique( $connection_uuids ) );
	}
}
