<?php
/**
 * REST API Products controller
 *
 * Handles requests to the /products endpoint.
 *
 * @package WooCommerce\RestApi
 * @since   2.6.0
 */

declare(strict_types=1);

namespace Automattic\WooCommerce\Internal\RestApi\Routes\V4\Products;

use Automattic\WooCommerce\Enums\CatalogVisibility;
use Automattic\WooCommerce\Enums\DimensionUnit;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\ProductStockStatus;
use Automattic\WooCommerce\Enums\ProductTaxStatus;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Enums\WeightUnit;
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareRestControllerTrait;
use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use Automattic\WooCommerce\Utilities\I18nUtil;
use Automattic\WooCommerce\Utilities\MetaDataUtil;
use WC_REST_Products_V2_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WC_Admin_Duplicate_Product;
use WC_REST_CRUD_Controller;
use WC_Data_Store;
use WC_Product_Attribute;
use WC_Product_Factory;
use WC_Product_Simple;
use WC_REST_Exception;


defined( 'ABSPATH' ) || exit;

/**
 * REST API Products controller class.
 *
 * @extends WC_REST_Products_V2_Controller
 */
class Controller extends WC_REST_Products_V2_Controller {

	use CogsAwareRestControllerTrait;

	/**
	 * Fields stripped from the response for users without product management capabilities
	 * (e.g. authors who can view published products via the edit_posts fallback).
	 *
	 * @since 10.8.0
	 */
	private const SENSITIVE_FIELDS = array(
		'cost_of_goods_sold',
		'downloads',
		'download_limit',
		'download_expiry',
		'meta_data',
		'purchase_note',
	);

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc/v4';

	/**
	 * The value of the 'search_sku' argument if present.
	 *
	 * See prepare_objects_query()
	 *
	 * @var string
	 */
	private $search_sku_arg_value = '';

	/**
	 * If the 'search_name_or_sku' argument is present this will be set
	 * to an array of the (space-separated) tokens that form the argument value.
	 *
	 * @var array|null
	 */
	private $search_name_or_sku_tokens = null;

	/**
	 * If the 'search_fields' argument is present with 'search' this will be set
	 * to an array containing the fields to search and tokenized search terms.
	 *
	 * @var array|null
	 */
	private $search_fields_tokens = null;

	/**
	 * Suggested product ids.
	 *
	 * @var array
	 */
	private $suggested_products_ids = array();

	/**
	 * Product statuses to exclude from the query.
	 *
	 * @var array
	 */
	private $exclude_status = array();

	/**
	 * Stock quantity bounds for the current collection query.
	 *
	 * @var array
	 */
	private $stock_quantity_filter = array();

	/**
	 * Stores attachment IDs processed during the current request for potential cleanup.
	 *
	 * @var array
	 */
	private $processed_attachment_ids_for_request = array();

	/**
	 * Register the routes for products.
	 */
	public function register_routes() {
		parent::register_routes();

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/suggested-products',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => $this->with_cache(
						array( $this, 'get_suggested_products' ),
						array(
							'endpoint_id'              => 'get_suggested_products',
							'relevant_version_strings' => array( 'list_products' ),
						)
					),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_suggested_products_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/duplicate',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'duplicate_product' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Override the get_item permissions so that published products which are
	 * not password-protected are available to users without the
	 * 'read_private_posts' capability but can edit posts.
	 * This is required for the Product block in the editor, see:
	 * https://github.com/woocommerce/woocommerce/pull/61470
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return bool|WP_Error
	 */
	public function get_item_permissions_check( $request ) {
		$object = $this->get_object( (int) $request['id'] );

		if ( $object && 0 !== $object->get_id() ) {
			if ( 'product' !== $object->post_type && 'product_variation' !== $object->post_type ) {
				return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
			}

			$object_id        = $object->get_id();
			$post_type_object = get_post_type_object( $object->post_type );
			$permission       = false;

			if ( $post_type_object instanceof \WP_Post_Type ) {
				// These are the default permissions inherited from
				// `WC_REST_Products_V2_Controller`.
				$permission = current_user_can( $post_type_object->cap->read_private_posts, $object_id );

				// We add an special case when the post is published, not
				// password-protected and the user has post edit capabilities.
				if ( ! $permission && 'publish' === $object->get_status() && ! post_password_required( $object_id ) ) {
					$permission = current_user_can( 'edit_posts' ) && current_user_can( $post_type_object->cap->read, $object_id );
				}
			}

			/**
			* Filter the permission to view a product.
			*
			* @since 10.4.0
			* @param bool $permission The permission to view a product.
			* @param string $cap The capability to check.
			* @param int $object_id The ID of the product.
			* @param string $post_type The post type of the product.
			*/
			$permission = apply_filters( 'woocommerce_rest_check_permissions', $permission, 'read', $object_id, $object->post_type );

			if ( ! $permission ) {
				return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
			}
		}

		return true;
	}

