<?php
/**
 * Store Index REST Controller
 *
 * Handles REST API endpoints for AI Store Index feature.
 *
 * @package AIPT\Api\StoreIndex
 * @since 2.5.0
 * @updated 2.5.0 Added multi-profile support
 */

namespace AIPT\Api\StoreIndex;

use AIPT\Api\StoreIndex\ErrorCode;
use AIPT\Api\StoreIndex\OperationLock;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

/**
 * StoreIndexController class
 *
 * REST API controller for store index management.
 * Data is preloaded via wp_localize_script for initial page load.
 * API endpoints are used for mutations and profile switching only.
 */
class StoreIndexController extends WP_REST_Controller {

	/**
	 * REST API namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'aipt/v1';

	/**
	 * REST API base.
	 *
	 * @var string
	 */
	protected $rest_base = 'store-index';

	/**
	 * ProfileManager instance.
	 *
	 * @var ProfileManager
	 */
	private ProfileManager $profile_manager;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->profile_manager = new ProfileManager();
	}

	/**
	 * Get profile ID from request or use active/default.
	 *
	 * Throws exception if no profile can be determined, as this indicates
	 * a database corruption or migration failure.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return int Profile ID (always > 0).
	 * @throws \RuntimeException If no profile can be determined.
	 */
	private function getProfileId( WP_REST_Request $request ): int {
		$profile_id = $request->get_param( 'profile_id' );
		if ( $profile_id !== null && absint( $profile_id ) > 0 ) {
			return absint( $profile_id );
		}

		$active = $this->profile_manager->getActive();
		if ( $active && isset( $active['id'] ) && (int) $active['id'] > 0 ) {
			return (int) $active['id'];
		}

		$default = $this->profile_manager->getDefault();
		if ( $default && isset( $default['id'] ) && (int) $default['id'] > 0 ) {
			return (int) $default['id'];
		}

		throw new \RuntimeException(
			__( 'No store index profile found. Please deactivate and reactivate the plugin.', 'ai-product-tools' )
		);

	}

	/**
	 * Safe wrapper for getProfileId that returns WP_REST_Response on error.
	 *
	 * Use this in REST endpoints to avoid uncaught RuntimeException causing 500s.
	 * Returns profile ID on success, or WP_REST_Response with error on failure.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return int|WP_REST_Response Profile ID or error response.
	 */
	private function getProfileIdOrResponse( WP_REST_Request $request ) {
		try {
			return $this->getProfileId( $request );
		} catch ( \RuntimeException $e ) {

			return ErrorCode::toRestResponse( $e, ErrorCode::AIPT_INDEX_GENERIC, 500
			);
		}
	}

	/**
	 * Get IndexSettings instance for a profile.
	 *
	 * @param int $profile_id Profile ID.
	 * @return IndexSettings
	 */
	private function getSettings( int $profile_id ): IndexSettings {
		return new IndexSettings( $profile_id );
	}

	/**
	 * Get IndexStorage instance for a profile.
	 *
	 * @param int $profile_id Profile ID.
	 * @return IndexStorage
	 */
	private function getStorage( int $profile_id ): IndexStorage {
		return new IndexStorage( $profile_id );
	}

	/**
	 * Get IndexBuilder instance for a profile.
	 *
	 * @param int $profile_id Profile ID.
	 * @return IndexBuilder
	 */
	private function getBuilder( int $profile_id ): IndexBuilder {
		return new IndexBuilder( $profile_id );
	}

	/**
	 * Register REST API routes.
	 */
	public function register_routes(): void {

		$profile_id_arg = array(
			'profile_id' => array(
				'type'              => 'integer',
				'default'           => 0,
				'description'       => __( 'Profile ID. Defaults to active profile if not specified.', 'ai-product-tools' ),
				'sanitize_callback' => 'absint',
			),
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/status',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_status' ),
					'permission_callback' => array( $this, 'check_permissions' ),
					'args'                => $profile_id_arg,
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/settings',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_settings_endpoint' ),
					'permission_callback' => array( $this, 'check_permissions' ),
					'args'                => $profile_id_arg,
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/settings',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'update_settings' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => array_merge( $profile_id_arg, $this->get_settings_args() ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/build',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'trigger_build' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => array_merge(
						$profile_id_arg,
						array(
							'force' => array(
								'type'        => 'boolean',
								'default'     => false,
								'description' => __( 'Force rebuild, cancelling any running build.', 'ai-product-tools' ),
							),
						)
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/cancel',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'cancel_build' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => array_merge(
						$profile_id_arg,
						array(
							'force' => array(
								'type'        => 'boolean',
								'default'     => false,
								'description' => __( 'Force cancel even if build appears not running (handles stuck states).', 'ai-product-tools' ),
							),
						)
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/process',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'process_batch' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => $profile_id_arg,
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/filter-options',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_filter_options' ),
					'permission_callback' => array( $this, 'check_permissions' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/reset',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'reset_index' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => array_merge(
						$profile_id_arg,
						array(
							'force' => array(
								'type'        => 'boolean',
								'default'     => false,
								'description' => __( 'Force reset even if build appears not running (handles stuck states).', 'ai-product-tools' ),
							),
						)
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/estimate-products',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'estimate_products' ),
					'permission_callback' => array( $this, 'check_permissions' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/profiles',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'list_profiles' ),
					'permission_callback' => array( $this, 'check_permissions' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/profiles',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_profile' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => array(
						'name' => array(
							'type'              => 'string',
							'required'          => true,
							'description'       => __( 'Profile name (max 50 chars, alphanumeric + spaces/dashes).', 'ai-product-tools' ),
							'sanitize_callback' => 'sanitize_text_field',
							'validate_callback' => array( $this, 'validate_profile_name' ),
						),
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/profiles/(?P<id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_profile' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => array(
						'id'   => array(
							'type'              => 'integer',
							'required'          => true,
							'sanitize_callback' => 'absint',
						),
						'name' => array(
							'type'              => 'string',
							'description'       => __( 'Profile name (max 50 chars, alphanumeric + spaces/dashes).', 'ai-product-tools' ),
							'sanitize_callback' => 'sanitize_text_field',
							'validate_callback' => array( $this, 'validate_profile_name' ),
						),
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/profiles/(?P<id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_profile' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => array(
						'id' => array(
							'type'              => 'integer',
							'required'          => true,
							'sanitize_callback' => 'absint',
						),
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/profiles/(?P<id>\d+)/activate',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'activate_profile' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
					'args'                => array(
						'id' => array(
							'type'              => 'integer',
							'required'          => true,
							'sanitize_callback' => 'absint',
						),
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/profiles/global-status',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_global_building_status' ),
					'permission_callback' => array( $this, 'check_permissions' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/profiles/available-fields',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_available_fields' ),
					'permission_callback' => array( $this, 'check_permissions' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/global-settings',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_global_settings' ),
					'permission_callback' => array( $this, 'check_permissions' ),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'update_global_settings' ),
					'permission_callback' => array( $this, 'check_permissions_write' ),
				),
			)
		);
	}

	/**
	 * Check read permission (manage_woocommerce).
	 *
	 * @return bool|WP_Error
	 */
	public function check_permissions() {
		if ( ! $this->check_woocommerce_active() ) {
			return new WP_Error(
				'woocommerce_required',
				__( 'WooCommerce is required for this feature.', 'ai-product-tools' ),
				array( 'status' => 424 )
			);
		}

		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return new WP_Error(
				'rest_forbidden',
				__( 'You do not have permission to access this resource.', 'ai-product-tools' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Check write permission (manage_options).
	 *
	 * @return bool|WP_Error
	 */
	public function check_permissions_write() {
		if ( ! $this->check_woocommerce_active() ) {
			return new WP_Error(
				'woocommerce_required',
				__( 'WooCommerce is required for this feature.', 'ai-product-tools' ),
				array( 'status' => 424 )
			);
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return new WP_Error(
				'rest_forbidden',
				__( 'You do not have permission to modify this resource.', 'ai-product-tools' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Validate profile name for REST API requests.
	 *
	 * Validates that the profile name meets requirements before the request handler is called.
	 * This provides early validation and better error messages.
	 *
	 * @param string          $value   The profile name value.
	 * @param WP_REST_Request $request The request object.
	 * @param string          $param   The parameter name.
	 * @return bool|WP_Error True if valid, WP_Error if invalid.
	 */
	public function validate_profile_name( $value, $request, $param ) {

		if ( empty( $value ) && 'PUT' === $request->get_method() ) {
			return true;
		}

		if ( strlen( $value ) > 50 ) {
			return new WP_Error(
				'invalid_profile_name',
				__( 'Profile name cannot exceed 50 characters.', 'ai-product-tools' ),
				array( 'status' => 400 )
			);
		}

		if ( empty( $value ) && 'POST' === $request->get_method() ) {
			return new WP_Error(
				'invalid_profile_name',
				__( 'Profile name cannot be empty.', 'ai-product-tools' ),
				array( 'status' => 400 )
			);
		}

		if ( ! empty( $value ) && ! preg_match( '/^[a-zA-Z0-9 \-]+$/', $value ) ) {
			return new WP_Error(
				'invalid_profile_name',
				__( 'Profile name can only contain letters, numbers, spaces, and dashes.', 'ai-product-tools' ),
				array( 'status' => 400 )
			);
		}

		return true;
	}

	/**
	 * Check if WooCommerce is active.
	 *
	 * @return bool
	 */
	private function check_woocommerce_active(): bool {
		return class_exists( 'WooCommerce' );
	}

	/**
	 * Get current index status.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_status( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = $this->getProfileIdOrResponse( $request );
		if ( $profile_id instanceof WP_REST_Response ) {
			return $profile_id;
		}

		$settings = $this->getSettings( $profile_id );
		$status   = $settings->getStatus();

		if ( ( $status['status'] ?? '' ) === 'building' ) {
			$status['needs_manual_process'] = $this->hasPendingBuildAction( $profile_id, $status );
		}

		if ( ( $status['status'] ?? '' ) === 'built_empty' ) {
			$status['warning'] = __( 'The store index build finished, but no products matched the active filters.', 'ai-product-tools' );
			if ( ! isset( $status['effective_filter_summary'] ) || ! is_array( $status['effective_filter_summary'] ) ) {
				$status['effective_filter_summary'] = array();
			}
		}

		$status_version = (int) ( $status['meta']['status_version'] ?? 0 );
		$etag           = sprintf( '"aipt-storeindex-%d-%d"', $profile_id, $status_version );

		$if_none_match = $request->get_header( 'if_none_match' );
		if ( is_string( $if_none_match ) ) {

			$client_etag = trim( (string) preg_replace( '#^W/#i', '', trim( $if_none_match ) ) );
			if ( $client_etag === $etag ) {
				$response = new WP_REST_Response( null, 304 );
				$response->header( 'ETag', $etag );
				return $response;
			}
		}

		$response = new WP_REST_Response( $status, 200 );
		$response->header( 'ETag', $etag );
		return $response;
	}

	/**
	 * Check if manual processing is needed for a build.
	 *
	 * Returns true if build is in progress but not yet complete.
	 * This enables the frontend to call /process endpoint for batch processing
	 * without relying on Action Scheduler queries (which can be slow on high-volume sites).
	 *
	 * @param int   $profile_id Profile ID.
	 * @param array $status     Current status array.
	 * @return bool True if manual processing may be needed.
	 */
	private function hasPendingBuildAction( int $profile_id, array $status ): bool {

		$job_id = (int) ( $status['build_job_id'] ?? 0 );
		if ( $job_id === 0 ) {
			return false;
		}

		$progress = (int) ( $status['progress_percent'] ?? 0 );
		if ( $progress >= 100 ) {
			return false;
		}

		return true;
	}

	/**
	 * Process a single pending build batch.
	 *
	 * This endpoint is called by the frontend when async processing
	 * isn't working (Action Scheduler/WP-Cron issues).
	 * Processes exactly one batch per call to avoid timeout issues.
	 *
	 * Uses status-based pagination instead of Action Scheduler queries
	 * to work reliably on high-volume sites with many scheduled actions.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function process_batch( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = $this->getProfileIdOrResponse( $request );
		if ( $profile_id instanceof WP_REST_Response ) {
			return $profile_id;
		}

		$settings = $this->getSettings( $profile_id );

		$settings->clearCache();
		$status = $settings->getStatus();

		$current_status = $status['status'] ?? 'idle';
		$job_id         = (int) ( $status['build_job_id'] ?? 0 );

		if ( $current_status === 'completed' ) {
			return new WP_REST_Response(
				array(
					'success' => true,
					'message' => __( 'Build already complete.', 'ai-product-tools' ),
					'status'  => $status,
				),
				200
			);
		}

		if ( $current_status === 'failed' ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => $status['error'] ?? __( 'Build failed.', 'ai-product-tools' ),
					'status'  => $status,
				),
				200
			);
		}

		if ( $current_status !== 'building' || $job_id === 0 ) {

			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => __( 'No build in progress.', 'ai-product-tools' ),
					'status'  => $status,
				),
				200
			);
		}

		$progress = (int) ( $status['progress_percent'] ?? 0 );
		if ( $progress >= 100 ) {
			return new WP_REST_Response(
				array(
					'success' => true,
					'message' => __( 'Build already complete.', 'ai-product-tools' ),
					'status'  => $status,
				),
				200
			);
		}

		$last_id         = (int) ( $status['last_processed_id'] ?? 0 );
		$processed_count = (int) ( $status['processed_product_count'] ?? 0 );
		$batch_size      = (int) $settings->get( 'batch_size', 100 );

		try {

			$builder = $this->getBuilder( $profile_id );
			$builder->cancelPendingBuildActions();

			$builder->processBatch(
				$job_id,
				$last_id,
				$batch_size,
				$processed_count
			);

			$settings->clearCache();
			$new_status = $settings->getStatus();

			return new WP_REST_Response(
				array(
					'success' => true,
					'message' => __( 'Batch processed.', 'ai-product-tools' ),
					'status'  => $new_status,
				),
				200
			);

		} catch ( \Exception $e ) {

			error_log(
				sprintf(
					'[AIPT] StoreIndexController::process_batch() - Failed: %s | Trace: %s',
					$e->getMessage(),
					$e->getTraceAsString()
				)
			);

			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => __( 'An internal error occurred while processing the batch.', 'ai-product-tools' ),
					'status'  => $settings->getStatus(),
				),
				500
			);
		}
	}

	/**
	 * Get current settings.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_settings_endpoint( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = $this->getProfileIdOrResponse( $request );
		if ( $profile_id instanceof WP_REST_Response ) {
			return $profile_id;
		}

		$settings = $this->getSettings( $profile_id );

		return new WP_REST_Response( $settings->getAll(), 200 );
	}

	/**
	 * Update settings.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_settings( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = $this->getProfileIdOrResponse( $request );
		if ( $profile_id instanceof WP_REST_Response ) {
			return $profile_id;
		}

		$settings = $this->getSettings( $profile_id );
		$params   = $request->get_params();

		$allowed_keys    = array_keys( IndexSettings::getDefaults() );
		$settings_params = array_intersect_key( $params, array_flip( $allowed_keys ) );

		try {
			$persisted = $settings->update( $settings_params );

			if ( ! $persisted ) {

				return ErrorCode::toRestResponse(
					new \RuntimeException( 'IndexSettings::update() returned false' ),
					ErrorCode::AIPT_INDEX_SETTINGS_PERSIST_FAILED,
					500
				);
			}

			return new WP_REST_Response(
				array(
					'success'  => true,
					'settings' => $settings->getAll(),
				),
				200
			);
		} catch ( \InvalidArgumentException $e ) {
			return new WP_REST_Response(
				array(
					'code'    => 'invalid_parameter',
					'message' => $e->getMessage(),
					'data'    => array( 'status' => 400 ),
				),
				400
			);
		}
	}

	/**
	 * Trigger index build.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function trigger_build( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = $this->getProfileIdOrResponse( $request );
		if ( $profile_id instanceof WP_REST_Response ) {
			return $profile_id;
		}

		$op_lock = new OperationLock();
		if ( ! $op_lock->acquire( $profile_id, 'build' ) ) {
			return ErrorCode::toRestResponse(
				new \RuntimeException( 'OperationLock contention on trigger_build' ),
				ErrorCode::AIPT_INDEX_OPERATION_IN_PROGRESS,
				409
			);
		}

		try {
			$settings = $this->getSettings( $profile_id );
			$force    = (bool) $request->get_param( 'force' );

			error_log(
				sprintf(
					'[AIPT] StoreIndexController::trigger_build() - Profile: %d, Force: %s, Enabled: %s',
					$profile_id,
					$force ? 'true' : 'false',
					$settings->isEnabled() ? 'true' : 'false'
				)
			);

			if ( ! $settings->isEnabled() ) {

				return new WP_REST_Response(
					array(
						'code'    => 'feature_disabled',
						'message' => __( 'AI Store Index feature is disabled.', 'ai-product-tools' ),
						'data'    => array( 'status' => 400 ),
					),
					400
				);
			}

			$builder = $this->getBuilder( $profile_id );

			if ( $builder->isRunning() && ! $force ) {

				return new WP_REST_Response(
					array(
						'code'    => 'build_in_progress',
						'message' => __( 'Index build already in progress. Use force=true to restart.', 'ai-product-tools' ),
						'data'    => array(
							'status'           => 409,
							'current_progress' => $builder->getProgress(),
						),
					),
					409
				);
			}

			try {

				$job_id = $builder->start( $force );

				return new WP_REST_Response(
					array(
						'success' => true,
						'message' => __( 'Index build started.', 'ai-product-tools' ),
						'job_id'  => $job_id,
					),
					200
				);
			} catch ( \RuntimeException $e ) {
				return ErrorCode::toRestResponse( $e, ErrorCode::AIPT_INDEX_BUILD_FAILED, 500 );
			} catch ( \InvalidArgumentException $e ) {
				return new WP_REST_Response(
					array(
						'code'    => 'feature_disabled',
						'message' => $e->getMessage(),
						'data'    => array( 'status' => 400 ),
					),
					400
				);
			}
		} finally {
			$op_lock->release();
		}
	}

	/**
	 * Cancel running build.
	 *
	 * Supports force parameter to recover from stuck builds.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function cancel_build( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = $this->getProfileIdOrResponse( $request );
		if ( $profile_id instanceof WP_REST_Response ) {
			return $profile_id;
		}

		$op_lock = new OperationLock();
		if ( ! $op_lock->acquire( $profile_id, 'cancel' ) ) {
			return ErrorCode::toRestResponse(
				new \RuntimeException( 'OperationLock contention on cancel_build' ),
				ErrorCode::AIPT_INDEX_OPERATION_IN_PROGRESS,
				409
			);
		}

		try {
			$force   = (bool) $request->get_param( 'force' );
			$builder = $this->getBuilder( $profile_id );

			$settings       = $this->getSettings( $profile_id );
			$current_status = $settings->getStatus();
			$is_running     = $builder->isRunning( true );

			error_log(
				sprintf(
					'[AIPT] StoreIndexController::cancel_build() - Profile: %d, Force: %s, isRunning: %s, Status: %s, JobID: %s',
					$profile_id,
					$force ? 'true' : 'false',
					$is_running ? 'true' : 'false',
					$current_status['status'] ?? 'unknown',
					$current_status['build_job_id'] ?? 'none'
				)
			);

			if ( ! $is_running && ! $force ) {

				error_log(
					sprintf(
						'[AIPT] StoreIndexController::cancel_build() - RETURNING no_build_running. Full status: %s',
						wp_json_encode( $current_status )
					)
				);
				return new WP_REST_Response(
					array(
						'code'    => 'no_build_running',
						'message' => __( 'No index build is currently running.', 'ai-product-tools' ),
						'data'    => array( 'status' => 400 ),
					),
					400
				);
			}

			$cancelled = $builder->cancel( $force );

			if ( $cancelled ) {
				return new WP_REST_Response(
					array(
						'success' => true,
						'message' => $force
							? __( 'Index build force cancelled (status reset).', 'ai-product-tools' )
							: __( 'Index build cancelled.', 'ai-product-tools' ),
					),
					200
				);
			}

			return new WP_REST_Response(
				array(
					'code'    => 'cancel_failed',
					'message' => __( 'Failed to cancel index build.', 'ai-product-tools' ),
					'data'    => array( 'status' => 500 ),
				),
				500
			);
		} finally {
			$op_lock->release();
		}
	}

	/**
	 * Reset the product index.
	 *
	 * Deletes index file, clears status, preserves settings.
	 * Supports force parameter to recover from stuck builds.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function reset_index( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = $this->getProfileIdOrResponse( $request );
		if ( $profile_id instanceof WP_REST_Response ) {
			return $profile_id;
		}

		$op_lock = new OperationLock();
		if ( ! $op_lock->acquire( $profile_id, 'reset' ) ) {
			return ErrorCode::toRestResponse(
				new \RuntimeException( 'OperationLock contention on reset_index' ),
				ErrorCode::AIPT_INDEX_OPERATION_IN_PROGRESS,
				409
			);
		}

		$force = (bool) $request->get_param( 'force' );

		try {
			$builder  = $this->getBuilder( $profile_id );
			$settings = $this->getSettings( $profile_id );
			$builder->reset( $force );

			return new WP_REST_Response(
				array(
					'success' => true,
					'message' => __( 'Index reset successfully.', 'ai-product-tools' ),
					'status'  => $settings->getStatus(),
				),
				200
			);
		} catch ( \Exception $e ) {
			return ErrorCode::toRestResponse( $e, ErrorCode::AIPT_INDEX_GENERIC, 500 );
		} finally {
			$op_lock->release();
		}
	}

	/**
	 * Get available filter options (categories, tags).
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_filter_options( WP_REST_Request $request ): WP_REST_Response {

		$categories = $this->get_product_categories();

		$tags = $this->get_product_tags();

		$total_products = $this->get_total_products_count();

		return new WP_REST_Response(
			array(
				'categories'     => $categories,
				'tags'           => $tags,
				'total_products' => $total_products,
			),
			200
		);
	}

	/**
	 * Get product categories with counts.
	 *
	 * @return array
	 */
	private function get_product_categories(): array {
		$terms = get_terms(
			array(
				'taxonomy'   => 'product_cat',
				'hide_empty' => true,
				'orderby'    => 'count',
				'order'      => 'DESC',
				'number'     => 10,
			)
		);

		if ( is_wp_error( $terms ) ) {
			return array();
		}

		return array_map(
			function ( $term ) {
				return array(
					'id'    => (int) $term->term_id,
					'name'  => $term->name,
					'count' => (int) $term->count,
				);
			},
			$terms
		);
	}

	/**
	 * Get product tags with counts.
	 *
	 * @return array
	 */
	private function get_product_tags(): array {
		$terms = get_terms(
			array(
				'taxonomy'   => 'product_tag',
				'hide_empty' => true,
				'orderby'    => 'count',
				'order'      => 'DESC',
				'number'     => 10,
			)
		);

		if ( is_wp_error( $terms ) ) {
			return array();
		}

		return array_map(
			function ( $term ) {
				return array(
					'id'    => (int) $term->term_id,
					'name'  => $term->name,
					'count' => (int) $term->count,
				);
			},
			$terms
		);
	}

	/**
	 * Get total published products count.
	 *
	 * @return int
	 */
	private function get_total_products_count(): int {
		$counts = wp_count_posts( 'product' );
		return isset( $counts->publish ) ? (int) $counts->publish : 0;
	}

	/**
	 * Estimate products matching the given filter settings.
	 *
	 * Supports two modes:
	 * - Preview mode (use_saved_settings=false): Uses request filters for live preview while user adjusts settings
	 * - Saved mode (use_saved_settings=true): Uses saved profile settings to match actual build behavior
	 *
	 * This prevents mismatch between "Products to index" shown in UI and actual indexed count.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function estimate_products( WP_REST_Request $request ): WP_REST_Response {
		try {
			$request_filters = $request->get_json_params();
			

			$use_saved_settings = isset( $request_filters['use_saved_settings'] ) && $request_filters['use_saved_settings'];
			
			if ( $use_saved_settings ) {

				$profile_id = $this->getProfileIdOrResponse( $request );
				if ( $profile_id instanceof WP_REST_Response ) {
					return $profile_id;
				}

				$settings = new IndexSettings( $profile_id );
				$saved    = $settings->getAll();
				
				$filters = array(
					'categories'       => $saved['categories'] ?? array(),
					'tags'             => $saved['tags'] ?? array(),
					'stock_filter'     => $saved['stock_filter'] ?? 'all',
					'date_filter_days' => $saved['date_filter_days'] ?? 0,
					'max_products'     => $saved['max_products'] ?? 10000,
					'filter_mode'      => $saved['filter_mode'] ?? 'all',
				);
			} else {

				$filters = $request_filters;
			}

			$cache_key = 'aipt_index_estimate_' . md5( wp_json_encode( $filters ) . ( $use_saved_settings ? '_saved' : '' ) );
			$cached    = wp_cache_get( $cache_key, 'aipt_store_index' );
			if ( $cached !== false ) {
				return new WP_REST_Response(
					array(
						'success' => true,
						'data'    => array( 'count' => (int) $cached ),
					),
					200
				);
			}

			global $wpdb;

			$categories       = isset( $filters['categories'] ) && is_array( $filters['categories'] )
				? array_map( 'absint', $filters['categories'] )
				: array();
			$tags             = isset( $filters['tags'] ) && is_array( $filters['tags'] )
				? array_map( 'absint', $filters['tags'] )
				: array();
			$stock_filter     = isset( $filters['stock_filter'] ) && in_array( $filters['stock_filter'], array( 'all', 'instock', 'outofstock' ), true )
				? $filters['stock_filter']
				: 'all';
			$date_filter_days = isset( $filters['date_filter_days'] )
				? absint( $filters['date_filter_days'] )
				: 0;
			$max_products     = isset( $filters['max_products'] )
				? absint( $filters['max_products'] )
				: 10000;
			$filter_mode      = isset( $filters['filter_mode'] ) && in_array( $filters['filter_mode'], array( 'all', 'any' ), true )
				? $filters['filter_mode']
				: 'all';

			$where = array( "p.post_type = 'product'", "p.post_status = 'publish'" );

			$taxonomy_conditions = array();

			if ( ! empty( $categories ) ) {
				$cat_placeholders      = implode( ',', array_fill( 0, count( $categories ), '%d' ) );
				$taxonomy_conditions[] = $wpdb->prepare(
					"p.ID IN (
                        SELECT tr.object_id FROM {$wpdb->term_relationships} tr
                        INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
                        WHERE tt.taxonomy = 'product_cat' AND tt.term_id IN ({$cat_placeholders})
                    )",
					...$categories
				);
			}

			if ( ! empty( $tags ) ) {
				$tag_placeholders      = implode( ',', array_fill( 0, count( $tags ), '%d' ) );
				$taxonomy_conditions[] = $wpdb->prepare(
					"p.ID IN (
                        SELECT tr.object_id FROM {$wpdb->term_relationships} tr
                        INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
                        WHERE tt.taxonomy = 'product_tag' AND tt.term_id IN ({$tag_placeholders})
                    )",
					...$tags
				);
			}

			if ( ! empty( $taxonomy_conditions ) ) {
				$operator = ( 'any' === $filter_mode ) ? ' OR ' : ' AND ';
				$where[]  = '(' . implode( $operator, $taxonomy_conditions ) . ')';
			}

			if ( 'all' !== $stock_filter ) {
				$where[] = $wpdb->prepare(
					"p.ID IN (
                        SELECT pm.post_id FROM {$wpdb->postmeta} pm
                        WHERE pm.meta_key = '_stock_status' AND pm.meta_value = %s
                    )",
					$stock_filter
				);
			}

			if ( $date_filter_days > 0 ) {

				$date_cutoff = date( 'Y-m-d H:i:s', strtotime( "-{$date_filter_days} days", current_time( 'timestamp' ) ) );
				$where[]     = $wpdb->prepare( 'p.post_date >= %s', $date_cutoff );
			}

			$where_clause = implode( ' AND ', $where );

			$count_func = ( 'any' === $filter_mode ) ? 'COUNT(DISTINCT p.ID)' : 'COUNT(*)';

			$count = (int) $wpdb->get_var( "SELECT {$count_func} FROM {$wpdb->posts} p WHERE {$where_clause}" );

			$effective_count = min( $count, $max_products );

			wp_cache_set( $cache_key, $effective_count, 'aipt_store_index', 300 );

			return new WP_REST_Response(
				array(
					'success' => true,
					'data'    => array(
						'count'       => $effective_count,
						'total_match' => $count,
						'capped'      => $count > $max_products,
					),
				),
				200
			);
		} catch ( \Exception $e ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => __( 'Failed to estimate products.', 'ai-product-tools' ),
				),
				500
			);
		}
	}

	/**
	 * Get settings endpoint arguments.
	 *
	 * @return array
	 */
	private function get_settings_args(): array {
		return array(
			'enabled'          => array(
				'type'              => 'boolean',
				'sanitize_callback' => 'rest_sanitize_boolean',
			),
			'max_products'     => array(
				'type'              => 'integer',
				'minimum'           => 1,
				'maximum'           => 50000,
				'sanitize_callback' => 'absint',
			),
			'categories'       => array(
				'type'  => 'array',
				'items' => array( 'type' => 'integer' ),
			),
			'stock_filter'     => array(
				'type' => 'string',
				'enum' => array( 'all', 'instock', 'outofstock' ),
			),
			'date_filter_days' => array(
				'type'              => 'integer',
				'minimum'           => 0,
				'maximum'           => 365,
				'sanitize_callback' => 'absint',
			),
			'tags'             => array(
				'type'  => 'array',
				'items' => array( 'type' => 'integer' ),
			),
			'schedule'         => array(
				'type' => 'string',
				'enum' => array( 'daily', 'twice_daily', 'weekly' ),
			),
			'batch_size'       => array(
				'type'              => 'integer',
				'minimum'           => 50,
				'maximum'           => 500,
				'sanitize_callback' => 'absint',
			),
			'included_fields'  => array(
				'type'        => 'array',
				'items'       => array(
					'type' => 'string',
					'enum' => ProfileManager::AVAILABLE_FIELDS,
				),
				'description' => __( 'Fields to include in the index.', 'ai-product-tools' ),
			),
		);
	}

	/**
	 * List all profiles.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function list_profiles( WP_REST_Request $request ): WP_REST_Response {
		return new WP_REST_Response(
			array(
				'success'  => true,
				'profiles' => $this->profile_manager->getAll(),
			),
			200
		);
	}

	/**
	 * Create a new profile.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function create_profile( WP_REST_Request $request ): WP_REST_Response {
		$name = $request->get_param( 'name' );

		try {
			$profile_id = $this->profile_manager->create( $name );
			$profile    = $this->profile_manager->getById( $profile_id );

			return new WP_REST_Response(
				array(
					'success' => true,
					'message' => __( 'Profile created successfully.', 'ai-product-tools' ),
					'profile' => $profile,
				),
				201
			);
		} catch ( \InvalidArgumentException $e ) {
			return new WP_REST_Response(
				array(
					'code'    => 'invalid_profile_name',
					'message' => $e->getMessage(),
					'data'    => array( 'status' => 400 ),
				),
				400
			);
		} catch ( \RuntimeException $e ) {

			return ErrorCode::toRestResponse( $e, ErrorCode::AIPT_INDEX_GENERIC, 500 );
		}
	}

	/**
	 * Update a profile.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function update_profile( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = (int) $request->get_param( 'id' );
		$params     = $request->get_params();

		$data = array();
		if ( isset( $params['name'] ) ) {
			$data['name'] = $params['name'];
		}

		if ( empty( $data ) ) {
			return new WP_REST_Response(
				array(
					'code'    => 'nothing_to_update',
					'message' => __( 'No update data provided.', 'ai-product-tools' ),
					'data'    => array( 'status' => 400 ),
				),
				400
			);
		}

		try {
			$this->profile_manager->update( $profile_id, $data );
			$profile = $this->profile_manager->getById( $profile_id );

			return new WP_REST_Response(
				array(
					'success' => true,
					'message' => __( 'Profile updated successfully.', 'ai-product-tools' ),
					'profile' => $profile,
				),
				200
			);
		} catch ( \InvalidArgumentException $e ) {
			return new WP_REST_Response(
				array(
					'code'    => 'invalid_data',
					'message' => $e->getMessage(),
					'data'    => array( 'status' => 400 ),
				),
				400
			);
		} catch ( \RuntimeException $e ) {

			return ErrorCode::toRestResponse( $e, ErrorCode::AIPT_INDEX_GENERIC, 500 );
		}
	}

	/**
	 * Delete a profile.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function delete_profile( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = (int) $request->get_param( 'id' );

		try {
			$this->profile_manager->delete( $profile_id );

			return new WP_REST_Response(
				array(
					'success' => true,
					'message' => __( 'Profile deleted successfully.', 'ai-product-tools' ),
				),
				200
			);
		} catch ( \InvalidArgumentException $e ) {
			return new WP_REST_Response(
				array(
					'code'    => 'cannot_delete',
					'message' => $e->getMessage(),
					'data'    => array( 'status' => 400 ),
				),
				400
			);
		} catch ( \RuntimeException $e ) {

			return ErrorCode::toRestResponse( $e, ErrorCode::AIPT_INDEX_GENERIC, 500 );
		}
	}

	/**
	 * Activate a profile.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function activate_profile( WP_REST_Request $request ): WP_REST_Response {
		$profile_id = (int) $request->get_param( 'id' );

		try {
			$this->profile_manager->setActive( $profile_id );
			$profile = $this->profile_manager->getById( $profile_id );

			return new WP_REST_Response(
				array(
					'success' => true,
					'message' => __( 'Profile activated successfully.', 'ai-product-tools' ),
					'profile' => $profile,
				),
				200
			);
		} catch ( \InvalidArgumentException $e ) {
			return new WP_REST_Response(
				array(
					'code'    => 'invalid_profile',
					'message' => $e->getMessage(),
					'data'    => array( 'status' => 400 ),
				),
				400
			);
		}
	}

	/**
	 * Get global building status across all profiles.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_global_building_status( WP_REST_Request $request ): WP_REST_Response {
		return new WP_REST_Response(
			array(
				'success' => true,
				'data'    => $this->profile_manager->getGlobalBuildingStatus(),
			),
			200
		);
	}

	/**
	 * Get available fields for field selection.
	 *
	 * Returns static field definitions for the field selection UI.
	 * Data is preloaded via wp_localize_script for initial page load.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_available_fields( WP_REST_Request $request ): WP_REST_Response {

		$mandatory_fields = ProfileManager::getMandatoryFields();

		$fields = array(
			array(
				'key'         => 'sku',
				'label'       => __( 'SKU', 'ai-product-tools' ),
				'description' => __( 'Product stock keeping unit.', 'ai-product-tools' ),
				'group'       => 'basic',
				'mandatory'   => in_array( 'sku', $mandatory_fields, true ),
			),
			array(
				'key'         => 'title',
				'label'       => __( 'Title', 'ai-product-tools' ),
				'description' => __( 'Product name/title. Required for product identification.', 'ai-product-tools' ),
				'group'       => 'basic',
				'mandatory'   => in_array( 'title', $mandatory_fields, true ),
			),
			array(
				'key'         => 'short_description',
				'label'       => __( 'Short Description', 'ai-product-tools' ),
				'description' => __( 'Product excerpt/short description. Recommended for better AI context.', 'ai-product-tools' ),
				'group'       => 'content',
				'mandatory'   => in_array( 'short_description', $mandatory_fields, true ),
			),
			array(
				'key'         => 'description',
				'label'       => __( 'Full Description', 'ai-product-tools' ),
				'description' => __( 'Complete product description.', 'ai-product-tools' ),
				'group'       => 'content',
				'mandatory'   => in_array( 'description', $mandatory_fields, true ),
			),
			array(
				'key'         => 'price',
				'label'       => __( 'Price', 'ai-product-tools' ),
				'description' => __( 'Regular and sale prices. Required for product recommendations.', 'ai-product-tools' ),
				'group'       => 'basic',
				'mandatory'   => in_array( 'price', $mandatory_fields, true ),
			),
			array(
				'key'         => 'stock_status',
				'label'       => __( 'Stock Status', 'ai-product-tools' ),
				'description' => __( 'In stock, out of stock, on backorder.', 'ai-product-tools' ),
				'group'       => 'inventory',
				'mandatory'   => in_array( 'stock_status', $mandatory_fields, true ),
			),
			array(
				'key'         => 'stock_quantity',
				'label'       => __( 'Stock Quantity', 'ai-product-tools' ),
				'description' => __( 'Number of items in stock.', 'ai-product-tools' ),
				'group'       => 'inventory',
				'mandatory'   => in_array( 'stock_quantity', $mandatory_fields, true ),
			),
			array(
				'key'         => 'categories',
				'label'       => __( 'Categories', 'ai-product-tools' ),
				'description' => __( 'Product category assignments. Required for filtering.', 'ai-product-tools' ),
				'group'       => 'taxonomy',
				'mandatory'   => in_array( 'categories', $mandatory_fields, true ),
			),
			array(
				'key'         => 'tags',
				'label'       => __( 'Tags', 'ai-product-tools' ),
				'description' => __( 'Product tag assignments.', 'ai-product-tools' ),
				'group'       => 'taxonomy',
				'mandatory'   => in_array( 'tags', $mandatory_fields, true ),
			),
			array(
				'key'         => 'attributes',
				'label'       => __( 'Attributes', 'ai-product-tools' ),
				'description' => __( 'Product attributes (color, size, etc.).', 'ai-product-tools' ),
				'group'       => 'taxonomy',
				'mandatory'   => in_array( 'attributes', $mandatory_fields, true ),
			),
		);

		$groups = array(
			array(
				'key'   => 'basic',
				'label' => __( 'Basic Info', 'ai-product-tools' ),
			),
			array(
				'key'   => 'content',
				'label' => __( 'Content', 'ai-product-tools' ),
			),
			array(
				'key'   => 'inventory',
				'label' => __( 'Inventory', 'ai-product-tools' ),
			),
			array(
				'key'   => 'taxonomy',
				'label' => __( 'Taxonomy', 'ai-product-tools' ),
			),
		);

		return new WP_REST_Response(
			array(
				'success'          => true,
				'fields'           => $fields,
				'groups'           => $groups,
				'default_fields'   => ProfileManager::getDefaultIncludedFields(),
				'mandatory_fields' => $mandatory_fields,
			),
			200
		);
	}

	/**
	 * Get global settings.
	 *
	 * Global settings are shared across all profiles and stored in the default profile.
	 * Data is preloaded via wp_localize_script for initial page load.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_global_settings( WP_REST_Request $request ): WP_REST_Response {
		$profile_manager = new ProfileManager();
		$global_settings = $profile_manager->getGlobalSettings();

		return new WP_REST_Response(
			array(
				'success'  => true,
				'settings' => $global_settings,
			),
			200
		);
	}

	/**
	 * Update global settings.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_global_settings( WP_REST_Request $request ): WP_REST_Response {
		$settings = $request->get_json_params();

		if ( empty( $settings ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'message' => __( 'No settings provided.', 'ai-product-tools' ),
				),
				400
			);
		}

		$profile_manager = new ProfileManager();

		try {
			$result = $profile_manager->updateGlobalSettings( $settings );

			if ( ! $result ) {

				return ErrorCode::toRestResponse(
					new \RuntimeException( 'ProfileManager::updateGlobalSettings() returned false' ),
					ErrorCode::AIPT_INDEX_SETTINGS_PERSIST_FAILED,
					500
				);
			}

			$updated_settings = $profile_manager->getGlobalSettings();

			return new WP_REST_Response(
				array(
					'success'  => true,
					'message'  => __( 'Global settings updated successfully.', 'ai-product-tools' ),
					'settings' => $updated_settings,
				),
				200
			);
		} catch ( \Exception $e ) {

			return ErrorCode::toRestResponse( $e, ErrorCode::AIPT_INDEX_GENERIC, 500 );
		}
	}
}
