<?php
/**
 * Reviews REST API Controller
 *
 * @package Ugcify\Admin\API
 */

namespace Ugcify\Admin\API;

use Ugcify\Review\Review;
use Ugcify\Review\ReviewValidator;
use Ugcify\Review\ReviewMeta;
use Ugcify\Review\HelpfulVotes;
use Ugcify\Database\ReviewRepository;
use Ugcify\Email\EmailSender;
use Ugcify\Integrations\WooCommerce\ABVariantRuntime;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

/**
 * Class ReviewsAPI

 * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value
 */
class ReviewsAPI extends WP_REST_Controller {
	/**
	 * Namespace
	 *
	 * @var string
	 */
	protected $namespace = 'ugcify/v1';

	/**
	 * Rest base
	 *
	 * @var string
	 */
	protected $rest_base = 'reviews';

	/**
	 * Review repository
	 *
	 * @var ReviewRepository
	 */
	private $repository;

	/**
	 * Review validator
	 *
	 * @var ReviewValidator
	 */
	private $validator;

	/**
	 * Review meta
	 *
	 * @var ReviewMeta
	 */
	private $meta;

	/**
	 * Helpful votes.
	 *
	 * @var HelpfulVotes
	 */
	private $helpful_votes;

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->repository = new ReviewRepository();
		$this->validator  = new ReviewValidator();
		$this->meta       = new ReviewMeta();
		$this->helpful_votes = new HelpfulVotes();
	}

	/**
	 * Register routes
	 */
	public function register_routes() {
		// List reviews — intentionally public so the frontend widget can display reviews
		// without requiring authentication. Read-only; no sensitive data is exposed.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => '__return_true', // Public: frontend review display.
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => '__return_true', // Public: customers submit reviews from the frontend.
				),
			)
		);

		// Review submission eligibility check.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/can-submit',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'can_submit_review' ),
					'permission_callback' => '__return_true', // Public: rate-limit check before the review form is shown.
				),
			)
		);

		// Single review.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => '__return_true', // Public: frontend lightbox loads individual reviews.
					'args'                => array(
						'id' => array(
							'required'          => true,
							'validate_callback' => function ( $param ) {
								return is_numeric( $param );
							},
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'check_update_permission' ),
					'args'                => array(
						'id' => array(
							'required'          => true,
							'validate_callback' => function ( $param ) {
								return is_numeric( $param );
							},
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array(
						'id' => array(
							'required'          => true,
							'validate_callback' => function ( $param ) {
								return is_numeric( $param );
							},
						),
					),
				),
			)
		);

		// Reply to review.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/reply',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'reply_to_review' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array(
						'id'      => array(
							'required'          => true,
							'validate_callback' => function ( $param ) {
								return is_numeric( $param );
							},
						),
						'message' => array(
							'required'          => true,
							'sanitize_callback' => 'sanitize_textarea_field',
						),
					),
				),
			)
		);

		// Get creator submissions.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/creator-submissions',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_creator_submissions' ),
					'permission_callback' => 'is_user_logged_in',
				),
			)
		);
	}

	/**
	 * Require manage_options or manage_woocommerce for admin-only endpoints.
	 *
	 * @return bool|\WP_Error
	 */
	public function check_admin_permission() {
		if ( current_user_can( 'manage_options' ) || current_user_can( 'manage_woocommerce' ) ) {
			return true;
		}
		return new \WP_Error(
			'rest_forbidden',
			__( 'Sorry, you are not allowed to access this resource.', 'ugcify' ),
			array( 'status' => 403 )
		);
	}

	/**
	 * Permission callback for review updates.
	 * Admins can update any review; logged-in users can only update their own.
	 *
	 * @return bool|\WP_Error
	 */
	public function check_update_permission() {
		if ( current_user_can( 'manage_options' ) || current_user_can( 'manage_woocommerce' ) ) {
			return true;
		}

		if ( is_user_logged_in() ) {
			return true; // Ownership is enforced inside update_item().
		}

		return new \WP_Error(
			'rest_forbidden',
			__( 'Sorry, you are not allowed to access this resource.', 'ugcify' ),
			array( 'status' => 403 )
		);
	}

	/**
	 * Get reviews
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_items( $request ) {
		$filters = array();

		// Map query params to filters.
		$filter_params = array(
			'id',
			'user_id',
			'product_id',
			'status',
			'is_featured',
			'is_pinned',
			'rating',
			'min_rating',
			'max_rating',
			'type',
			'date_from',
			'date_to',
			'has_image',
			'has_video',
			'search_content',
			'search_user_email',
			'search_guest_email',
			'search_author',
			'search_product_name',
			'search_product_id',
			'tags',
			'categories',
			'contributor_type',
			'page',
			'per_page',
			'order_by',
			'order',
			'prioritize_pinned',
			'prioritize_featured',
			'priority_exempt_type',
		);

		foreach ( $filter_params as $param ) {
			$value = $request->get_param( $param );
			if ( null !== $value ) {
				// Convert boolean strings to actual booleans.
				if ( in_array( $param, array( 'has_image', 'has_video', 'prioritize_pinned', 'prioritize_featured', 'priority_exempt_type' ), true ) ) {
					$filters[ $param ] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
				} else {
					$filters[ $param ] = $value;
				}
			}
		}

		$reviews = $this->repository->query( $filters );
		$total   = $this->repository->count( $filters );
		$stats_product_id = isset( $filters['product_id'] ) ? absint( $filters['product_id'] ) : 0;
		$stats_status     = isset( $filters['status'] ) ? sanitize_key( (string) $filters['status'] ) : null;
		$stats_filters    = array(
			'tags'       => isset( $filters['tags'] ) ? $filters['tags'] : array(),
			'categories' => isset( $filters['categories'] ) ? $filters['categories'] : array(),
			'type'       => isset( $filters['type'] ) ? $filters['type'] : array(),
		);
		$avg_rating       = $this->repository->get_average_rating( $stats_product_id, $stats_status, $stats_filters );
		$distribution     = $this->repository->get_rating_distribution( $stats_product_id, $stats_status, $stats_filters );
		$rated_count      = array_sum( $distribution );

		$items = array();
		foreach ( $reviews as $review ) {
			$item         = $review->to_array();
			$meta         = $this->meta->get_meta( $review->id );


			// Author info (name/email) and is_guest_user
			$is_guest_user = empty( $item['user_id'] );
			$item['is_guest_user'] = $is_guest_user;
			if ( $is_guest_user ) {
				// Try meta first, then fallback to $item fields
				$guest_name = isset( $meta['guest_name'] ) ? $meta['guest_name'] : (isset($item['guest_name']) ? $item['guest_name'] : '');
				$guest_email = isset( $meta['guest_email'] ) ? $meta['guest_email'] : (isset($item['guest_email']) ? $item['guest_email'] : '');
				$item['author_name'] = $guest_name;
				$item['author_email'] = $guest_email;
				$avatar_data = get_avatar_data(
					$guest_email,
					array(
						'size'    => 120,
						'default' => '',
					)
				);
			} else {
				$user = get_user_by( 'id', $item['user_id'] );
				$item['author_name'] = $user ? $user->display_name : '';
				$item['author_email'] = $user ? $user->user_email : '';
				$avatar_data = get_avatar_data(
					$item['user_id'],
					array(
						'size'    => 120,
						'default' => '',
					)
				);
			}
			$item['author_avatar'] = ( ! empty( $avatar_data['found_avatar'] ) && ! empty( $avatar_data['url'] ) ) ? $avatar_data['url'] : '';
			unset( $item['user_name'], $item['user_email'], $item['guest_name'], $item['guest_email'], $item['is_guest_user'] );

			// Remove guest_name and guest_email from meta
			unset( $meta['guest_name'], $meta['guest_email'] );

			// Normalize verified_purchase to boolean
			if ( isset( $meta['verified_purchase'] ) ) {
				$vp = $meta['verified_purchase'];
				if ( is_array( $vp ) ) {
					$item['verified_purchase'] = ( isset( $vp[0] ) && ( $vp[0] === '1' || $vp[0] === 'true' ) );
				} else {
					$item['verified_purchase'] = (bool) $vp;
				}
				unset( $meta['verified_purchase'] );
			}

			// Normalize images and videos (including social fallback media).
			$this->normalize_media_fields( $item, $meta );

			// Add product data
			$product_id = isset( $item['product_id'] ) ? $item['product_id'] : 0;
			if ( function_exists( 'wc_get_product' ) && $product_id ) {
				$product = wc_get_product( $product_id );
				if ( $product ) {
					$item['product'] = $this->build_product_payload( $product, false );
				}
			}

			if ( class_exists( '\\Ugcify\\Influencer\\PublicReviewPresentation' ) ) {
				$item['contributor_type'] = \Ugcify\Influencer\PublicReviewPresentation::get_contributor_type( $meta, $item );
				$item['creator_display']  = \Ugcify\Influencer\PublicReviewPresentation::build_review_display_data( $item, $meta );
			} else {
				$item['contributor_type'] = '';
				$item['creator_display']  = array( 'is_creator_content' => false );
			}

			$item['helpful_votes'] = $this->helpful_votes->get_summary( $review->id, get_current_user_id() );
			unset( $meta[ HelpfulVotes::META_KEY ] );

			$item['meta'] = $meta;
			$items[]      = $item;
		}

		$response = new WP_REST_Response( $items, 200 );
		$response->header( 'X-WP-Total', $total );
		$response->header( 'X-Total-Count', $total );
		$response->header( 'X-Total-Items', $total ); // Alternative header name.
		$response->header( 'X-UGCIFY-Average-Rating', (string) $avg_rating );
		$response->header( 'X-UGCIFY-Rated-Count', (string) $rated_count );
		$response->header( 'X-UGCIFY-Rating-Distribution', wp_json_encode( $distribution ) );

		return $response;
	}

	/**
	 * Get single review
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_item( $request ) {
		$id = (int) $request->get_param( 'id' );

		$review = $this->repository->find( $id );

		if ( ! $review ) {
			return new WP_Error(
				'review_not_found',
				__( 'Review not found.', 'ugcify' ),
				array( 'status' => 404 )
			);
		}

		$data = $review->to_array();
		$meta = $this->meta->get_meta( $review->id );


		// Author info (name/email) and is_guest_user
		$is_guest_user = empty( $data['user_id'] );
		$data['is_guest_user'] = $is_guest_user;
		if ( $is_guest_user ) {
			// Try meta first, then fallback to $data fields
			$guest_name = isset( $meta['guest_name'] ) ? $meta['guest_name'] : (isset($data['guest_name']) ? $data['guest_name'] : '');
			$guest_email = isset( $meta['guest_email'] ) ? $meta['guest_email'] : (isset($data['guest_email']) ? $data['guest_email'] : '');
			$data['author_name'] = $guest_name;
			$data['author_email'] = $guest_email;
			$avatar_data = get_avatar_data(
				$guest_email,
				array(
					'size'    => 120,
					'default' => '',
				)
			);
		} else {
			$user = get_user_by( 'id', $data['user_id'] );
			$data['author_name'] = $user ? $user->display_name : '';
			$data['author_email'] = $user ? $user->user_email : '';
			$avatar_data = get_avatar_data(
				$data['user_id'],
				array(
					'size'    => 120,
					'default' => '',
				)
			);
		}
		$data['author_avatar'] = ( ! empty( $avatar_data['found_avatar'] ) && ! empty( $avatar_data['url'] ) ) ? $avatar_data['url'] : '';
		unset( $data['user_name'], $data['user_email'], $data['guest_name'], $data['guest_email'], $data['is_guest_user'] );

		// Remove guest_name and guest_email from meta
		unset( $meta['guest_name'], $meta['guest_email'] );

		// Normalize verified_purchase to boolean
		if ( isset( $meta['verified_purchase'] ) ) {
			$vp = $meta['verified_purchase'];
			if ( is_array( $vp ) ) {
				$data['verified_purchase'] = ( isset( $vp[0] ) && ( $vp[0] === '1' || $vp[0] === 'true' ) );
			} else {
				$data['verified_purchase'] = (bool) $vp;
			}
			unset( $meta['verified_purchase'] );
		}

		// Normalize images and videos (including social fallback media).
		$this->normalize_media_fields( $data, $meta );

		// Add product data
		$product_id = isset( $data['product_id'] ) ? $data['product_id'] : 0;
		if ( function_exists( 'wc_get_product' ) && $product_id ) {
			$product = wc_get_product( $product_id );
			if ( $product ) {
				$data['product'] = $this->build_product_payload( $product, true );
			}
		}

		if ( class_exists( '\\Ugcify\\Influencer\\PublicReviewPresentation' ) ) {
			$data['contributor_type'] = \Ugcify\Influencer\PublicReviewPresentation::get_contributor_type( $meta, $data );
			$data['creator_display']  = \Ugcify\Influencer\PublicReviewPresentation::build_review_display_data( $data, $meta );
		} else {
			$data['contributor_type'] = '';
			$data['creator_display']  = array( 'is_creator_content' => false );
		}

		$data['helpful_votes'] = $this->helpful_votes->get_summary( $review->id, get_current_user_id() );
		unset( $meta[ HelpfulVotes::META_KEY ] );

		$data['meta'] = $meta;

		return new WP_REST_Response( $data, 200 );
	}

	/**
	 * Create review
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_item( $request ) {
		$data = $request->get_json_params();

		if ( empty( $data ) ) {
			return new WP_Error(
				'invalid_request',
				__( 'Invalid JSON data.', 'ugcify' ),
				array( 'status' => 400 )
			);
		}

		// Remove points from input - will be calculated internally.
		unset( $data['points'] );

		// Validate review data.
		$validation = $this->validator->validate( $data );
		if ( is_wp_error( $validation ) ) {
			return new WP_Error(
				$validation->get_error_code(),
				$validation->get_error_message(),
				array( 'status' => 400 )
			);
		}

		// Set defaults.
		$data['status'] = isset( $data['status'] ) ? $data['status'] : 'pending';
		$data['points'] = 0; // Initialize points.

		$product_id       = isset( $data['product_id'] ) ? absint( $data['product_id'] ) : 0;
		$rate_limit_check = $this->validate_review_submission_frequency_limit( $product_id );
		if ( is_wp_error( $rate_limit_check ) ) {
			return $rate_limit_check;
		}

		// Extract meta if provided.
		$meta = isset( $data['meta'] ) ? $data['meta'] : array();
		unset( $data['meta'] );

		// Check for spam before creating review.
		require_once UGCIFY_PATH . 'includes/Admin/API/SpamSettingsAPI.php';
		$content_to_check = isset( $data['content'] ) ? $data['content'] : '';
		if ( SpamSettingsAPI::is_spam( $content_to_check ) ) {
			$data['status'] = 'spam';
		}

		// Auto-determine review type based on media.
		$data['type'] = $this->determine_review_type( $meta );

		// Check verified purchase.
		$is_verified = $this->check_verified_purchase( $data );
		$meta['verified_purchase'] = $is_verified;
		if ( ! is_user_logged_in() ) {
			$client_ip = $this->get_client_ip_address();
			if ( ! empty( $client_ip ) ) {
				$meta['submission_ip'] = $client_ip;
			}
		}

		$meta = $this->maybe_add_creator_submission_meta( $data, $meta );

		// Create review.
		$review = new Review( $data );
		$id     = $this->repository->create( $review );

		if ( is_wp_error( $id ) ) {
			return new WP_Error(
				$id->get_error_code(),
				$id->get_error_message(),
				array( 'status' => 500 )
			);
		}

		// Add meta if provided.
		if ( ! empty( $meta ) && is_array( $meta ) ) {
			foreach ( $meta as $key => $value ) {
				$this->meta->add_meta( $id, $key, $value );
			}
		}

		// Return created review.
		$created_review             = $this->repository->find( $id );
		$response_data              = $created_review->to_array();
		$response_data['meta']      = $this->meta->get_meta( $id );

		// Fire review submitted action for email triggers.
		do_action( 'ugcify_review_submitted', $id, $response_data );

		if ( 'approved' === ( $response_data['status'] ?? '' ) ) {
			do_action( 'ugcify_review_status_changed', $id, 'approved', 'pending' );
		}

		return new WP_REST_Response( $response_data, 201 );
	}

	/**
	 * Ensure reviews created by logged-in creators keep the creator submission marker.
	 *
	 * The influencer portal uses the general review endpoint, so creator submissions
	 * must be tagged here the same way the legacy endpoint already does.
	 *
	 * @param array<string,mixed> $data Review data.
	 * @param array<string,mixed> $meta Review meta payload.
	 * @return array<string,mixed>
	 */
	private function maybe_add_creator_submission_meta( array $data, array $meta ): array {
		// Only tag as creator content when the portal is enabled AND the review
		// was explicitly submitted from the portal page (not the normal review form).
		if (
			! class_exists( '\\Ugcify\\Influencer\\Settings' ) ||
			! \Ugcify\Influencer\Settings::is_enabled()
		) {
			return $meta;
		}

		if ( empty( $data['from_portal'] ) ) {
			return $meta;
		}

		if ( ! is_user_logged_in() ) {
			return $meta;
		}

		$current_user_id = get_current_user_id();
		if ( $current_user_id < 1 ) {
			return $meta;
		}

		$review_user_id = isset( $data['user_id'] ) ? absint( $data['user_id'] ) : 0;
		if ( $review_user_id > 0 && $review_user_id !== $current_user_id ) {
			return $meta;
		}

		if ( ! empty( $meta['_ugcify_contributor_type'] ) ) {
			return $meta;
		}

		$user_repository = new \Ugcify\Database\UserRepository();
		if ( $user_repository->is_creator( $current_user_id ) ) {
			$meta['_ugcify_contributor_type'] = 'influencer';
		}

		return $meta;
	}

	/**
	 * Check if current user can submit a review in the current frequency window.
	 *
	 * @return WP_REST_Response
	 */
	public function can_submit_review( WP_REST_Request $request ) {
		$product_id       = absint( $request->get_param( 'product_id' ) );
		$rate_limit_check = $this->validate_review_submission_frequency_limit( $product_id );

		if ( is_wp_error( $rate_limit_check ) ) {
			return new WP_REST_Response(
				array(
					'can_submit' => false,
					'message'    => $rate_limit_check->get_error_message(),
				),
				429
			);
		}

		return new WP_REST_Response(
			array(
				'can_submit' => true,
				'message'    => '',
			),
			200
		);
	}

	/**
	 * Validate review submission frequency limit.
	 *
	 * @return true|WP_Error
	 */
	private function validate_review_submission_frequency_limit( $product_id = 0 ) {
		global $wpdb;

		$settings = get_option( 'ugcify_review_form_settings', array() );
		if ( empty( $settings['enableReviewFrequencyLimit'] ) ) {
			return true;
		}

		$window_value = isset( $settings['reviewLimitFrequencyValue'] ) ? absint( $settings['reviewLimitFrequencyValue'] ) : 1;
		$window_unit  = isset( $settings['reviewLimitFrequencyUnit'] ) ? sanitize_key( $settings['reviewLimitFrequencyUnit'] ) : 'hour';
		$max_reviews  = isset( $settings['maxReviewsPerFrequency'] ) ? absint( $settings['maxReviewsPerFrequency'] ) : 3;

		if ( $window_value < 1 ) {
			$window_value = 1;
		}
		if ( ! in_array( $window_unit, array( 'minute', 'hour', 'day' ), true ) ) {
			$window_unit = 'hour';
		}
		if ( $max_reviews < 1 ) {
			$max_reviews = 1;
		}

		$window_seconds = $this->get_rate_window_seconds( $window_value, $window_unit );
		$now_dt         = new \DateTime( 'now', wp_timezone() );
		$window_start_dt = clone $now_dt;
		$window_start_dt->modify( '-' . $window_seconds . ' seconds' );
		$window_start   = $window_start_dt->format( 'Y-m-d H:i:s' );
		$table_reviews  = $wpdb->prefix . 'ugcify_reviews';

		$product_id = absint( $product_id );
		if ( $product_id < 1 ) {
			return true;
		}

		if ( is_user_logged_in() ) {
			$user_id = get_current_user_id();
			$review_count = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM {$table_reviews} WHERE user_id = %d AND product_id = %d AND created_at >= %s",
					$user_id,
					$product_id,
					$window_start
				)
			);
		} else {
			$client_ip = $this->get_client_ip_address();
			if ( empty( $client_ip ) ) {
				$client_ip = 'unknown';
			}

			$table_meta = $wpdb->prefix . 'ugcify_review_meta';
			$review_count = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*)
					FROM {$table_reviews} r
					INNER JOIN {$table_meta} m ON m.review_id = r.id
					WHERE m.meta_key = %s
					AND m.meta_value = %s
					AND r.product_id = %d
					AND r.created_at >= %s",
					'submission_ip',
					$client_ip,
					$product_id,
					$window_start
				)
			);
		}

		if ( $review_count >= $max_reviews ) {
			return new WP_Error(
				'review_submission_rate_limited',
				sprintf(
					/* translators: 1: max reviews, 2: frequency value, 3: frequency unit label. */
					__( 'You can only submit %1$d review(s) per %2$d %3$s. Please try again later.', 'ugcify' ),
					$max_reviews,
					$window_value,
					$this->get_frequency_unit_label( $window_unit )
				),
				array( 'status' => 429 )
			);
		}

		return true;
	}

	/**
	 * Get rate limit window in seconds.
	 *
	 * @param int    $value Window value.
	 * @param string $unit Window unit.
	 * @return int
	 */
	private function get_rate_window_seconds( $value, $unit ) {
		$sanitized_value = max( 1, (int) $value );

		switch ( $unit ) {
			case 'day':
				return $sanitized_value * DAY_IN_SECONDS;
			case 'hour':
				return $sanitized_value * HOUR_IN_SECONDS;
			case 'minute':
			default:
				return $sanitized_value * MINUTE_IN_SECONDS;
		}
	}

	/**
	 * Get frequency unit label.
	 *
	 * @param string $unit Frequency unit.
	 * @return string
	 */
	private function get_frequency_unit_label( $unit ) {
		switch ( $unit ) {
			case 'day':
				return __( 'day(s)', 'ugcify' );
			case 'hour':
				return __( 'hour(s)', 'ugcify' );
			case 'minute':
			default:
				return __( 'minute(s)', 'ugcify' );
		}
	}

	/**
	 * Get client IP address.
	 *
	 * @return string
	 */
	private function get_client_ip_address() {
		$ip_sources = array(
			'HTTP_CF_CONNECTING_IP',
			'HTTP_X_FORWARDED_FOR',
			'HTTP_X_REAL_IP',
			'REMOTE_ADDR',
		);

		foreach ( $ip_sources as $source ) {
			if ( empty( $_SERVER[ $source ] ) ) {
				continue;
			}

			$candidate = sanitize_text_field( wp_unslash( $_SERVER[ $source ] ) );
			$parts     = explode( ',', (string) $candidate );
			$ip        = trim( $parts[0] );

			if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
				return $ip;
			}
		}

		return '';
	}

	/**
	 * Update review
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_item( $request ) {
		$id   = (int) $request->get_param( 'id' );
		$data = $request->get_json_params();

		if ( empty( $data ) ) {
			return new WP_Error(
				'invalid_request',
				__( 'Invalid JSON data.', 'ugcify' ),
				array( 'status' => 400 )
			);
		}

		// Check if review exists.
		$review = $this->repository->find( $id );
		if ( ! $review ) {
			return new WP_Error(
				'review_not_found',
				__( 'Review not found.', 'ugcify' ),
				array( 'status' => 404 )
			);
		}

		// Non-admin users can only edit their own reviews and are restricted
		// to content/rating/media fields — they cannot change status, featured, etc.
		$is_admin = current_user_can( 'manage_options' ) || current_user_can( 'manage_woocommerce' );
		if ( ! $is_admin ) {
			$owner_id = (int) ( $review->user_id ?? 0 );
			if ( $owner_id < 1 || $owner_id !== get_current_user_id() ) {
				return new WP_Error(
					'rest_forbidden',
					__( 'You can only edit your own reviews.', 'ugcify' ),
					array( 'status' => 403 )
				);
			}

			// Strip any fields a non-admin must not change.
			$allowed_keys = array( 'rating', 'content', 'meta' );
			foreach ( array_keys( $data ) as $key ) {
				if ( ! in_array( $key, $allowed_keys, true ) ) {
					unset( $data[ $key ] );
				}
			}
		}

		// Validate individual fields if provided.
		if ( isset( $data['rating'] ) ) {
			$validation = $this->validator->validate_rating( $data['rating'] );
			if ( is_wp_error( $validation ) ) {
				return new WP_Error(
					$validation->get_error_code(),
					$validation->get_error_message(),
					array( 'status' => 400 )
				);
			}
		}

		if ( isset( $data['status'] ) ) {
			$validation = $this->validator->validate_status( $data['status'] );
			if ( is_wp_error( $validation ) ) {
				return new WP_Error(
					$validation->get_error_code(),
					$validation->get_error_message(),
					array( 'status' => 400 )
				);
			}

			if ( 'approved' === $data['status'] ) {
				$incoming_product_id = isset( $data['product_id'] ) ? (int) $data['product_id'] : 0;
				$current_product_id  = isset( $review->product_id ) ? (int) $review->product_id : 0;
				$target_product_id   = $incoming_product_id > 0 ? $incoming_product_id : $current_product_id;

				if ( $target_product_id <= 0 ) {
					return new WP_Error(
						'product_required_for_publish',
						__( 'A product must be assigned before publishing this review.', 'ugcify' ),
						array( 'status' => 400 )
					);
				}
			}
		}

		if ( isset( $data['is_featured'] ) ) {
			$data['is_featured'] = (int) (bool) $data['is_featured'];
		}

		if ( isset( $data['is_pinned'] ) ) {
			$data['is_pinned'] = (int) (bool) $data['is_pinned'];
		}

		// Extract and handle meta separately.
		$meta = isset( $data['meta'] ) ? $data['meta'] : null;
		unset( $data['meta'] );

		// Remove points from update - managed internally.
		unset( $data['points'] );

		// If meta is being updated, recalculate type.
		if ( null !== $meta ) {
			$existing_meta = $this->meta->get_meta( $id );
			$merged_meta   = array_merge( $existing_meta, $meta );
			$data['type']  = $this->determine_review_type( $merged_meta );
		}

		// Capture old status and featured state before update for triggers.
		$old_status      = $review->status;
		$old_is_featured = (int) $review->is_featured;

		// Update review.
		$result = $this->repository->update( $id, $data );

		if ( is_wp_error( $result ) ) {
			return new WP_Error(
				$result->get_error_code(),
				$result->get_error_message(),
				array( 'status' => 500 )
			);
		}

		// Update meta if provided.
		if ( null !== $meta && is_array( $meta ) ) {
			foreach ( $meta as $key => $value ) {
				$this->meta->update_meta( $id, $key, $value );
			}
		}

		// Fire status changed action for email triggers.
		if ( isset( $data['status'] ) && $data['status'] !== $old_status ) {
			do_action( 'ugcify_review_status_changed', $id, $data['status'], $old_status );
		}

		// Fire featured changed action for reward triggers.
		if ( isset( $data['is_featured'] ) && $data['is_featured'] !== $old_is_featured ) {
			/**
			 * Fires when a review's featured status changes.
			 *
			 * @param int $review_id    The review ID.
			 * @param int $is_featured  New featured status (1 or 0).
			 * @param int $was_featured Previous featured status (1 or 0).
			 */
			do_action( 'ugcify_review_featured_changed', $id, $data['is_featured'], $old_is_featured );
		}

		// Return updated review.
		$updated_review        = $this->repository->find( $id );
		$response_data         = $updated_review->to_array();
		$response_data['meta'] = $this->meta->get_meta( $id );

		return new WP_REST_Response( $response_data, 200 );
	}

	/**
	 * Delete review
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_item( $request ) {
		$id = (int) $request->get_param( 'id' );

		// Check if review exists.
		$review = $this->repository->find( $id );
		if ( ! $review ) {
			return new WP_Error(
				'review_not_found',
				__( 'Review not found.', 'ugcify' ),
				array( 'status' => 404 )
			);
		}

		$result = $this->repository->delete( $id );

		if ( is_wp_error( $result ) ) {
			return new WP_Error(
				$result->get_error_code(),
				$result->get_error_message(),
				array( 'status' => 500 )
			);
		}

		return new WP_REST_Response(
			array(
				'deleted' => true,
				'id'      => $id,
			),
			200
		);
	}

	/**
	 * Reply to a review via email
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function reply_to_review( $request ) {
		$id      = (int) $request->get_param( 'id' );
		$message = $request->get_param( 'message' );

		// Check if review exists.
		$review = $this->repository->find( $id );
		if ( ! $review ) {
			return new WP_Error(
				'review_not_found',
				__( 'Review not found.', 'ugcify' ),
				array( 'status' => 404 )
			);
		}

		// Determine reviewer email and name.
		$reviewer_email = '';
		$reviewer_name  = '';

		if ( $review->guest_email ) {
			$reviewer_email = $review->guest_email;
			$reviewer_name  = $review->guest_name ?: __( 'Guest', 'ugcify' );
		} elseif ( $review->user_id ) {
			$user = get_user_by( 'id', $review->user_id );
			if ( $user ) {
				$reviewer_email = $user->user_email;
				$reviewer_name  = $user->display_name;
			}
		}

		if ( empty( $reviewer_email ) ) {
			return new WP_Error(
				'no_email',
				__( 'Reviewer email not found.', 'ugcify' ),
				array( 'status' => 400 )
			);
		}

		// Get product information.
		$product      = wc_get_product( $review->product_id );
		$product_name = $product ? $product->get_name() : __( 'Product', 'ugcify' );
		$product_link = $product ? '<a href="' . esc_url( $product->get_permalink() ) . '">' . esc_html( $product_name ) . '</a>' : $product_name;

		// Prepare email placeholder data.
		$placeholder_data = array(
			'user_name'     => $reviewer_name,
			'product_name'  => $product_name,
			'product_link'  => $product_link,
			'reply_message' => nl2br( esc_html( $message ) ),
			'site_name'     => get_bloginfo( 'name' ),
		);

		// Send email using EmailSender.
		$email_sent = EmailSender::send( 'review_reply', $reviewer_email, $placeholder_data );

		if ( ! $email_sent ) {
			return new WP_Error(
				'email_failed',
				__( 'Failed to send email.', 'ugcify' ),
				array( 'status' => 500 )
			);
		}

		// Fire action for MailMint automation trigger.
		do_action( 'ugcify_review_reply_sent', $id, $reviewer_email, $message );

		return new WP_REST_Response(
			array(
				'success' => true,
				'message' => __( 'Reply sent successfully.', 'ugcify' ),
			),
			200
		);
	}

	/**
	 * Determine review type based on meta
	 *
	 * @param array $meta Review meta data.
	 * @return string Review type: text, image, video, or mixed.
	 */
	private function determine_review_type( array $meta ) {
		$has_images = ! empty( $meta['images'] ) && is_array( $meta['images'] ) && count( $meta['images'] ) > 0;
		$has_videos = ! empty( $meta['videos'] ) && is_array( $meta['videos'] ) && count( $meta['videos'] ) > 0;

		if ( $has_images && $has_videos ) {
			return 'mixed';
		}

		if ( $has_videos ) {
			return 'video';
		}

		if ( $has_images ) {
			return 'image';
		}

		return 'text';
	}

	/**
	 * Normalize media fields for API payload.
	 *
	 * Ensures `images` and `videos` are arrays and applies social fallback
	 * from `_social_media_url` when direct media arrays are empty.
	 *
	 * @param array $payload Review payload.
	 * @param array $meta    Review meta payload.
	 * @return void
	 */
	private function normalize_media_fields( array &$payload, array &$meta ) {
		$payload['images'] = $this->extract_media_urls_from_meta( $meta, 'images' );
		$payload['videos'] = $this->extract_media_urls_from_meta( $meta, 'videos' );

		if ( ! empty( $payload['images'] ) || ! empty( $payload['videos'] ) ) {
			return;
		}

		if ( ! $this->is_social_review_type( isset( $payload['type'] ) ? (string) $payload['type'] : '' ) ) {
			return;
		}

		if ( 'youtube' === strtolower( isset( $payload['type'] ) ? (string) $payload['type'] : '' ) ) {
			return;
		}

		$social_media_url = $this->get_meta_string_value( $meta, '_social_media_url' );
		if ( '' === $social_media_url ) {
			return;
		}

		if ( $this->is_social_video_url( $social_media_url ) ) {
			$payload['videos'][] = $social_media_url;
			return;
		}

		$payload['images'][] = $social_media_url;
	}

	/**
	 * Extract media URL list from meta entry.
	 *
	 * @param array  $meta Review meta payload.
	 * @param string $key  Meta key.
	 * @return array<int, string>
	 */
	private function extract_media_urls_from_meta( array &$meta, $key ) {
		$urls = array();

		if ( ! isset( $meta[ $key ] ) ) {
			return $urls;
		}

		$raw = $meta[ $key ];
		if ( is_array( $raw ) && isset( $raw[0] ) ) {
			$raw = maybe_unserialize( $raw[0] );
		}

		if ( is_string( $raw ) ) {
			$raw = '' !== trim( $raw ) ? array( $raw ) : array();
		}

		if ( ! is_array( $raw ) ) {
			$raw = array();
		}

		foreach ( $raw as $entry ) {
			$url = esc_url_raw( (string) $entry );
			if ( '' === $url ) {
				continue;
			}
			$urls[] = $url;
		}

		unset( $meta[ $key ] );

		return array_values( array_unique( $urls ) );
	}

	/**
	 * Get scalar meta value as string.
	 *
	 * @param array  $meta Review meta payload.
	 * @param string $key  Meta key.
	 * @return string
	 */
	private function get_meta_string_value( array $meta, $key ) {
		if ( ! isset( $meta[ $key ] ) ) {
			return '';
		}

		$raw = $meta[ $key ];
		if ( is_array( $raw ) ) {
			$raw = isset( $raw[0] ) ? $raw[0] : '';
		}

		return esc_url_raw( trim( (string) $raw ) );
	}

	/**
	 * Check if review type is social.
	 *
	 * @param string $type Review type.
	 * @return bool
	 */
	private function is_social_review_type( $type ) {
		return in_array( strtolower( $type ), array( 'instagram', 'youtube', 'tiktok' ), true );
	}

	/**
	 * Check if social media URL likely points to a video file.
	 *
	 * @param string $url Media URL.
	 * @return bool
	 */
	private function is_social_video_url( $url ) {
		$path = (string) wp_parse_url( $url, PHP_URL_PATH );
		$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
		$query = strtolower( (string) wp_parse_url( $url, PHP_URL_QUERY ) );

		$ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
		if ( in_array( $ext, array( 'mp4', 'webm', 'ogg', 'mov', 'm4v' ), true ) ) {
			return true;
		}

		$is_instagram_cdn = false !== strpos( $host, 'cdninstagram.com' ) || false !== strpos( $host, 'fbcdn.net' );
		$has_video_hint = false !== strpos( $path, '/video' ) || false !== strpos( $query, 'video' );

		return $is_instagram_cdn && $has_video_hint;
	}

	/**
	 * Check if user has purchased the product
	 *
	 * @param array $review_data Review data.
	 * @return bool True if verified purchase.
	 */
	private function check_verified_purchase( array $review_data ) {
		// Guest users cannot be verified.
		if ( empty( $review_data['user_id'] ) ) {
			return false;
		}

		$user_id    = $review_data['user_id'];
		$product_id = $review_data['product_id'];

		// Check if WooCommerce is active.
		if ( ! function_exists( 'wc_customer_bought_product' ) ) {
			return false;
		}

		// Get user email.
		$user = get_user_by( 'id', $user_id );
		if ( ! $user ) {
			return false;
		}

		// Check if user has purchased this product.
		return wc_customer_bought_product( $user->user_email, $user_id, $product_id );
	}

	/**
	 * Build normalized product payload for frontend.
	 *
	 * @param \WC_Product $product Product object.
	 * @param bool        $include_variations Include variation matrix for variable products.
	 * @return array
	 */
	private function build_product_payload( $product, $include_variations = false ) {
		$thumbnail_url = get_the_post_thumbnail_url( $product->get_id(), 'thumbnail' );
		$ab_runtime    = new ABVariantRuntime();
		$ab_image_ctx  = $ab_runtime->get_variant_image_context( $product->get_id() );

		if ( ! empty( $ab_image_ctx['can_override_images'] ) && ! empty( $ab_image_ctx['featured_attachment_id'] ) ) {
			$resolved_thumbnail = wp_get_attachment_image_url( absint( $ab_image_ctx['featured_attachment_id'] ), 'thumbnail' );
			if ( $resolved_thumbnail ) {
				$thumbnail_url = $resolved_thumbnail;
			}
		} elseif ( ! empty( $ab_image_ctx['can_override_images'] ) && ! empty( $ab_image_ctx['featured_image_url'] ) ) {
			$thumbnail_url = esc_url_raw( (string) $ab_image_ctx['featured_image_url'] );
		}

		$current_price_raw = $product->get_price();
		$regular_price_raw = $product->get_regular_price();
		$sale_price_raw    = $product->get_sale_price();

		$current_price_display = '' !== $current_price_raw
			? wc_get_price_to_display(
				$product,
				array(
					'price' => (float) $current_price_raw,
				)
			)
			: '';
		$regular_price_display = '' !== $regular_price_raw
			? wc_get_price_to_display(
				$product,
				array(
					'price' => (float) $regular_price_raw,
				)
			)
			: '';
		$sale_price_display = '' !== $sale_price_raw
			? wc_get_price_to_display(
				$product,
				array(
					'price' => (float) $sale_price_raw,
				)
			)
			: '';

		$payload = array(
			'id'                 => $product->get_id(),
			'title'              => $product->get_title(),
			'url'                => get_permalink( $product->get_id() ),
			'price'              => $current_price_raw,
			'regular_price'      => $regular_price_raw,
			'sale_price'         => $sale_price_raw,
			'on_sale'            => $product->is_on_sale(),
			'type'               => $product->get_type(),
			'currency_symbol'    => get_woocommerce_currency_symbol(),
			'price_html'         => $product->get_price_html(),
			'current_price_html' => '' !== $current_price_display ? wc_price( $current_price_display ) : '',
			'regular_price_html' => '' !== $regular_price_display ? wc_price( $regular_price_display ) : '',
			'sale_price_html'    => '' !== $sale_price_display ? wc_price( $sale_price_display ) : '',
			'thumbnail'          => $thumbnail_url,
		);

		if ( $include_variations && $product->is_type( 'variable' ) ) {
			$available_variations = $this->build_available_variations_payload( $product );
			$payload['is_variable']          = true;
			$payload['default_attributes']   = $product->get_default_attributes();
			$payload['variation_attributes'] = $this->build_variation_attributes_payload( $product, $available_variations );
			$payload['available_variations'] = $available_variations;
		} else {
			$payload['is_variable']          = false;
			$payload['default_attributes']   = array();
			$payload['variation_attributes'] = array();
			$payload['available_variations'] = array();
		}

		return $payload;
	}

	/**
	 * Build variation attributes payload.
	 *
	 * @param \WC_Product_Variable $product Variable product object.
	 * @param array                $available_variations Available purchasable variations.
	 * @return array
	 */
	private function build_variation_attributes_payload( $product, $available_variations = array() ) {
		$variation_attributes = $product->get_variation_attributes();
		$attributes_payload   = array();

		foreach ( $variation_attributes as $attribute_key => $options ) {
			$taxonomy_slug = str_replace( 'attribute_', '', $attribute_key );
			$label         = wc_attribute_label( $taxonomy_slug, $product );
			$available_values = array();

			foreach ( $available_variations as $variation ) {
				if ( ! empty( $variation['attributes'][ $attribute_key ] ) ) {
					$available_values[] = $variation['attributes'][ $attribute_key ];
				} elseif ( ! empty( $variation['attributes'][ $taxonomy_slug ] ) ) {
					$available_values[] = $variation['attributes'][ $taxonomy_slug ];
				}
			}

			$available_values = array_map( 'strval', $available_values );
			$available_values = array_unique( $available_values );
			$normalized    = array();

			foreach ( $options as $option ) {
				if ( ! empty( $available_values ) && ! in_array( (string) $option, $available_values, true ) ) {
					continue;
				}

				$normalized[] = array(
					'value' => $option,
					'label' => $this->get_attribute_option_label( $taxonomy_slug, $option ),
				);
			}

			if ( empty( $normalized ) ) {
				continue;
			}

			$attributes_payload[] = array(
				'key'     => $attribute_key,
				'slug'    => $taxonomy_slug,
				'label'   => $label,
				'options' => $normalized,
			);
		}

		return $attributes_payload;
	}

	/**
	 * Build available variations payload.
	 *
	 * @param \WC_Product_Variable $product Variable product object.
	 * @return array
	 */
	private function build_available_variations_payload( $product ) {
		$variations = array();

		foreach ( $product->get_children() as $variation_id ) {
			$variation = wc_get_product( $variation_id );
			if ( ! $variation || ! $variation->exists() || ! $variation->is_purchasable() || ! $variation->is_in_stock() ) {
				continue;
			}

			$current_price_raw = $variation->get_price();
			$regular_price_raw = $variation->get_regular_price();
			$sale_price_raw    = $variation->get_sale_price();

			$current_price_display = '' !== $current_price_raw
				? wc_get_price_to_display(
					$variation,
					array(
						'price' => (float) $current_price_raw,
					)
				)
				: '';
			$regular_price_display = '' !== $regular_price_raw
				? wc_get_price_to_display(
					$variation,
					array(
						'price' => (float) $regular_price_raw,
					)
				)
				: '';
			$sale_price_display = '' !== $sale_price_raw
				? wc_get_price_to_display(
					$variation,
					array(
						'price' => (float) $sale_price_raw,
					)
				)
				: '';

			$variations[] = array(
				'id'                 => $variation->get_id(),
				'attributes'         => $variation->get_attributes(),
				'on_sale'            => $variation->is_on_sale(),
				'in_stock'           => $variation->is_in_stock(),
				'price'              => $current_price_raw,
				'regular_price'      => $regular_price_raw,
				'sale_price'         => $sale_price_raw,
				'price_html'         => $variation->get_price_html(),
				'current_price_html' => '' !== $current_price_display ? wc_price( $current_price_display ) : '',
				'regular_price_html' => '' !== $regular_price_display ? wc_price( $regular_price_display ) : '',
				'sale_price_html'    => '' !== $sale_price_display ? wc_price( $sale_price_display ) : '',
			);
		}

		return $variations;
	}

	/**
	 * Resolve attribute option label.
	 *
	 * @param string $taxonomy_slug Taxonomy slug.
	 * @param string $option_value Attribute option value.
	 * @return string
	 */
	private function get_attribute_option_label( $taxonomy_slug, $option_value ) {
		if ( taxonomy_exists( $taxonomy_slug ) ) {
			$term = get_term_by( 'slug', $option_value, $taxonomy_slug );
			if ( $term && ! is_wp_error( $term ) ) {
				return $term->name;
			}
		}

		return ucwords( str_replace( array( '-', '_' ), ' ', (string) $option_value ) );
	}

	/**
	 * Get creator submissions for the current user.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_creator_submissions( WP_REST_Request $request ) {
		global $wpdb;
		
		$user_id = get_current_user_id();
		
		if ( ! $user_id ) {
			return new WP_Error(
				'not_logged_in',
				__( 'You must be logged in.', 'ugcify' ),
				array( 'status' => 401 )
			);
		}
		
		// Check if user is a creator.
		$user_repository = new \Ugcify\Database\UserRepository();
		if ( ! $user_repository->is_creator( $user_id ) ) {
			return new WP_Error(
				'not_creator',
				__( 'Only creators can view their submissions.', 'ugcify' ),
				array( 'status' => 403 )
			);
		}
		
		// Fetch creator reviews.
		$review_repository = new \Ugcify\Database\ReviewRepository();
		$review_meta_handler = new \Ugcify\Review\ReviewMeta();
		
		$creator_reviews = $review_repository->query_creator_submissions( $user_id, array(
			'per_page' => 100,
			'page'     => 1,
			'order_by' => 'created_at',
			'order'    => 'DESC',
		) );
		
		// Count by status.
		$status_counts = array(
			'all'      => 0,
			'pending'  => 0,
			'approved' => 0,
			'rejected' => 0,
		);
		
		// Format reviews.
		$formatted_reviews = array();
		foreach ( $creator_reviews as $review ) {
			$meta = $review_meta_handler->get_meta( $review->id );
			$images = isset( $meta['images'][0] ) ? maybe_unserialize( $meta['images'][0] ) : array();
			$videos = isset( $meta['videos'][0] ) ? maybe_unserialize( $meta['videos'][0] ) : array();
			
			// Get product details.
			$product_name = '';
			$product_image = '';
			if ( function_exists( 'wc_get_product' ) && $review->product_id ) {
				$product = wc_get_product( $review->product_id );
				if ( $product ) {
					$product_name = $product->get_name();
					$product_image = wp_get_attachment_url( $product->get_image_id() );
				}
			}
			
			$formatted_reviews[] = array(
				'id'            => $review->id,
				'product_id'    => $review->product_id,
				'product_name'  => $product_name,
				'product_image' => $product_image,
				'rating'        => $review->rating,
				'content'       => wp_strip_all_tags( $review->content ),
				'status'        => $review->status,
				'created_at'    => date_i18n( get_option( 'date_format' ), strtotime( $review->created_at ) ),
				'updated_at'    => date_i18n( get_option( 'date_format' ), strtotime( $review->updated_at ) ),
				'images_count'  => count( $images ),
				'videos_count'  => count( $videos ),
			);
			
			$status_counts['all']++;
			if ( isset( $status_counts[ $review->status ] ) ) {
				$status_counts[ $review->status ]++;
			}
		}
		
		// Check for unmigrated reviews.
		$reviews_table = $wpdb->prefix . 'ugcify_reviews';
		$meta_table = $wpdb->prefix . 'ugcify_review_meta';
		
		$unmigrated_count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(r.id) FROM {$reviews_table} AS r 
				LEFT JOIN {$meta_table} AS rm ON r.id = rm.review_id AND rm.meta_key = '_ugcify_contributor_type'
				WHERE r.user_id = %d AND rm.review_id IS NULL",
				$user_id
			)
		);
		
		return rest_ensure_response(
			array(
				'success' => true,
				'data'    => array(
					'reviews'          => $formatted_reviews,
					'counts'           => $status_counts,
					'needs_migration'  => $unmigrated_count > 0,
					'unmigrated_count' => (int) $unmigrated_count,
				),
			)
		);
	}
}