	/**
	 * Duplicate a product and returns the duplicated product.
	 * The product status is set to "draft" and the name includes a "(copy)" at the end by default.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return WP_REST_Response|WP_Error
	 */
	public function duplicate_product( $request ) {
		$product_id = $request->get_param( 'id' );
		$product    = wc_get_product( $product_id );

		if ( ! $product ) {
			return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
		}

		// Creating product object from request data in preparation for copying.
		$updated_product    = $this->prepare_object_for_database( $request );
		$duplicated_product = ( new WC_Admin_Duplicate_Product() )->product_duplicate( $updated_product );

		if ( is_wp_error( $duplicated_product ) ) {
			return new WP_Error( 'woocommerce_rest_product_duplicate_error', $duplicated_product->get_error_message(), array( 'status' => 400 ) );
		}

		$response_data = $duplicated_product->get_data();

		return new WP_REST_Response( $response_data, 200 );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param \WC_Product                          $product  Object data.
	 * @param WP_REST_Request<array<string,mixed>> $request Request object.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $product, $request ) {
		$links = parent::prepare_links( $product, $request );

		if ( $product->is_type( ProductType::VARIABLE ) && $product->has_child() ) {
			$links['variations'] = array();

			foreach ( $product->get_children() as $variation_id ) {
				$links['variations'][] = array(
					'href'       => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $variation_id ) ),
					'embeddable' => true,
				);
			}
		}

		return $links;
	}

	/**
	 * Get the images for a product or product variation.
	 *
	 * @param WC_Product|WC_Product_Variation $product Product instance.
	 * @return array
	 */
	protected function get_images( $product ) {
		$image_size = $this->request['image_size'] ?? 'full';
		$image_size = is_string( $image_size ) && '' !== $image_size ? sanitize_text_field( $image_size ) : 'full';

		$images         = array();
		$attachment_ids = array();

		// Add featured image.
		if ( $product->get_image_id() ) {
			$attachment_ids[] = $product->get_image_id();
		}

		// Add gallery images.
		$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );

		if ( ! empty( $attachment_ids ) ) {
			// Prime caches to reduce future queries.
			_prime_post_caches( $attachment_ids );
		}

		// Build image data.
		foreach ( $attachment_ids as $attachment_id ) {
			$attachment_post = get_post( $attachment_id );
			if ( is_null( $attachment_post ) ) {
				continue;
			}

			$attachment = wp_get_attachment_image_src( $attachment_id, $image_size );

			if ( ! is_array( $attachment ) ) {
				continue;
			}
			$thumbnail = wp_get_attachment_image_src( $attachment_id, 'woocommerce_thumbnail' );

			$images[] = array(
				'id'                => (int) $attachment_id,
				'date_created'      => wc_rest_prepare_date_response( $attachment_post->post_date, false ),
				'date_created_gmt'  => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ),
				'date_modified'     => wc_rest_prepare_date_response( $attachment_post->post_modified, false ),
				'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ),
				'src'               => current( $attachment ),
				'name'              => get_the_title( $attachment_id ),
				'alt'               => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
				'srcset'            => (string) wp_get_attachment_image_srcset( $attachment_id, $image_size ),
				'sizes'             => (string) wp_get_attachment_image_sizes( $attachment_id, $image_size ),
				'thumbnail'         => current( $thumbnail ),
			);
		}

		return $images;
	}

	/**
	 * Make extra product orderby features supported by WooCommerce available to the WC API.
	 * This includes 'price', 'popularity', and 'rating'.
	 *
	 * @param WP_REST_Request $request Request data.
	 * @return array
	 */
	protected function prepare_objects_query( $request ) {
		$args = WC_REST_CRUD_Controller::prepare_objects_query( $request );

		// Set post_status.
		$args['post_status'] = $request['status'];

		// Filter by a list of product statuses.
		if ( ! empty( $request['include_status'] ) ) {
			$args['post_status'] = $request['include_status'];
		}

		if ( ! empty( $request['exclude_status'] ) ) {
			$this->exclude_status = $request['exclude_status'];
		} else {
			$this->exclude_status = array();
		}

		// Filter downloadable products.
		if ( isset( $request['downloadable'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				$args,
				array(
					'key'   => '_downloadable',
					'value' => wc_bool_to_string( $request['downloadable'] ),
				)
			);
		}

		// Filter virtual products.
		if ( isset( $request['virtual'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				$args,
				array(
					'key'   => '_virtual',
					'value' => wc_bool_to_string( $request['virtual'] ),
				)
			);
		}

		// Taxonomy query to filter products by type, category,
		// tag, shipping class, and attribute.
		$tax_query = array();

		// Map between taxonomy name and arg's key.
		$taxonomies = array(
			'product_cat'            => 'category',
			'product_tag'            => 'tag',
			'product_shipping_class' => 'shipping_class',
		);

		// Set tax_query for each passed arg.
		foreach ( $taxonomies as $taxonomy => $key ) {
			if ( ! empty( $request[ $key ] ) ) {
				$tax_query[] = array(
					'taxonomy' => $taxonomy,
					'field'    => 'term_id',
					'terms'    => $request[ $key ],
				);
			}
		}

		if ( ! empty( $request['exclude_category'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_cat',
				'field'    => 'term_id',
				'terms'    => $request['exclude_category'],
				'operator' => 'NOT IN',
			);
		}

		// Filter product type by slug.
		$terms = array();
		if ( ! empty( $request['include_types'] ) ) {
			$terms = $request['include_types'];
		} elseif ( ! empty( $request['type'] ) ) {
			$terms[] = $request['type'];
		}

		if ( ! empty( $terms ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $terms,
			);
		}

		// Add exclude types filter.
		if ( ! empty( $request['exclude_types'] ) ) {
			$tax_query[] = array(
				'taxonomy' => 'product_type',
				'field'    => 'slug',
				'terms'    => $request['exclude_types'],
				'operator' => 'NOT IN',
			);
		}

		// Filter by attribute and term.
		if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
			if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
				$tax_query[] = array(
					'taxonomy' => $request['attribute'],
					'field'    => 'term_id',
					'terms'    => $request['attribute_term'],
				);
			}
		}

		// Build tax_query if taxonomies are set.
		if ( ! empty( $tax_query ) ) {
			if ( ! empty( $args['tax_query'] ) ) {
				$args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
			} else {
				$args['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
			}
		}

		// Filter featured.
		if ( is_bool( $request['featured'] ) ) {
			$args['tax_query'][] = array(
				'taxonomy' => 'product_visibility',
				'field'    => 'name',
				'terms'    => 'featured',
				'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
			);
		}

		// Search parameter precedence: search_fields > search_name_or_sku > search_sku > sku.
		$search_fields = $request['search_fields'] ?? array();
		$search_arg    = trim( $request['search'] ?? '' );

		if ( $search_fields && $search_arg ) {
			$tokens = array_filter( array_map( 'trim', explode( ' ', $search_arg ) ) );

			$this->search_fields_tokens = array(
				'fields' => $search_fields,
				'tokens' => $tokens,
			);

			unset( $request['search'], $request['search_sku'], $request['sku'], $request['search_name_or_sku'], $args['s'] );
		}

		$search_name_or_sku_arg = $request['search_name_or_sku'] ?? '';

		if ( '' !== $search_name_or_sku_arg ) {
			// Do a tokenized search for name or SKU. Supersedes the 'search', 'search_sku' and 'sku' arguments.
			$tokens                          = array_filter( array_map( 'trim', explode( ' ', $search_name_or_sku_arg ) ) );
			$this->search_name_or_sku_tokens = $tokens;

			unset( $request['search'] );
			unset( $args['s'] );
			unset( $request['search_sku'] );
			unset( $request['sku'] );
		} elseif ( wc_product_sku_enabled() ) {
			// Do a partial match for a sku. Supersedes the 'sku' argument, that does exact matching.
			if ( ! empty( $request['search_sku'] ) ) {
				// Store this for use in the query clause filters.
				$this->search_sku_arg_value = $request['search_sku'];

				unset( $request['sku'] );
			}

			// Filter by sku.
			if ( ! empty( $request['sku'] ) ) {
				$skus = explode( ',', $request['sku'] );
				// Include the current string as a SKU too.
				if ( 1 < count( $skus ) ) {
					$skus[] = $request['sku'];
				}

				$args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					$args,
					array(
						'key'     => '_sku',
						'value'   => $skus,
						'compare' => 'IN',
					)
				);
			}
		}

		if ( ! empty( $request['global_unique_id'] ) ) {
			$global_unique_ids  = array_map( 'trim', explode( ',', $request['global_unique_id'] ) );
			$args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				$args,
				array(
					'key'     => '_global_unique_id',
					'value'   => $global_unique_ids,
					'compare' => 'IN',
				)
			);
		}

		// Filter by tax class.
		if ( ! empty( $request['tax_class'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				$args,
				array(
					'key'   => '_tax_class',
					'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
				)
			);
		}

		// Price filter.
		if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
			$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) );  // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		}

		$min_stock_quantity = $request['min_stock_quantity'];
		$max_stock_quantity = $request['max_stock_quantity'];

		if ( null !== $min_stock_quantity || null !== $max_stock_quantity ) {
			$this->stock_quantity_filter = array(
				'min'               => $min_stock_quantity,
				'max'               => $max_stock_quantity,
				'post_statuses'     => (array) $args['post_status'],
				'excluded_statuses' => $this->exclude_status,
			);
		} else {
			$this->stock_quantity_filter = array();
		}

		// Filter product by stock_status.
		if ( ! empty( $request['stock_status'] ) ) {
			$args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				$args,
				array(
					'key'     => '_stock_status',
					'value'   => $request['stock_status'],
					'compare' => 'IN',
				)
			);
		}

		// Filter by on sale products.
		if ( is_bool( $request['on_sale'] ) ) {
			$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
			$on_sale_ids = wc_get_product_ids_on_sale();

			// Use 0 when there's no on sale products to avoid return all products.
			$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;

			if ( true === $request['on_sale'] && ! empty( $this->stock_quantity_filter ) && ! empty( $args['post__in'] ) ) {
				$args['post__in'] = array_values( array_intersect( array_map( 'absint', $args['post__in'] ), array_map( 'absint', $on_sale_ids ) ) );

				if ( empty( $args['post__in'] ) ) {
					$args['post__in'] = array( 0 );
				}
			} else {
				$args[ $on_sale_key ] += $on_sale_ids;
			}
		}

		// Force the post_type argument, since it's not a user input variable.
		if ( ! empty( $request['sku'] ) || ! empty( $request['search_sku'] ) || $this->search_name_or_sku_tokens || $this->search_fields_tokens ) {
			$args['post_type'] = array( 'product', 'product_variation' );
		} else {
			$args['post_type'] = $this->post_type;
		}

		$ordering_args   = WC()->query->get_catalog_ordering_args( $args['orderby'], $args['order'] );
		$args['orderby'] = $ordering_args['orderby'];
		$args['order']   = $ordering_args['order'];
		if ( $ordering_args['meta_key'] ) {
			$args['meta_key'] = $ordering_args['meta_key']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
		}

		/*
		 * When the suggested products ids is not empty,
		 * filter the query to return only the suggested products,
		 * overwriting the post__in parameter.
		 */
		if ( ! empty( $this->suggested_products_ids ) ) {
			$args['post__in'] = $this->suggested_products_ids;
		}

		// Force the post_type argument, since it's not a user input variable.
		if ( ! empty( $request['global_unique_id'] ) ) {
			$args['post_type'] = array( 'product', 'product_variation' );
		}

		return $args;
	}

	/**
	 * Build the variation post status SQL fragment for stock quantity filtering.
	 *
	 * @param array  $post_statuses Product statuses included in the collection query.
	 * @param array  $excluded_statuses Product statuses excluded from the collection query.
	 * @param string $posts_alias Posts table alias.
	 * @return string
	 */
	private function get_stock_quantity_variation_status_where( array $post_statuses, array $excluded_statuses, string $posts_alias = 'posts' ): string {
		global $wpdb;

		$clauses       = array();
		$post_statuses = array_filter( $post_statuses );

		if ( in_array( 'any', $post_statuses, true ) ) {
			$post_statuses = array( ProductStatus::PUBLISH );
		}

		if ( ! empty( $post_statuses ) ) {
			$prepared_statuses = array();
			foreach ( $post_statuses as $post_status ) {
				$prepared_statuses[] = $wpdb->prepare( '%s', $post_status );
			}

			$clauses[] = "{$posts_alias}.post_status IN ( " . implode( ', ', $prepared_statuses ) . ' )';
		}

		if ( ! empty( $excluded_statuses ) ) {
			$prepared_statuses = array();
			foreach ( $excluded_statuses as $post_status ) {
				$prepared_statuses[] = $wpdb->prepare( '%s', $post_status );
			}

			$clauses[] = "{$posts_alias}.post_status NOT IN ( " . implode( ', ', $prepared_statuses ) . ' )';
		}

		return $clauses ? 'AND ' . implode( ' AND ', $clauses ) : '';
	}

	/**
	 * Get objects.
	 *
	 * @param array $query_args Query args.
	 * @return array
	 */
	protected function get_objects( $query_args ) {
		$add_search_criteria       = $this->search_sku_arg_value || $this->search_name_or_sku_tokens || $this->search_fields_tokens;
		$add_stock_quantity_filter = ! empty( $this->stock_quantity_filter );

		// Add filters for search criteria in product postmeta via the lookup table.
		if ( $add_search_criteria ) {
			add_filter( 'posts_join', array( $this, 'add_search_criteria_to_wp_query_join' ) );
			add_filter( 'posts_where', array( $this, 'add_search_criteria_to_wp_query_where' ) );
		}

		// Add filters for stock quantity ranges.
		if ( $add_stock_quantity_filter ) {
			add_filter( 'posts_where', array( $this, 'add_stock_quantity_to_wp_query_where' ) );
		}

		// Add filters for excluding product statuses.
		if ( ! empty( $this->exclude_status ) ) {
			add_filter( 'posts_where', array( $this, 'exclude_product_statuses' ) );
		}

		$result = parent::get_objects( $query_args );

		// Remove filters for search criteria in product postmeta via the lookup table.
		if ( $add_search_criteria ) {
			remove_filter( 'posts_join', array( $this, 'add_search_criteria_to_wp_query_join' ) );
			remove_filter( 'posts_where', array( $this, 'add_search_criteria_to_wp_query_where' ) );

			$this->search_sku_arg_value      = '';
			$this->search_name_or_sku_tokens = null;
			$this->search_fields_tokens      = null;
		}

		// Remove filters for stock quantity ranges.
		if ( $add_stock_quantity_filter ) {
			remove_filter( 'posts_where', array( $this, 'add_stock_quantity_to_wp_query_where' ) );

			$this->stock_quantity_filter = array();
		}

		// Remove filters for excluding product statuses.
		if ( ! empty( $this->exclude_status ) ) {
			remove_filter( 'posts_where', array( $this, 'exclude_product_statuses' ) );

			$this->exclude_status = array();
		}

		// Batch-prime image attachment caches for the whole collection, rather than once per
		// product when get_images() runs during serialization.
		if ( ! empty( $result['objects'] ) ) {
			wc_get_container()->get( ProductUtil::class )->prime_image_caches( $result['objects'] );
		}

		return $result;
	}

	/**
	 * Add stock quantity bounds to the product collection query.
	 *
	 * Product variation stock is stored against the variation, but the products
	 * collection returns variable parent products by default. Matching variation
	 * rows are also mapped back to their parent product IDs.
	 *
	 * @param string $where Where clause used to search posts.
	 * @return string
	 */
	public function add_stock_quantity_to_wp_query_where( $where ) {
		if ( empty( $this->stock_quantity_filter ) ) {
			return $where;
		}

		global $wpdb;

		$direct_stock_where    = $this->get_stock_quantity_where_clause( 'direct_lookup' );
		$variation_stock_where = $this->get_stock_quantity_where_clause( 'variation_lookup' );
		$variation_status      = $this->get_stock_quantity_variation_status_where(
			$this->stock_quantity_filter['post_statuses'],
			$this->stock_quantity_filter['excluded_statuses'],
			'variation_posts'
		);

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $where . "
			AND (
				EXISTS (
					SELECT 1
					FROM {$wpdb->wc_product_meta_lookup} AS direct_lookup
					WHERE direct_lookup.product_id = {$wpdb->posts}.ID
						AND direct_lookup.stock_quantity IS NOT NULL
						AND {$direct_stock_where}
				)
				OR EXISTS (
					SELECT 1
					FROM {$wpdb->posts} AS variation_posts
					INNER JOIN {$wpdb->wc_product_meta_lookup} AS variation_lookup
						ON variation_posts.ID = variation_lookup.product_id
					WHERE variation_posts.post_type = 'product_variation'
						AND variation_posts.post_parent = {$wpdb->posts}.ID
						AND variation_posts.post_parent > 0
						{$variation_status}
						AND variation_lookup.stock_quantity IS NOT NULL
						AND {$variation_stock_where}
				)
			)";
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Build the stock quantity SQL fragment for the provided lookup table alias.
	 *
	 * @param string $lookup_alias Product lookup table alias.
	 * @return string
	 */
	private function get_stock_quantity_where_clause( string $lookup_alias ): string {
		global $wpdb;

		$min_stock_quantity = $this->stock_quantity_filter['min'];
		$max_stock_quantity = $this->stock_quantity_filter['max'];

		if ( null !== $min_stock_quantity && null !== $max_stock_quantity ) {
			return $wpdb->prepare(
				"{$lookup_alias}.stock_quantity BETWEEN %f AND %f", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				(float) $min_stock_quantity,
				(float) $max_stock_quantity
			);
		}

		if ( null !== $min_stock_quantity ) {
			return $wpdb->prepare( "{$lookup_alias}.stock_quantity >= %f", (float) $min_stock_quantity ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		}

		return $wpdb->prepare( "{$lookup_alias}.stock_quantity <= %f", (float) $max_stock_quantity ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	}

	/**
	 * Join `wc_product_meta_lookup` table when SKU search query is present.
	 *
	 * @param string $join Join clause used to search posts.
	 * @return string
	 */
	public function add_search_criteria_to_wp_query_join( $join ) {
		// Check if already joined to avoid duplicate joins.
		if ( strstr( $join, 'wc_product_meta_lookup' ) ) {
			return $join;
		}

		// Only join if we need meta table search.
		if ( ! $this->search_fields_tokens &&
			! $this->search_sku_arg_value &&
			! ( $this->search_name_or_sku_tokens && wc_product_sku_enabled() ) ) {
			return $join;
		}

		global $wpdb;

		$join .= " LEFT JOIN $wpdb->wc_product_meta_lookup wc_product_meta_lookup
						ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";

		return $join;
	}

	/**
	 * Add a where clause for matching the SKU field.
	 *
	 * @param string $where Where clause used to search posts.
	 * @return string
	 */
	public function add_search_criteria_to_wp_query_where( $where ) {
		global $wpdb;

		if ( $this->search_fields_tokens ) {
			$where .= $this->build_dynamic_search_clauses(
				$this->search_fields_tokens['tokens'],
				$this->search_fields_tokens['fields']
			);
		} elseif ( $this->search_name_or_sku_tokens ) {
			$searchable_fields = wc_product_sku_enabled() ? array( 'name', 'sku' ) : array( 'name' );
			$where            .= $this->build_dynamic_search_clauses(
				$this->search_name_or_sku_tokens,
				$searchable_fields
			);
		} elseif ( ! empty( $this->search_sku_arg_value ) ) {
			$like_search = '%' . $wpdb->esc_like( $this->search_sku_arg_value ) . '%';
			$where      .= ' AND ' . $wpdb->prepare( '(wc_product_meta_lookup.sku LIKE %s)', $like_search );
		}
		return $where;
	}

	/**
	 * Build search clauses for dynamic product search.
	 *
	 * @param array $tokens Search tokens.
	 * @param array $fields Fields to search in.
	 * @return string
	 */
	private function build_dynamic_search_clauses( $tokens, $fields ) {
		global $wpdb;

		if ( empty( $fields ) || empty( $tokens ) ) {
			return '';
		}

		$column_map = array(
			'name'              => "{$wpdb->posts}.post_title",
			'sku'               => 'wc_product_meta_lookup.sku',
			'global_unique_id'  => 'wc_product_meta_lookup.global_unique_id',
			'description'       => "{$wpdb->posts}.post_content",
			'short_description' => "{$wpdb->posts}.post_excerpt",
		);

		$field_clauses = array();

		foreach ( $tokens as $token ) {
			$like_search         = '%' . $wpdb->esc_like( $token ) . '%';
			$field_token_clauses = array();

			foreach ( $fields as $field ) {
				if ( ! isset( $column_map[ $field ] ) ) {
					continue;
				}

				$db_column             = $column_map[ $field ];
				$field_token_clauses[] = $wpdb->prepare( "({$db_column} LIKE %s)", $like_search ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			}

			if ( $field_token_clauses ) {
				$field_clauses[] = '(' . implode( ' OR ', $field_token_clauses ) . ')';
			}
		}

		return $field_clauses ? ' AND (' . implode( ' AND ', $field_clauses ) . ')' : '';
	}

	/**
	 * Exclude product statuses from the query.
	 *
	 * @param string $where Where clause used to search posts.
	 * @return string
	 */
	public function exclude_product_statuses( $where ) {
		if ( ! empty( $this->exclude_status ) && is_array( $this->exclude_status ) ) {
			global $wpdb;

			$not_in = array();
			foreach ( $this->exclude_status as $status_to_exclude ) {
				$not_in[] = $wpdb->prepare( '%s', $status_to_exclude );
			}

			$not_in = join( ', ', $not_in );
			return $where . " AND $wpdb->posts.post_status NOT IN ( $not_in )";
		}

		return $where;
	}

	/**
	 * Set product images.
	 *
	 * @throws WC_REST_Exception REST API exceptions.
	 * @param WC_Product $product Product instance.
	 * @param array      $images  Images data.
	 * @return WC_Product
	 */
	protected function set_product_images( $product, $images ) {
		$images = is_array( $images ) ? array_filter( $images ) : array();

		if ( ! empty( $images ) ) {
			$gallery = array();

			foreach ( $images as $index => $image ) {
				$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
				// The request can contain an attachment ID, if it doesn't, it's a new upload.
				$is_new_upload = false;

				if ( 0 === $attachment_id && isset( $image['src'] ) ) {
					$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );

					if ( is_wp_error( $upload ) ) {
						/**
						 * Filter to check if it should suppress the image upload error, false by default.
						 */
						if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment
							throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
						} else {
							continue;
						}
					}

					$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
					$is_new_upload = true;
				}

				if ( ! wp_attachment_is_image( $attachment_id ) ) {
					/* translators: %s: image ID */
					throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				}

				if ( $is_new_upload && $attachment_id > 0 ) {
					// Tracking this for rollback purposes.
					$this->processed_attachment_ids_for_request[] = $attachment_id;
				}

				$featured_image = $product->get_image_id();

				if ( 0 === $index ) {
					$product->set_image_id( $attachment_id );
					wc_product_attach_featured_image( $attachment_id, $product, false );
				} else {
					$gallery[] = $attachment_id;
				}

				// Set the image alt if present.
				if ( ! empty( $image['alt'] ) ) {
					update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
				}

				// Set the image name if present.
				if ( ! empty( $image['name'] ) ) {
					wp_update_post(
						array(
							'ID'         => $attachment_id,
							'post_title' => $image['name'],
						)
					);
				}
			}

			$product->set_gallery_image_ids( $gallery );
		} else {
			$product->set_image_id( '' );
			$product->set_gallery_image_ids( array() );
		}

		return $product;
	}

	/**
	 * Prepare a single product for create or update.
	 *
	 * @param  WP_REST_Request $request Request object.
	 * @param  bool            $creating If is creating a new object.
	 * @return WP_Error|WC_Data
	 */
	protected function prepare_object_for_database( $request, $creating = false ) {
		$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;

		// Type is the most important part here because we need to be using the correct class and methods.
		if ( isset( $request['type'] ) ) {
			$classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] );

			if ( ! class_exists( $classname ) ) {
				$classname = 'WC_Product_Simple';
			}

			$product = new $classname( $id );
		} elseif ( isset( $request['id'] ) ) {
			$product = wc_get_product( $id );
		} else {
			$product = new WC_Product_Simple();
		}

		if ( ProductType::VARIATION === $product->get_type() ) {
			return new WP_Error(
				"woocommerce_rest_invalid_{$this->post_type}_id",
				__( 'To manipulate product variations you should use the /products/&lt;product_id&gt;/variations/&lt;id&gt; endpoint.', 'woocommerce' ),
				array(
					'status' => 404,
				)
			);
		}

		// Post title.
		if ( isset( $request['name'] ) ) {
			$product->set_name( wp_filter_post_kses( $request['name'] ) );
		}

		// Post content.
		if ( isset( $request['description'] ) ) {
			$product->set_description( wp_filter_post_kses( $request['description'] ) );
		}

		// Post excerpt.
		if ( isset( $request['short_description'] ) ) {
			$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
		}

		// Post status.
		if ( isset( $request['status'] ) ) {
			$product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : ProductStatus::DRAFT );
		}

		// Post slug.
		if ( isset( $request['slug'] ) ) {
			$product->set_slug( $request['slug'] );
		}

		// Menu order.
		if ( isset( $request['menu_order'] ) ) {
			$product->set_menu_order( $request['menu_order'] );
		}

		// Comment status.
		if ( isset( $request['reviews_allowed'] ) ) {
			$product->set_reviews_allowed( $request['reviews_allowed'] );
		}

		// Post password.
		if ( isset( $request['post_password'] ) ) {
			$product->set_post_password( $request['post_password'] );
		}

		// Virtual.
		if ( isset( $request['virtual'] ) ) {
			$product->set_virtual( $request['virtual'] );
		}

		// Tax status.
		if ( isset( $request['tax_status'] ) ) {
			$product->set_tax_status( $request['tax_status'] );
		}

		// Tax Class.
		if ( isset( $request['tax_class'] ) ) {
			$product->set_tax_class( $request['tax_class'] );
		}

		// Catalog Visibility.
		if ( isset( $request['catalog_visibility'] ) ) {
			$product->set_catalog_visibility( $request['catalog_visibility'] );
		}

		// Purchase Note.
		if ( isset( $request['purchase_note'] ) ) {
			$product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) );
		}

		// Featured Product.
		if ( isset( $request['featured'] ) ) {
			$product->set_featured( $request['featured'] );
		}

		// Shipping data.
		$product = $this->save_product_shipping_data( $product, $request );

		// SKU.
		if ( isset( $request['sku'] ) ) {
			$product->set_sku( wc_clean( $request['sku'] ) );
		}

		// Unique ID.
		if ( isset( $request['global_unique_id'] ) ) {
			$product->set_global_unique_id( wc_clean( $request['global_unique_id'] ) );
		}

		// Attributes.
		if ( isset( $request['attributes'] ) ) {
			$attributes = array();

			foreach ( $request['attributes'] as $attribute ) {
				$attribute_id   = 0;
				$attribute_name = '';

				// Check ID for global attributes or name for product attributes.
				if ( ! empty( $attribute['id'] ) ) {
					$attribute_id   = absint( $attribute['id'] );
					$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
				} elseif ( ! empty( $attribute['name'] ) ) {
					$attribute_name = wc_clean( $attribute['name'] );
				}

				if ( ! $attribute_id && ! $attribute_name ) {
					continue;
				}

				if ( $attribute_id ) {

					if ( isset( $attribute['options'] ) ) {
						$options = $attribute['options'];

						if ( ! is_array( $attribute['options'] ) ) {
							// Text based attributes - Posted values are term names.
							$options = explode( WC_DELIMITER, $options );
						}

						$values = array_map( 'wc_sanitize_term_text_based', $options );
						$values = array_filter( $values, 'strlen' );
					} else {
						$values = array();
					}

					if ( ! empty( $values ) ) {
						// Add attribute to array, but don't set values.
						$attribute_object = new WC_Product_Attribute();
						$attribute_object->set_id( $attribute_id );
						$attribute_object->set_name( $attribute_name );
						$attribute_object->set_options( $values );
						$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
						$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
						$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
						$attributes[] = $attribute_object;
					}
				} elseif ( isset( $attribute['options'] ) ) {
					// Custom attribute - Add attribute to array and set the values.
					if ( is_array( $attribute['options'] ) ) {
						$values = $attribute['options'];
					} else {
						$values = explode( WC_DELIMITER, $attribute['options'] );
					}
					$attribute_object = new WC_Product_Attribute();
					$attribute_object->set_name( $attribute_name );
					$attribute_object->set_options( $values );
					$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
					$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
					$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
					$attributes[] = $attribute_object;
				}
			}
			$product->set_attributes( $attributes );
		}

		// Sales and prices.
		if ( in_array( $product->get_type(), array( ProductType::VARIABLE, ProductType::GROUPED ), true ) ) {
			$product->set_regular_price( '' );
			$product->set_sale_price( '' );
			$product->set_date_on_sale_to( '' );
			$product->set_date_on_sale_from( '' );
			$product->set_price( '' );
		} else {
			// Regular Price.
			if ( isset( $request['regular_price'] ) ) {
				$product->set_regular_price( $request['regular_price'] );
			}

			// Sale Price.
			if ( isset( $request['sale_price'] ) ) {
				$product->set_sale_price( $request['sale_price'] );
			}

			if ( isset( $request['date_on_sale_from'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from'] );
			}

			if ( isset( $request['date_on_sale_from_gmt'] ) ) {
				$product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
			}

			if ( isset( $request['date_on_sale_to'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to'] );
			}

			if ( isset( $request['date_on_sale_to_gmt'] ) ) {
				$product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
			}
		}

		// Product parent ID.
		if ( isset( $request['parent_id'] ) ) {
			$product->set_parent_id( $request['parent_id'] );
		}

		// Sold individually.
		if ( isset( $request['sold_individually'] ) ) {
			$product->set_sold_individually( $request['sold_individually'] );
		}

		// Stock status; stock_status has priority over in_stock.
		if ( isset( $request['stock_status'] ) ) {
			$stock_status = $request['stock_status'];
		} else {
			$stock_status = $product->get_stock_status();
		}

		// Stock data.
		if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
			// Manage stock.
			if ( isset( $request['manage_stock'] ) ) {
				$product->set_manage_stock( $request['manage_stock'] );
			}

			// Backorders.
			if ( isset( $request['backorders'] ) ) {
				$product->set_backorders( $request['backorders'] );
			}

			if ( $product->is_type( ProductType::GROUPED ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
			} elseif ( $product->is_type( ProductType::EXTERNAL ) ) {
				$product->set_manage_stock( 'no' );
				$product->set_backorders( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( ProductStockStatus::IN_STOCK );
			} elseif ( $product->get_manage_stock() ) {
				// Stock status is always determined by children so sync later.
				if ( ! $product->is_type( ProductType::VARIABLE ) ) {
					$product->set_stock_status( $stock_status );
				}

				// Stock quantity.
				if ( isset( $request['stock_quantity'] ) ) {
					$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
				} elseif ( isset( $request['inventory_delta'] ) ) {
					$stock_quantity  = wc_stock_amount( $product->get_stock_quantity() );
					$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
					$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
				}

				// Low stock amount.
				// isset() returns false for value null, thus we need to check whether the value has been sent by the request.
				if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) {
					if ( null === $request['low_stock_amount'] ) {
						$product->set_low_stock_amount( '' );
					} else {
						$product->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) );
					}
				}
			} else {
				// Don't manage stock.
				$product->set_manage_stock( 'no' );
				$product->set_stock_quantity( '' );
				$product->set_stock_status( $stock_status );
				$product->set_low_stock_amount( '' );
			}
		} elseif ( ! $product->is_type( ProductType::VARIABLE ) ) {
			$product->set_stock_status( $stock_status );
		}

		// Upsells.
		if ( isset( $request['upsell_ids'] ) ) {
			$upsells = array();
			$ids     = $request['upsell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$upsells[] = $id;
					}
				}
			}

			$product->set_upsell_ids( $upsells );
		}

		// Cross sells.
		if ( isset( $request['cross_sell_ids'] ) ) {
			$crosssells = array();
			$ids        = $request['cross_sell_ids'];

			if ( ! empty( $ids ) ) {
				foreach ( $ids as $id ) {
					if ( $id && $id > 0 ) {
						$crosssells[] = $id;
					}
				}
			}

			$product->set_cross_sell_ids( $crosssells );
		}

		// Product categories.
		if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
			$product = $this->save_taxonomy_terms( $product, $request['categories'] );
		}

		// Product tags.
		if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
			$new_tags = array();

			foreach ( $request['tags'] as $tag ) {
				if ( ! isset( $tag['name'] ) ) {
					$new_tags[] = $tag;
					continue;
				}

				if ( ! term_exists( $tag['name'], 'product_tag' ) ) {
					// Create the tag if it doesn't exist.
					$term = wp_insert_term( $tag['name'], 'product_tag' );

					if ( ! is_wp_error( $term ) ) {
						$new_tags[] = array(
							'id' => $term['term_id'],
						);

						continue;
					}
				} else {
					// Tag exists, assume user wants to set the product with this tag.
					$new_tags[] = array(
						'id' => get_term_by( 'name', $tag['name'], 'product_tag' )->term_id,
					);
				}
			}

			$product = $this->save_taxonomy_terms( $product, $new_tags, 'tag' );
		}

		// Downloadable.
		if ( isset( $request['downloadable'] ) ) {
			$product->set_downloadable( $request['downloadable'] );
		}

		// Downloadable options.
		if ( $product->get_downloadable() ) {

			// Downloadable files.
			if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
				$product = $this->save_downloadable_files( $product, $request['downloads'] );
			}

			// Download limit.
			if ( isset( $request['download_limit'] ) ) {
				$product->set_download_limit( $request['download_limit'] );
			}

			// Download expiry.
			if ( isset( $request['download_expiry'] ) ) {
				$product->set_download_expiry( $request['download_expiry'] );
			}
		}

		// Product url and button text for external products.
		if ( $product->is_type( ProductType::EXTERNAL ) ) {
			if ( isset( $request['external_url'] ) ) {
				$product->set_product_url( $request['external_url'] );
			}

			if ( isset( $request['button_text'] ) ) {
				$product->set_button_text( sanitize_text_field( $request['button_text'] ) );
			}
		}

		// Save default attributes for variable products.
		if ( $product->is_type( ProductType::VARIABLE ) ) {
			$product = $this->save_default_attributes( $product, $request );
		}

		// Set children for a grouped product.
		if ( $product->is_type( ProductType::GROUPED ) && isset( $request['grouped_products'] ) ) {
			$product->set_children( $request['grouped_products'] );
		}

		// Check for featured/gallery images, upload it and set it.
		if ( isset( $request['images'] ) ) {
			$product = $this->set_product_images( $product, $request['images'] );
		}

		// Allow set meta_data.
		MetaDataUtil::update( $request['meta_data'], $product ); // @phpstan-ignore argument.type (missing `use WC_Product` causes phantom namespace-local type)

		if ( ! empty( $request['date_created'] ) ) {
			$date = rest_parse_date( $request['date_created'] );

			if ( $date ) {
				$product->set_date_created( $date );
			}
		}

		if ( ! empty( $request['date_created_gmt'] ) ) {
			$date = rest_parse_date( $request['date_created_gmt'], true );

			if ( $date ) {
				$product->set_date_created( $date );
			}
		}

		if ( $this->cogs_is_enabled() ) {
			$this->set_cogs_info_in_product_object( $request, $product );
		}

		/**
		 * Filters an object before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`,
		 * refers to the object type slug.
		 *
		 * @param WC_Data         $product  Object object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating If is creating a new object.
		 */
		return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment
	}

	/**
	 * Get the Product's schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$weight_unit_label    = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', WeightUnit::KILOGRAM ) );
		$dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', DimensionUnit::CENTIMETER ) );
		$schema               = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'                    => array(
					'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'                  => array(
					'description' => __( 'Product name.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'slug'                  => array(
					'description' => __( 'Product slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'permalink'             => array(
					'description' => __( 'Product URL.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_created'          => array(
					'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_created_gmt'      => array(
					'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_modified'         => array(
					'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_modified_gmt'     => array(
					'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'type'                  => array(
					'description' => __( 'Product type.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => ProductType::SIMPLE,
					'enum'        => array_keys( wc_get_product_types() ),
					'context'     => array( 'view', 'edit' ),
				),
				'status'                => array(
					'description' => __( 'Product status (post status).', 'woocommerce' ),
					'type'        => 'string',
					'default'     => ProductStatus::PUBLISH,
					'enum'        => array_merge( array_keys( get_post_statuses() ), array( ProductStatus::FUTURE, ProductStatus::AUTO_DRAFT, ProductStatus::TRASH ) ),
					'context'     => array( 'view', 'edit' ),
				),
				'featured'              => array(
					'description' => __( 'Featured product.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'catalog_visibility'    => array(
					'description' => __( 'Catalog visibility.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => CatalogVisibility::VISIBLE,
					'enum'        => array( CatalogVisibility::VISIBLE, CatalogVisibility::CATALOG, CatalogVisibility::SEARCH, CatalogVisibility::HIDDEN ),
					'context'     => array( 'view', 'edit' ),
				),
				'description'           => array(
					'description' => __( 'Product description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'short_description'     => array(
					'description' => __( 'Product short description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sku'                   => array(
					'description' => __( 'Stock Keeping Unit.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'global_unique_id'      => array(
					'description' => __( 'GTIN, UPC, EAN or ISBN.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'price'                 => array(
					'description' => __( 'Current product price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'regular_price'         => array(
					'description' => __( 'Product regular price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'sale_price'            => array(
					'description' => __( 'Product sale price.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from'     => array(
					'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_from_gmt' => array(
					'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to'       => array(
					'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'date_on_sale_to_gmt'   => array(
					'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
					'type'        => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'price_html'            => array(
					'description' => __( 'Price formatted in HTML.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'on_sale'               => array(
					'description' => __( 'Shows if the product is on sale.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'purchasable'           => array(
					'description' => __( 'Shows if the product can be bought.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'total_sales'           => array(
					'description' => __( 'Amount of sales.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'virtual'               => array(
					'description' => __( 'If the product is virtual.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloadable'          => array(
					'description' => __( 'If the product is downloadable.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'downloads'             => array(
					'description' => __( 'List of downloadable files.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'File ID.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'File name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'file' => array(
								'description' => __( 'File URL.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'download_limit'        => array(
					'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'download_expiry'       => array(
					'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
					'type'        => 'integer',
					'default'     => -1,
					'context'     => array( 'view', 'edit' ),
				),
				'external_url'          => array(
					'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit' ),
				),
				'button_text'           => array(
					'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'tax_status'            => array(
					'description' => __( 'Tax status.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => ProductTaxStatus::TAXABLE,
					'enum'        => array( ProductTaxStatus::TAXABLE, ProductTaxStatus::SHIPPING, ProductTaxStatus::NONE ),
					'context'     => array( 'view', 'edit' ),
				),
				'tax_class'             => array(
					'description' => __( 'Tax class.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'manage_stock'          => array(
					'description' => __( 'Stock management at product level.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'stock_quantity'        => array(
					'description' => __( 'Stock quantity.', 'woocommerce' ),
					'type'        => wc_is_stock_amount_integer() ? 'integer' : 'number',
					'context'     => array( 'view', 'edit' ),
				),
				'stock_status'          => array(
					'description' => __( 'Controls the stock status of the product.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => ProductStockStatus::IN_STOCK,
					'enum'        => array_keys( wc_get_product_stock_status_options() ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders'            => array(
					'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
					'type'        => 'string',
					'default'     => 'no',
					'enum'        => array( 'no', 'notify', 'yes' ),
					'context'     => array( 'view', 'edit' ),
				),
				'backorders_allowed'    => array(
					'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'backordered'           => array(
					'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'low_stock_amount'      => array(
					'description' => __( 'Low Stock amount for the product.', 'woocommerce' ),
					'type'        => array( 'integer', 'null' ),
					'context'     => array( 'view', 'edit' ),
				),
				'sold_individually'     => array(
					'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'view', 'edit' ),
				),
				'weight'                => array(
					/* translators: %s: weight unit */
					'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit_label ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'dimensions'            => array(
					'description' => __( 'Product dimensions.', 'woocommerce' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'length' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit_label ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'width'  => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit_label ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'height' => array(
							/* translators: %s: dimension unit */
							'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit_label ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
					),
				),
				'shipping_required'     => array(
					'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_taxable'      => array(
					'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'shipping_class'        => array(
					'description' => __( 'Shipping class slug.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'shipping_class_id'     => array(
					'description' => __( 'Shipping class ID.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'reviews_allowed'       => array(
					'description' => __( 'Allow reviews.', 'woocommerce' ),
					'type'        => 'boolean',
					'default'     => true,
					'context'     => array( 'view', 'edit' ),
				),
				'post_password'         => array(
					'description' => __( 'Post password.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'average_rating'        => array(
					'description' => __( 'Reviews average rating.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'rating_count'          => array(
					'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'related_ids'           => array(
					'description' => __( 'List of related products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'upsell_ids'            => array(
					'description' => __( 'List of up-sell products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'cross_sell_ids'        => array(
					'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
				),
				'parent_id'             => array(
					'description' => __( 'Product parent ID.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'purchase_note'         => array(
					'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'categories'            => array(
					'description' => __( 'List of categories.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'Category ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Category name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Category slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'brands'                => array(
					'description' => __( 'List of brands.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'Brand ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Brand name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Brand slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'tags'                  => array(
					'description' => __( 'List of tags.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'   => array(
								'description' => __( 'Tag ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name' => array(
								'description' => __( 'Tag name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'slug' => array(
								'description' => __( 'Tag slug.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
						),
					),
				),
				'images'                => array(
					'description' => __( 'List of images.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'                => array(
								'description' => __( 'Image ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'date_created'      => array(
								'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_created_gmt'  => array(
								'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_modified'     => array(
								'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'date_modified_gmt' => array(
								'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
								'type'        => 'date-time',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'src'               => array(
								'description' => __( 'Image URL.', 'woocommerce' ),
								'type'        => 'string',
								'format'      => 'uri',
								'context'     => array( 'view', 'edit' ),
							),
							'name'              => array(
								'description' => __( 'Image name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'alt'               => array(
								'description' => __( 'Image alternative text.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'has_options'           => array(
					'description' => __( 'Shows if the product needs to be configured before it can be bought.', 'woocommerce' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'attributes'            => array(
					'description' => __( 'List of attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'        => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name'      => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'position'  => array(
								'description' => __( 'Attribute position.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'visible'   => array(
								'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'variation' => array(
								'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ),
								'type'        => 'boolean',
								'default'     => false,
								'context'     => array( 'view', 'edit' ),
							),
							'options'   => array(
								'description' => __( 'List of available term names of the attribute.', 'woocommerce' ),
								'type'        => 'array',
								'items'       => array(
									'type' => 'string',
								),
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'default_attributes'    => array(
					'description' => __( 'Defaults variation attributes.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'     => array(
								'description' => __( 'Attribute ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
							),
							'name'   => array(
								'description' => __( 'Attribute name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'option' => array(
								'description' => __( 'Selected attribute term name.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
				'variations'            => array(
					'description' => __( 'List of variations IDs.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type' => 'integer',
					),
					'readonly'    => true,
				),
				'grouped_products'      => array(
					'description' => __( 'List of grouped products ID.', 'woocommerce' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'menu_order'            => array(
					'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
				),
				'meta_data'             => array(
					'description' => __( 'Meta data.', 'woocommerce' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit' ),
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'id'    => array(
								'description' => __( 'Meta ID.', 'woocommerce' ),
								'type'        => 'integer',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'key'   => array(
								'description' => __( 'Meta key.', 'woocommerce' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
							),
							'value' => array(
								'description' => __( 'Meta value.', 'woocommerce' ),
								'type'        => 'mixed',
								'context'     => array( 'view', 'edit' ),
							),
						),
					),
				),
			),
		);

		$post_type_obj = get_post_type_object( $this->post_type );
		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
			$schema['properties']['permalink_template'] = array(
				'description' => __( 'Permalink template for the product.', 'woocommerce' ),
				'type'        => 'string',
				'context'     => array( 'edit' ),
				'readonly'    => true,
			);

			$schema['properties']['generated_slug'] = array(
				'description' => __( 'Slug automatically generated from the product name.', 'woocommerce' ),
				'type'        => 'string',
				'context'     => array( 'edit' ),
				'readonly'    => true,
			);
		}

		if ( $this->cogs_is_enabled() ) {
			$schema = $this->add_cogs_related_product_schema( $schema, false );
		}

		// New properties added for v4.

		$schema['properties']['min_price'] = array(
			'description' => __( 'Product minimum price.', 'woocommerce' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit' ),
		);

		$schema['properties']['max_price'] = array(
			'description' => __( 'Product maximum price.', 'woocommerce' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit' ),
		);

		$schema['properties']['add_to_cart'] = array(
			'description' => __( 'Add to cart details.', 'woocommerce' ),
			'type'        => 'object',
			'context'     => array( 'view', 'edit' ),
			'properties'  => array(
				'url'         => array(
					'description' => __( 'Add to cart URL.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'description' => array(
					'description' => __( 'Add to cart description.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'text'        => array(
					'description' => __( 'Add to cart text.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'single_text' => array(
					'description' => __( 'Add to cart single text.', 'woocommerce' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
			),
			'readonly'    => true,
		);

		$schema = $this->add_embed_context_to_schema( $schema );

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Add the embed context to product schema properties available in view context.
	 *
	 * @param array $schema Product schema.
	 * @return array Product schema with embed context support.
	 */
	private function add_embed_context_to_schema( array $schema ): array {
		if ( empty( $schema['properties'] ) || ! is_array( $schema['properties'] ) ) {
			return $schema;
		}

		// WordPress REST embeds cannot be narrowed with _fields. Keep all non-sensitive
		// view fields embeddable for now; scalability is tracked in https://github.com/woocommerce/woocommerce/issues/64652.
		foreach ( $schema['properties'] as $property => $property_schema ) {
			if ( ! is_array( $property_schema ) ) {
				continue;
			}

			if ( in_array( $property, self::SENSITIVE_FIELDS, true ) ) {
				continue;
			}

			$schema['properties'][ $property ] = $this->add_embed_context_to_schema_property( $property_schema );
		}

		return $schema;
	}

	/**
	 * Add the embed context to a schema property and its nested properties.
	 *
	 * @param array $property_schema Schema property.
	 * @return array Schema property with embed context support.
	 */
	private function add_embed_context_to_schema_property( array $property_schema ): array {
		if (
			! empty( $property_schema['context'] ) &&
			is_array( $property_schema['context'] ) &&
			in_array( 'view', $property_schema['context'], true ) &&
			! in_array( 'embed', $property_schema['context'], true )
		) {
			$property_schema['context'][] = 'embed';
		}

		if ( ! empty( $property_schema['properties'] ) && is_array( $property_schema['properties'] ) ) {
			foreach ( $property_schema['properties'] as $property => $nested_property_schema ) {
				if ( ! is_array( $nested_property_schema ) ) {
					continue;
				}

				$property_schema['properties'][ $property ] = $this->add_embed_context_to_schema_property( $nested_property_schema );
			}
		}

		if ( ! empty( $property_schema['items']['properties'] ) && is_array( $property_schema['items']['properties'] ) ) {
			foreach ( $property_schema['items']['properties'] as $property => $nested_property_schema ) {
				if ( ! is_array( $nested_property_schema ) ) {
					continue;
				}

				$property_schema['items']['properties'][ $property ] = $this->add_embed_context_to_schema_property( $nested_property_schema );
			}
		}

		return $property_schema;
	}

	/**
	 * Add new options for 'orderby' to the collection params.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params                    = parent::get_collection_params();
		$params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) );

		unset( $params['in_stock'] );
		$params['stock_status'] = array(
			'description'       => __( 'Limit result set to products with any of the specified stock statuses.', 'woocommerce' ),
			'type'              => array( 'string', 'array' ),
			'items'             => array(
				'type' => 'string',
				'enum' => array_keys( wc_get_product_stock_status_options() ),
			),
			'sanitize_callback' => 'wp_parse_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['search_sku'] = array(
			'description'       => __( "Limit results to those with a SKU that partial matches a string. This argument takes precedence over 'sku'.", 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['search_name_or_sku'] = array(
			'description'       => __( "Limit results to those with a name or SKU that partial matches a string. This argument takes precedence over 'search', 'sku' and 'search_sku'.", 'woocommerce' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$search_fields_enum = array( 'name', 'global_unique_id', 'description', 'short_description' );
		if ( wc_product_sku_enabled() ) {
			$search_fields_enum[] = 'sku';
		}

		$params['search_fields'] = array(
			'description'       => __( 'Limit search to specific fields when used with search parameter. Available fields: name, sku, global_unique_id, description, short_description. This argument takes precedence over all other search parameters.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'string',
				'enum' => $search_fields_enum,
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_slug_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['include_status'] = array(
			'description'       => __( 'Limit result set to products with any of the statuses.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'string',
				'enum' => array_merge( array( 'any', ProductStatus::FUTURE, ProductStatus::TRASH ), array_keys( get_post_statuses() ) ),
			),
			'sanitize_callback' => 'wp_parse_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['exclude_status'] = array(
			'description'       => __( 'Exclude products with any of the statuses from result set.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'string',
				'enum' => array_merge( array( ProductStatus::FUTURE, ProductStatus::TRASH ), array_keys( get_post_statuses() ) ),
			),
			'sanitize_callback' => 'wp_parse_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['include_types'] = array(
			'description'       => __( 'Limit result set to products with any of the types.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'string',
				'enum' => array_keys( wc_get_product_types() ),
			),
			'sanitize_callback' => 'wp_parse_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['exclude_types'] = array(
			'description'       => __( 'Exclude products with any of the types from result set.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'string',
				'enum' => array_keys( wc_get_product_types() ),
			),
			'sanitize_callback' => 'wp_parse_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['exclude_category'] = array(
			'description'       => __( 'Exclude products that belong to specific product category IDs.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['min_stock_quantity'] = array(
			'description'       => __( 'Limit result set to products with stock quantity greater than or equal to the specified amount.', 'woocommerce' ),
			'type'              => wc_is_stock_amount_integer() ? 'integer' : 'number',
			'sanitize_callback' => 'wc_stock_amount',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['max_stock_quantity'] = array(
			'description'       => __( 'Limit result set to products with stock quantity less than or equal to the specified amount.', 'woocommerce' ),
			'type'              => wc_is_stock_amount_integer() ? 'integer' : 'number',
			'sanitize_callback' => 'wc_stock_amount',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['downloadable'] = array(
			'description'       => __( 'Limit result set to downloadable products.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'rest_sanitize_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['virtual'] = array(
			'description'       => __( 'Limit result set to virtual products.', 'woocommerce' ),
			'type'              => 'boolean',
			'sanitize_callback' => 'rest_sanitize_boolean',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Add new options for the suggested-products endpoint.
	 *
	 * @return array
	 */
	public function get_suggested_products_collection_params() {
		$params = parent::get_collection_params();

		$params['categories'] = array(
			'description'       => __( 'Limit result set to specific product categorie ids.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'sanitize_callback' => 'wp_parse_id_list',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$params['tags'] = array(
			'description'       => __( 'Limit result set to specific product tag ids.', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array(
				'type' => 'integer',
			),
			'default'           => array(),
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'wp_parse_id_list',
		);

		$params['limit'] = array(
			'description'       => __( 'Limit result set to specific amount of suggested products.', 'woocommerce' ),
			'type'              => 'integer',
			'default'           => 5,
			'validate_callback' => 'rest_validate_request_arg',
			'sanitize_callback' => 'absint',
		);

		return $params;
	}

	/**
	 * Get the downloads for a product.
	 *
	 * @param WC_Product $product Product instance.
	 *
	 * @return array
	 */
	protected function get_downloads( $product ) {
		$downloads = array();

		$context = isset( $this->request ) && isset( $this->request['context'] ) ? $this->request['context'] : 'view';

		if ( $product->is_downloadable() || 'edit' === $context ) {
			foreach ( $product->get_downloads() as $file_id => $file ) {
				$downloads[] = array(
					'id'                           => $file_id,
					// MD5 hash.
											'name' => $file['name'],
					'file'                         => $file['file'],
				);
			}
		}

		return $downloads;
	}

	/**
	 * Get product data.
	 *
	 * @param WC_Product $product Product instance.
	 * @param string     $context Request context. Options: 'view' and 'edit'.
	 *
	 * @return array
	 */
	protected function get_product_data( $product, $context = 'view' ) {
		$data = parent::get_product_data( ...func_get_args() );

		if ( isset( $this->request ) ) {
			$fields = $this->get_fields_for_response( $this->request );

			// Add stock_status if needed.
			if ( in_array( 'stock_status', $fields, true ) ) {
				$data['stock_status'] = $product->get_stock_status( $context );
			}

			// Add has_options if needed.
			if ( in_array( 'has_options', $fields, true ) ) {
				$data['has_options'] = $product->has_options( $context );
			}

			if ( in_array( 'post_password', $fields, true ) ) {
				$data['post_password'] = $product->get_post_password( $context );
			}

			if ( in_array( 'global_unique_id', $fields, true ) ) {
				$data['global_unique_id'] = $product->get_global_unique_id( $context );
			}

			if ( in_array( 'min_price', $fields, true ) ) {
				$data['min_price'] = method_exists( $product, 'get_min_price' ) ? $product->get_min_price() : '';
			}

			if ( in_array( 'max_price', $fields, true ) ) {
				$data['max_price'] = method_exists( $product, 'get_max_price' ) ? $product->get_max_price() : '';
			}

			$post_type_obj = get_post_type_object( $this->post_type );
			if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
				$permalink_template_requested = in_array( 'permalink_template', $fields, true );
				$generated_slug_requested     = in_array( 'generated_slug', $fields, true );

				if ( $permalink_template_requested || $generated_slug_requested ) {
					if ( ! function_exists( 'get_sample_permalink' ) ) {
						require_once ABSPATH . 'wp-admin/includes/post.php';
					}

					$sample_permalink = get_sample_permalink( $product->get_id(), $product->get_name(), '' );

					// Add permalink_template if needed.
					if ( $permalink_template_requested ) {
						$data['permalink_template'] = $sample_permalink[0];
					}

					// Add generated_slug if needed.
					if ( $generated_slug_requested ) {
						$data['generated_slug'] = $sample_permalink[1];
					}
				}
			}
		}

		return $data;
	}

	/**
	 * Get the suggested products.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return object
	 */
	public function get_suggested_products( $request ) {
		$categories  = $request->get_param( 'categories' );
		$tags        = $request->get_param( 'tags' );
		$exclude_ids = $request->get_param( 'exclude' );
		$limit       = $request->get_param( 'limit' ) ? $request->get_param( 'limit' ) : 5;

		$data_store = WC_Data_Store::load( 'product' );
		// @phpstan-ignore-next-line method.notFound
		$this->suggested_products_ids = $data_store->get_related_products(
			$categories,
			$tags,
			$exclude_ids,
			$limit,
			null
			// No need to pass the product ID.
		);

		// When no suggested products are found, return an empty array.
		if ( empty( $this->suggested_products_ids ) ) {
			return array();
		}

		// Ensure to respect the limit, since the data store may return more than the limit.
		$this->suggested_products_ids = array_slice( $this->suggested_products_ids, 0, $limit );

		return parent::get_items( $request );
	}

	/**
	 * Core function to prepare a single product output for response
	 * (doesn't fire hooks, ensure_response, or add links).
	 *
	 * @param WC_Data         $object_data Object data.
	 * @param WP_REST_Request $request     Request object.
	 * @param string          $context     Request context.
	 * @return array Product data to be included in the response.
	 */
	protected function prepare_object_for_response_core( $object_data, $request, $context ): array {
		$data = parent::prepare_object_for_response_core( $object_data, $request, $context );

		if ( $this->cogs_is_enabled() ) {
			$this->add_cogs_info_to_returned_product_data( $data, $object_data );
		}

		$data['add_to_cart'] = array(
			'url'         => $object_data->add_to_cart_url(),
			'description' => $object_data->add_to_cart_description(),
			'text'        => $object_data->add_to_cart_text(),
			'single_text' => $object_data->single_add_to_cart_text(),
		);

		$post_type_object = get_post_type_object( 'product' );

		if ( $post_type_object instanceof \WP_Post_Type && ! current_user_can( $post_type_object->cap->read_private_posts ) ) {
			foreach ( self::SENSITIVE_FIELDS as $field ) {
				unset( $data[ $field ] );
			}
		}

		return $data;
	}

	/**
	 * Create a single item.
	 * Handles cleanup of orphaned images if product creation fails.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		$this->processed_attachment_ids_for_request = array();

		$response = parent::create_item( $request );

		if ( is_wp_error( $response ) ) {
			if ( ! empty( $this->processed_attachment_ids_for_request ) ) {
				// Handle deletion of orphaned images.
				foreach ( $this->processed_attachment_ids_for_request as $attachment_id ) {
					wp_delete_attachment( (int) $attachment_id, true );
				}
			}
		}

		$this->processed_attachment_ids_for_request = array();

		return $response;
	}
}
