<?php

namespace CelerSearch\Admin;

defined( 'ABSPATH' ) || exit;

use CelerSearch\DataTransfer\ServiceConfig;
use CelerSearch\DataTransfer\StopWordsPresets;
use CelerSearch\Factories\IndexFactory;
use CelerSearch\Factories\ProviderFactory;
use CelerSearch\Factories\SearchAreaFactory;
use CelerSearch\Factories\ServiceFactory;
use CelerSearch\Interfaces\IRegistrable;
use CelerSearch\Repositories\IndexRepository;
use CelerSearch\Repositories\ServiceRepository;
use CelerSearch\Repositories\ViewRepository;
use CelerSearch\Manager;
use CelerSearch\Utilities\DateTimeUtilities;
use CelerSearch\Utilities\EnvUtilities;
use CelerSearch\Utilities\Logger;
use CelerSearch\Vendor\IgniteKit\WP\Validation\Validator;

class Ajax implements IRegistrable {

	protected $manager;

	public function __construct( Manager $manager ) {
		$this->manager = $manager;
	}

	/**
	 * Registers certain group of hooks (actions/filters)
	 * @return void
	 */
	public function register() : void {

		add_action( 'wp_ajax_celersearch_indices_query', [ $this, 'indices_query' ] );
		add_action( 'wp_ajax_celersearch_indices_find', [ $this, 'indices_find' ] );
		add_action( 'wp_ajax_celersearch_indices_store', [ $this, 'indices_store' ] );
		add_action( 'wp_ajax_celersearch_indices_remove', [ $this, 'indices_remove' ] );

		// Frontend compatible actions
		add_action( 'wp_ajax_celersearch_get_indices', [ $this, 'indices_query' ] );
		add_action( 'wp_ajax_celersearch_delete_index', [ $this, 'indices_remove' ] );
		add_action( 'wp_ajax_celersearch_get_index_types', [ $this, 'indices_get_types' ] );

		add_action( 'wp_ajax_celersearch_services_check_status', [ $this, 'services_check_status' ] );
		add_action( 'wp_ajax_celersearch_services_query', [ $this, 'services_query' ] );
		add_action( 'wp_ajax_celersearch_services_find', [ $this, 'services_find' ] );
		add_action( 'wp_ajax_celersearch_services_store', [ $this, 'services_store' ] );
		add_action( 'wp_ajax_celersearch_services_remove', [ $this, 'services_remove' ] );
		add_action( 'wp_ajax_celersearch_services_get_providers', [ $this, 'services_get_providers' ] );

		add_action( 'wp_ajax_celersearch_settings_get', [ $this, 'settings_get' ] );
		add_action( 'wp_ajax_celersearch_settings_save', [ $this, 'settings_save' ] );
		add_action( 'wp_ajax_celersearch_settings_get_post_types', [ $this, 'settings_get_post_types' ] );
		add_action( 'wp_ajax_celersearch_settings_get_area_types', [ $this, 'settings_get_area_types' ] );

		add_action( 'wp_ajax_celersearch_indices_check_stats', [ $this, 'indices_check_stats' ] );
		add_action( 'wp_ajax_celersearch_indices_rebuild', [ $this, 'indices_rebuild' ] );
		add_action( 'wp_ajax_celersearch_indices_get_settings', [ $this, 'indices_get_settings' ] );
		add_action( 'wp_ajax_celersearch_indices_update_settings', [ $this, 'indices_update_settings' ] );
		add_action( 'wp_ajax_celersearch_get_stop_words_presets', [ $this, 'get_stop_words_presets' ] );

		add_action( 'wp_ajax_celersearch_get_queue_actions', [ $this, 'get_queue_actions' ] );

		add_action( 'wp_ajax_celersearch_views_query', [ $this, 'views_query' ] );
		add_action( 'wp_ajax_celersearch_views_find', [ $this, 'views_find' ] );
		add_action( 'wp_ajax_celersearch_views_store', [ $this, 'views_store' ] );
		add_action( 'wp_ajax_celersearch_views_remove', [ $this, 'views_remove' ] );

		add_action( 'wp_ajax_celersearch_index_filterable_attributes', [ $this, 'index_filterable_attributes' ] );

	}

	/**
	 * Returns single service form DB
	 * @return void
	 */
	public function indices_find(): void {
		$this->check_access( 'manage_options' );
		$repo      = new IndexRepository();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$record_id = isset( $_GET['id'] ) ? (int) $_GET['id'] : null;
		$record    = $record_id ? $repo->find( $record_id ) : null;
		wp_send_json_success( [ 'record' => $record->toArray() ] );
	}

	/**
	 * Returns list of indices
	 *
	 * @return void
	 * @throws \Exception
	 */
	public function indices_query(): void {
		$this->check_access( 'manage_options' );
		$repo     = new IndexRepository();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$page     = isset( $_GET['page'] ) ? (int) $_GET['page'] : 1;
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$per_page = isset( $_GET['per_page'] ) ? (int) $_GET['per_page'] : 10;
		$counter  = $repo->count( [] );
		$records  = $repo->get( [], $page, $per_page );

		// Fetch services for each index
		$service_repo = new ServiceRepository();

		foreach ( $records as $key => $record ) {
			$formatted = $record->getFormatted();

			// Add service information
			if ( $record->getServiceId() ) {
				$service = $service_repo->find( $record->getServiceId() );
				if ( $service ) {
					$formatted->service_name = $service->getName();
					$formatted->service_status = $service->getStatus();
				} else {
					$formatted->service_name = __( 'Unknown', 'celersearch' );
					$formatted->service_status = null;
				}
			} else {
				$formatted->service_name = __( 'Not configured', 'celersearch' );
				$formatted->service_status = null;
			}

			// Determine index status based on service status
			if ( $formatted->service_status === ServiceRepository::STATUS_CONNECTED ) {
				$formatted->status = 'active';
			} else if ( $formatted->service_status === ServiceRepository::STATUS_FAILING ) {
				$formatted->status = 'failed';
			} else {
				$formatted->status = 'pending';
			}

			$records[ $key ] = $formatted;
		}

		wp_send_json_success( [ 'records' => $records, 'pagination' => [ 'current_page' => $page, 'total_pages' => $counter > 0 ? $counter / $per_page : 1 ] ] );
	}

	/**
	 * Returns available index types
	 * @return void
	 */
	public function indices_get_types(): void {
		$this->check_access( 'manage_options' );

		$supported = IndexFactory::get_supported_indices();
		$supported = array_filter( $supported, function( $index ) {
			if ( isset( $index['requires'] ) && is_callable( $index['requires'] ) ) {
				return call_user_func( $index['requires'] );
			}
			return true;
		} );
		$result = [];

		foreach ( $supported as $index ) {
			$fields = [];
			foreach ( $index['fields'] as $field ) {
				$field_data = [
					'name' => $field['name'],
					'label' => $field['label'],
					'type' => $field['type'],
					'rules' => isset( $field['rules'] ) ? $field['rules'] : '',
					'multiple' => isset( $field['multiple'] ) ? $field['multiple'] : false,
					'options' => [], // Default empty array
					'quick_select' => isset( $field['quick_select'] ) ? $field['quick_select'] : null,
				];

				// Handle options - if it's a callable, execute it
				if ( isset( $field['options'] ) ) {
					if ( is_callable( $field['options'] ) ) {
						$field_data['options'] = call_user_func( $field['options'] );
					} else {
						$field_data['options'] = $field['options'];
					}
				}

				$fields[] = $field_data;
			}

			$result[] = [
				'name' => $index['name'],
				'type' => $index['type'],
				'fields' => $fields,
			];
		}

		wp_send_json_success( [ 'records' => $result ] );
	}

	/**
	 * Store or update index
	 * @return void
	 */
	public function indices_store() {

		// Access control
		$this->check_access( 'manage_options' );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		// Data Retrieval
		$index_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
		$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
		$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '';
		$service_id = isset( $_POST['service_id'] ) ? (int) $_POST['service_id'] : null;
		$config = isset( $_POST['config'] ) && is_array( $_POST['config'] ) ? map_deep( wp_unslash( $_POST['config'] ), 'sanitize_text_field' ) : [];

		// Validations
		$validations = [
			'name' => 'required|min:2|max:100',
			'type' => 'required',
			'service_id' => 'required|numeric',
		];

		$validator = new Validator();
		$validation = $validator->make( [
			'name'       => $name,
			'type'       => $type,
			'service_id' => $service_id,
		], $validations );
		// phpcs:enable WordPress.Security.NonceVerification.Missing
		$validation->validate();

		if ( $validation->fails() ) {
			wp_send_json_error( [ 'message' => $validation->errors()->firstOfAll() ] );
		}

		try {
			$repo = new IndexRepository();

			// Generate slug from name
			$slug = sanitize_title( $name );

			// Check if slug exists and append number if needed
			$original_slug = $slug;
			$counter = 1;
			while ( true ) {
				global $wpdb;
				$table = $wpdb->prefix . 'celersearch_indices';
				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table
				$existing = $wpdb->get_var(
					$wpdb->prepare(
						"SELECT COUNT(*) FROM $table WHERE slug = %s AND id != %d",
						$slug,
						$index_id
					)
				);
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

				if ( $existing == 0 ) {
					break;
				}

				$slug = $original_slug . '-' . $counter;
				$counter++;
			}

			$data = [
				'name' => $name,
				'type' => $type,
				'slug' => $slug,
				'service_id' => $service_id,
				'config' => $config,
			];

			if ( $index_id > 0 ) {
				$result = $repo->update( $index_id, $data );
				$message = __( 'Index updated successfully.', 'celersearch' );
				// $wpdb->update returns false on error, 0 if no rows updated (no changes), or number of rows updated
				// We treat 0 as success since it means the record exists but nothing changed
				if ( $result === false ) {
					wp_send_json_error( [ 'message' => __( 'Unable to save index.', 'celersearch' ) ] );
				}
				wp_send_json_success( [
					'message' => $message,
					'record' => $repo->find( $index_id )->toArray()
				] );
			} else {
				$result = $repo->create( $data );
				$message = __( 'Index created successfully.', 'celersearch' );
				if ( $result ) {
					wp_send_json_success( [
						'message' => $message,
						'record' => $result->toArray()
					] );
				} else {
					wp_send_json_error( [ 'message' => __( 'Unable to save index.', 'celersearch' ) ] );
				}
			}

		} catch ( \Exception $e ) {
			$message = __( 'Failed to save index.', 'celersearch' );

			// Log error details for debugging
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Failed to save index', [
				'message' => $e->getMessage(),
				'trace'   => $e->getTraceAsString(),
			] );

			if ( EnvUtilities::is_debug() ) {
				wp_send_json_error( [ 'message' => $e->getMessage() ] );
			} else {
				wp_send_json_error( [ 'message' => $message ] );
			}
		}

	}

	/**
	 * Removes service from the database
	 * @return void
	 */
	public function indices_remove(): void {

		// Access control
		$this->check_access( 'manage_options' );

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$record_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;

		if ( ! $record_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid index id.', 'celersearch' ) ] );
		}

		// Check if index is used in settings
		$settings = get_option( 'celersearch_settings', [] );

		// Check if used as default index
		if ( isset( $settings['default_index_id'] ) && (int) $settings['default_index_id'] === $record_id ) {
			wp_send_json_error( [ 'message' => __( 'Cannot delete index. It is set as the default index in settings.', 'celersearch' ) ] );
		}

		// Check if used in any search area
		if ( ! empty( $settings['search_areas'] ) && is_array( $settings['search_areas'] ) ) {
			foreach ( $settings['search_areas'] as $area ) {
				if ( isset( $area['index_id'] ) && (int) $area['index_id'] === $record_id ) {
					$area_label = isset( $area['label'] ) ? sanitize_text_field( $area['label'] ) : __( 'Unknown', 'celersearch' );
					/* translators: %s: search area label */
					wp_send_json_error( [ 'message' => sprintf( __( 'Cannot delete index. It is used by search area "%s".', 'celersearch' ), $area_label ) ] );
				}
			}
		}

		// Attempt to delete the remote index (best-effort)
		try {
			$index = IndexFactory::create( $record_id );
			$index->delete();
		} catch ( \Exception $e ) {
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Failed to delete remote index', [
				'index_id' => $record_id,
				'message'  => $e->getMessage(),
			] );
		}

		$repo = new IndexRepository();
		if ( $repo->delete( $record_id ) ) {
			wp_send_json_success( [ 'message' => __( 'Index removed successfully.', 'celersearch' ) ] );
		} else {
			wp_send_json_error( [ 'message' => __( 'Unable to remove index.', 'celersearch' ) ] );
		}
	}


	/**
	 * Returns single service form DB
	 * @return void
	 */
	public function services_find(): void {
		$this->check_access( 'manage_options' );
		$repo      = new ServiceRepository();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$record_id = isset( $_GET['id'] ) ? (int) $_GET['id'] : null;
		$record    = $record_id ? $repo->find( $record_id ) : null;
		wp_send_json_success( [ 'record' => $record->toArray() ] );
	}

	/**
	 * Returns list of services
	 *
	 * @return void
	 * @throws \Exception
	 */
	public function services_query(): void {
		$this->check_access( 'manage_options' );
		$repo     = new ServiceRepository();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$page     = isset( $_GET['page'] ) ? (int) $_GET['page'] : 1;
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$per_page = isset( $_GET['per_page'] ) ? (int) $_GET['per_page'] : 10;
		$counter  = $repo->count( [] );
		$records  = $repo->get( [], $page, $per_page );
		foreach ( $records as $key => $record ) {
			$records[ $key ] = $record->getFormatted();
		}
		wp_send_json_success( [ 'records' => $records, 'pagination' => [ 'current_page' => $page, 'total_pages' => $counter > 0 ? $counter / $per_page : 1 ] ] );
	}

	/**
	 * On-demand health check for all services
	 *
	 * @return void
	 */
	public function services_check_status(): void {
		$this->check_access( 'manage_options' );

		$repo    = new ServiceRepository();
		$results = [];

		try {
			$services = $repo->get();

			foreach ( $services as $service ) {
				try {
					$instance  = ServiceFactory::create( $service->getId() );
					$is_healthy = $instance->health_check();
				} catch ( \Exception $e ) {
					$is_healthy = false;
				}

				if ( $is_healthy ) {
					$repo->update( $service->getId(), [
						'status'        => ServiceRepository::STATUS_CONNECTED,
						'failure_count' => 0,
						'checked_at'    => gmdate( 'Y-m-d H:i:s' ),
					] );
					$results[] = [
						'id'         => $service->getId(),
						'status'     => ServiceRepository::STATUS_CONNECTED,
						'checked_at' => gmdate( 'Y-m-d H:i:s' ),
					];
				} else {
					$failure_count = $service->getFailureCount() + 1;
					$new_status    = $failure_count >= ServiceRepository::FAILURE_THRESHOLD
						? ServiceRepository::STATUS_FAILING
						: $service->getStatus();

					$repo->update( $service->getId(), [
						'status'        => $new_status,
						'failure_count' => $failure_count,
						'checked_at'    => gmdate( 'Y-m-d H:i:s' ),
					] );
					$results[] = [
						'id'         => $service->getId(),
						'status'     => $new_status,
						'checked_at' => gmdate( 'Y-m-d H:i:s' ),
					];
				}
			}
		} catch ( \Exception $e ) {
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Service status check failed', [
				'exception' => $e->getMessage(),
			] );
			wp_send_json_error( [ 'message' => __( 'Failed to check service status.', 'celersearch' ) ] );
		}

		wp_send_json_success( [ 'results' => $results ] );
	}

	/**
	 * Test and store service
	 * @return void
	 */
	public function services_store() {

		// Access control
		$this->check_access( 'manage_options' );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		// Data Retrieval
		$name            = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
		$provider_slug   = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : '';
		$provider_conf   = ProviderFactory::get_provider( $provider_slug );
		$provider_fields = isset( $provider_conf['fields'] ) ? $provider_conf['fields'] : array();
		$input_config    = isset( $_POST['config'] ) && is_array( $_POST['config'] ) ? map_deep( wp_unslash( $_POST['config'] ), 'sanitize_text_field' ) : [];

		// Validations
		$validations = [ 'name' => 'required|min:2|max:100' ];
		if ( ! empty( $provider_fields ) ) {
			foreach ( $provider_fields as $provider_rule ) {
				if ( empty( $provider_rule['name'] ) || empty( $provider_rule['rules'] ) ) {
					continue;
				}
				$validations[ $provider_rule['name'] ] = $provider_rule['rules'];
			}
		}
		$validator       = new Validator();
		$sanitized_input = array_merge( [ 'name' => $name ], (array) $input_config );
		$validation      = $validator->make( $sanitized_input, $validations );
		// phpcs:enable WordPress.Security.NonceVerification.Missing
		$validation->validate();
		if ( $validation->fails() ) {
			wp_send_json_error( [ 'message' => $validation->errors()->firstOfAll() ] );
		}

		try {

			$data = [
				'name'     => $name,
				'provider' => $provider_slug,
				'config'   => $input_config,
			];

			// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()

			$server_config = new ServiceConfig( (object) $data );
			$index         = IndexFactory::create_for_testing( $server_config );
			$index->delete();
			$index->rebuild_index( 1 );

			if ( ! $index->check()->is_error_response() ) {

				$test = isset( $_POST['test_only'] ) && (bool) $_POST['test_only'];

				if ( ! $test ) {

					$repo       = new ServiceRepository();
					$service_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
					// phpcs:enable WordPress.Security.NonceVerification.Missing
					$data       = array_merge( $data, [ 'status' => ServiceRepository::STATUS_CONNECTED, 'checked_at' => gmdate( 'Y-m-d H:i:s' ), 'failure_count' => 0 ] );
					if ( $service_id > 0 ) {
						$result = $repo->update( $service_id, $data );
					} else {
						$result = $repo->create( $data );
					}

					if ( $result ) {
						wp_send_json_success( [
							'record' => $result->toArray(),
							'message' => $service_id > 0 ? __( 'Record updated successfully.', 'celersearch' ) : __( 'Record created successfully.', 'celersearch' )
						] );
					} else {
						wp_send_json_error( [ 'message' => __( 'Unable to save record.', 'celersearch' ) ] );
					}
				}

				wp_send_json_success( [ 'message' => __( 'Connection succeeded. You can save now!', 'celersearch' ) ] );

			} else {
				wp_send_json_error( [ 'message' => __( 'Connection failed. Please check your configuration. (1000)', 'celersearch' ) ] );
			}

		} catch ( \Exception $e ) {

			$message = __( 'Connection failed. Please check your configuration. (1001)', 'celersearch' );

			// Log error details for debugging
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Service connection failed', [
				'message' => $e->getMessage(),
				'trace'   => $e->getTraceAsString(),
			] );

			if ( EnvUtilities::is_debug() ) {
				wp_send_json_error( [ 'message' => empty( $e->getMessage() ) ? $message : $e->getMessage() ] );
			} else {
				wp_send_json_error( [ 'message' => $message ] );
			}
		}

		exit;

	}

	/**
	 * Removes service from the database
	 * @return void
	 */
	public function services_remove(): void {

		// Access control
		$this->check_access( 'manage_options' );

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$service_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;

		if ( ! $service_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid service id.', 'celersearch' ) ] );
		}

		$repo = new ServiceRepository();
		if ( $repo->delete( $service_id ) ) {
			wp_send_json_success( [ 'message' => __( 'Service removed successfully.', 'celersearch' ) ] );
		} else {
			wp_send_json_error( [ 'message' => __( 'Unable to remove service.', 'celersearch' ) ] );
		}
	}

	/**
	 * Returns the available providers
	 * @return void
	 */
	public function services_get_providers(): void {

		$this->check_access( 'manage_options' );

		wp_send_json_success( [
			'records' => ProviderFactory::get_supported_providers(),
		] );
	}


	/**
	 * Get settings
	 *
	 * @return void
	 */
	public function settings_get(): void {
		$this->check_access( 'manage_options' );

		$defaults = [
			'enable_search'      => false,
			'default_index_id'   => 0,
			'search_areas'       => [],
			'fallback_to_native' => true,
			'batch_size'         => 100,
		];

		$settings = get_option( 'celersearch_settings', [] );
		$settings = wp_parse_args( $settings, $defaults );

		wp_send_json_success( [ 'settings' => $settings ] );
	}

	/**
	 * Save settings
	 *
	 * @return void
	 */
	public function settings_save(): void {
		$this->check_access( 'manage_options' );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$enable_search      = isset( $_POST['enable_search'] ) ? (bool) $_POST['enable_search'] : false;
		$default_index_id   = isset( $_POST['default_index_id'] ) ? (int) $_POST['default_index_id'] : 0;
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized in loop below
		$search_areas       = isset( $_POST['search_areas'] ) && is_array( $_POST['search_areas'] ) ? wp_unslash( $_POST['search_areas'] ) : [];
		$fallback_to_native = isset( $_POST['fallback_to_native'] ) ? (bool) $_POST['fallback_to_native'] : true;
		$batch_size         = isset( $_POST['batch_size'] ) ? (int) $_POST['batch_size'] : 100;
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		// Sanitize batch size (between 10 and 500)
		$batch_size = max( 10, min( 500, $batch_size ) );

		// Sanitize search areas
		$sanitized_areas = [];
		foreach ( $search_areas as $area ) {
			if ( ! is_array( $area ) ) {
				continue;
			}

			// Validate area type exists
			$type = isset( $area['type'] ) ? sanitize_text_field( $area['type'] ) : '';
			if ( empty( $type ) || ! SearchAreaFactory::type_exists( $type ) ) {
				continue;
			}

			// Sanitize features
			$features = [];
			if ( isset( $area['features'] ) && is_array( $area['features'] ) ) {
				$area_type_def = SearchAreaFactory::get_type( $type );
				$declared_features = $area_type_def ? ( $area_type_def['features'] ?? [] ) : [];
				$declared_map = [];
				foreach ( $declared_features as $df ) {
					$declared_map[ $df['key'] ] = $df;
				}

				foreach ( $area['features'] as $feature_key => $feature_value ) {
					$sanitized_key = sanitize_text_field( $feature_key );
					if ( ! isset( $declared_map[ $sanitized_key ] ) ) {
						continue;
					}

					if ( is_array( $feature_value ) ) {
						$feature_data = [ 'enabled' => ! empty( $feature_value['enabled'] ) ];
						foreach ( $declared_map[ $sanitized_key ]['options'] ?? [] as $opt ) {
							$opt_key = $opt['key'];
							if ( isset( $feature_value[ $opt_key ] ) ) {
								$val = sanitize_text_field( $feature_value[ $opt_key ] );
								$valid_values = array_column( $opt['choices'] ?? [], 'value' );
								$feature_data[ $opt_key ] = in_array( $val, $valid_values, true ) ? $val : $opt['default'];
							} else {
								$feature_data[ $opt_key ] = $opt['default'];
							}
						}
						$features[ $sanitized_key ] = $feature_data;
					} else {
						$feature_data = [ 'enabled' => (bool) $feature_value ];
						foreach ( $declared_map[ $sanitized_key ]['options'] ?? [] as $opt ) {
							$feature_data[ $opt['key'] ] = $opt['default'];
						}
						$features[ $sanitized_key ] = $feature_data;
					}
				}
			}

			$sanitized_area = [
				'id'           => isset( $area['id'] ) ? sanitize_title( $area['id'] ) : uniqid( 'area_' ),
				'type'         => $type,
				'label'        => isset( $area['label'] ) ? sanitize_text_field( $area['label'] ) : '',
				'index_id'     => isset( $area['index_id'] ) ? (int) $area['index_id'] : 0,
				'default_sort' => isset( $area['default_sort'] ) && in_array( $area['default_sort'], [ '', 'date', 'date-asc', 'title', 'title-desc' ], true )
					? sanitize_text_field( $area['default_sort'] )
					: '',
				'features'     => $features,
				'enabled'      => isset( $area['enabled'] ) ? (bool) $area['enabled'] : true,
			];

			if ( ! empty( $area['post_type'] ) ) {
				$sanitized_area['post_type'] = sanitize_text_field( $area['post_type'] );
			}

			$sanitized_areas[] = $sanitized_area;
		}

		$settings = [
			'enable_search'      => $enable_search,
			'default_index_id'   => $default_index_id,
			'search_areas'       => $sanitized_areas,
			'fallback_to_native' => $fallback_to_native,
			'batch_size'         => $batch_size,
		];

		update_option( 'celersearch_settings', $settings );

		wp_send_json_success( [
			'message'  => __( 'Settings saved successfully.', 'celersearch' ),
			'settings' => $settings
		] );
	}

	/**
	 * Get available post types
	 *
	 * @return void
	 */
	public function settings_get_post_types(): void {
		$this->check_access( 'manage_options' );

		$post_types = get_post_types( [ 'public' => true ], 'objects' );
		$result     = [];

		foreach ( $post_types as $post_type ) {
			$result[] = [
				'value' => $post_type->name,
				'label' => $post_type->labels->singular_name,
			];
		}

		wp_send_json_success( [ 'post_types' => $result ] );
	}

	/**
	 * Get available search area types
	 *
	 * @return void
	 */
	public function settings_get_area_types(): void {
		$this->check_access( 'manage_options' );

		$types = SearchAreaFactory::get_available_types();

		wp_send_json_success( [ 'area_types' => $types ] );
	}

	/**
	 * On-demand stats check for all indices (document counts)
	 *
	 * @return void
	 */
	public function indices_check_stats(): void {
		$this->check_access( 'manage_options' );

		$repo    = new IndexRepository();
		$records = $repo->get( [], 1, 100 );
		$results = [];

		foreach ( $records as $record ) {
			$result = [
				'id'           => $record->getId(),
				'total_remote' => null,
				'total_local'  => null,
				'checked_at'   => null,
			];

			try {
				$index = IndexFactory::create( $record->getId() );

				// Remote count (provider-agnostic via BaseService::count_items)
				$count_response = $index->count_items();
				if ( ! $count_response->is_error_response() ) {
					$result['total_remote'] = $count_response->get_value();
				}

				// Local count (WordPress-side)
				$result['total_local'] = $index->get_candidate_count();
				$result['checked_at']  = gmdate( 'Y-m-d H:i:s' );

				// Persist to DB
				$repo->update( $record->getId(), [
					'total_remote' => $result['total_remote'],
					'total_local'  => $result['total_local'],
					'checked_at'   => $result['checked_at'],
				] );

				// Format for display
				$result['checked_at'] = DateTimeUtilities::format( $result['checked_at'] );

			} catch ( \Exception $e ) {
				// Index can't be created (missing service, etc.) — skip
			}

			$results[] = $result;
		}

		wp_send_json_success( [ 'results' => $results ] );
	}

	/**
	 * Rebuild index in batches
	 *
	 * @return void
	 */
	public function indices_rebuild(): void {
		$this->check_access( 'manage_options' );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$index_id = isset( $_POST['index_id'] ) ? (int) $_POST['index_id'] : 0;
		$page     = isset( $_POST['page'] ) ? (int) $_POST['page'] : 1;
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		if ( ! $index_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid index ID.', 'celersearch' ) ] );
		}

		try {
			// Get batch size from settings
			$settings   = get_option( 'celersearch_settings', [] );
			$batch_size = isset( $settings['batch_size'] ) ? (int) $settings['batch_size'] : 100;

			// Set up filter to override batch size
			$batch_size_filter = function( $default_size ) use ( $batch_size ) {
				return $batch_size;
			};
			add_filter( 'celersearch_indexable_items_batch_size', $batch_size_filter );

			// Create index instance directly with index ID
			$index = IndexFactory::create( $index_id );

			// Get total items count (only on first page)
			if ( $page === 1 ) {
				$total_items = $index->get_candidate_count();
			} else {
				// For subsequent pages, we calculate from batch size
				// This will be overridden by frontend tracking
				$total_items = 0;
			}

			// Rebuild this batch (only accepts page parameter)
			$index->rebuild_index( $page );

			// Remove the filter after use
			remove_filter( 'celersearch_indexable_items_batch_size', $batch_size_filter );

			// Calculate progress
			$items_processed = $page * $batch_size;

			// Determine if we're done based on total items
			$is_complete = false;
			if ( $page === 1 && $total_items === 0 ) {
				// Nothing to index - we're done immediately
				$is_complete = true;
			} else if ( $page === 1 && $total_items > 0 ) {
				// We know the total, check if we've processed all items
				$is_complete = $items_processed >= $total_items;
			} else if ( $page > 1 && $total_items === 0 ) {
				// For pages after the first, estimate completion
				// This is a fallback - the frontend will track total_items from page 1
				$is_complete = false; // Let frontend handle this
			}

			wp_send_json_success( [
				'page'            => $page,
				'total_items'     => $total_items,
				'items_processed' => $items_processed,
				'batch_size'      => $batch_size,
				'is_complete'     => $is_complete,
				'message'         => $is_complete
					? __( 'Indexing completed successfully.', 'celersearch' )
					/* translators: %d: batch number */
					: sprintf( __( 'Processing batch %d...', 'celersearch' ), $page )
			] );

		} catch ( \Exception $e ) {
			$message = __( 'Failed to rebuild index.', 'celersearch' );

			// Log error details for debugging
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Failed to rebuild index', [
				'message' => $e->getMessage(),
				'trace'   => $e->getTraceAsString(),
			] );

			if ( EnvUtilities::is_debug() ) {
				wp_send_json_error( [ 'message' => $e->getMessage() ] );
			} else {
				wp_send_json_error( [ 'message' => $message ] );
			}
		}
	}

	/**
	 * Get index settings
	 *
	 * @return void
	 */
	public function indices_get_settings(): void {
		$this->check_access( 'manage_options' );

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$index_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;

		if ( ! $index_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid index ID.', 'celersearch' ) ] );
		}

		try {
			$index    = IndexFactory::create( $index_id );
			$settings = $index->get_settings();

			// Get service info for engine-specific settings visibility
			$repo         = new IndexRepository();
			$index_config = $repo->find( $index_id );
			$service_id   = $index_config ? $index_config->getServiceId() : 0;
			$service_repo = new ServiceRepository();
			$service      = $service_id ? $service_repo->find( $service_id ) : null;
			$provider     = $service ? $service->getProvider() : 'meilisearch';

			wp_send_json_success( [
				'settings' => [
					'typo_enabled'         => $settings->is_typo_enabled(),
					'typo_min_one'         => $settings->get_typo_min_one(),
					'typo_min_two'         => $settings->get_typo_min_two(),
					'typo_disable_words'   => $settings->get_typo_disable_words(),
					'typo_disable_fields'  => $settings->get_typo_disable_fields(),
					'stop_words'           => $settings->get_stop_words(),
					'max_hits'             => $settings->get_max_hits(),
					'facet_max_values'     => $settings->get_facet_max_values(),
					'ranking_rules'        => $settings->get_ranking_rules(),
					'proximity_precision'  => $settings->get_proximity_precision(),
					'token_separators'     => $settings->get_token_separators(),
					'non_token_separators' => $settings->get_non_token_separators(),
				],
				'provider' => $provider,
			] );

		} catch ( \Exception $e ) {
			if ( EnvUtilities::is_debug() ) {
				wp_send_json_error( [ 'message' => $e->getMessage() ] );
			} else {
				wp_send_json_error( [ 'message' => __( 'Failed to retrieve settings.', 'celersearch' ) ] );
			}
		}
	}

	/**
	 * Update index settings
	 *
	 * @return void
	 */
	public function indices_update_settings(): void {
		$this->check_access( 'manage_options' );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$index_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below
		$settings = isset( $_POST['settings'] ) && is_array( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : [];
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		if ( ! $index_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid index ID.', 'celersearch' ) ] );
		}

		// Sanitize settings
		$sanitized = [];

		if ( isset( $settings['typo_enabled'] ) ) {
			$sanitized['typo_enabled'] = (bool) $settings['typo_enabled'];
		}
		if ( isset( $settings['typo_min_one'] ) ) {
			$sanitized['typo_min_one'] = max( 1, (int) $settings['typo_min_one'] );
		}
		if ( isset( $settings['typo_min_two'] ) ) {
			$sanitized['typo_min_two'] = max( 1, (int) $settings['typo_min_two'] );
		}
		if ( isset( $settings['typo_disable_words'] ) && is_array( $settings['typo_disable_words'] ) ) {
			$sanitized['typo_disable_words'] = array_map( 'sanitize_text_field', $settings['typo_disable_words'] );
		}
		if ( isset( $settings['typo_disable_fields'] ) && is_array( $settings['typo_disable_fields'] ) ) {
			$sanitized['typo_disable_fields'] = array_map( 'sanitize_text_field', $settings['typo_disable_fields'] );
		}
		if ( isset( $settings['stop_words'] ) && is_array( $settings['stop_words'] ) ) {
			$sanitized['stop_words'] = array_map( 'sanitize_text_field', $settings['stop_words'] );
		}
		if ( isset( $settings['max_hits'] ) ) {
			$sanitized['max_hits'] = max( 1, (int) $settings['max_hits'] );
		}
		if ( isset( $settings['facet_max_values'] ) ) {
			$sanitized['facet_max_values'] = max( 1, (int) $settings['facet_max_values'] );
		}
		if ( isset( $settings['ranking_rules'] ) && is_array( $settings['ranking_rules'] ) ) {
			$sanitized['ranking_rules'] = array_map( 'sanitize_text_field', $settings['ranking_rules'] );
		}
		if ( isset( $settings['proximity_precision'] ) ) {
			$allowed = [ 'byWord', 'byAttribute' ];
			$value   = sanitize_text_field( $settings['proximity_precision'] );
			$sanitized['proximity_precision'] = in_array( $value, $allowed, true ) ? $value : 'byWord';
		}
		if ( isset( $settings['token_separators'] ) && is_array( $settings['token_separators'] ) ) {
			$sanitized['token_separators'] = array_map( 'sanitize_text_field', $settings['token_separators'] );
		}
		if ( isset( $settings['non_token_separators'] ) && is_array( $settings['non_token_separators'] ) ) {
			$sanitized['non_token_separators'] = array_map( 'sanitize_text_field', $settings['non_token_separators'] );
		}

		try {
			// Save to database
			$repo   = new IndexRepository();
			$result = $repo->update_settings( $index_id, $sanitized );

			if ( ! $result ) {
				wp_send_json_error( [ 'message' => __( 'Failed to save settings.', 'celersearch' ) ] );
			}

			// Push settings to search engine
			$index   = IndexFactory::create( $index_id );
			$service = $index->get_service();
			$service->push_settings( $index );

			wp_send_json_success( [ 'message' => __( 'Settings updated successfully.', 'celersearch' ) ] );

		} catch ( \Throwable $e ) {
			wp_send_json_error( [ 'message' => $e->getMessage() ] );
		}
	}

	/**
	 * Get stop words presets
	 *
	 * @return void
	 */
	public function get_stop_words_presets(): void {
		$this->check_access( 'manage_options' );

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$language = isset( $_POST['language'] ) ? sanitize_text_field( wp_unslash( $_POST['language'] ) ) : '';

		if ( ! empty( $language ) ) {
			wp_send_json_success( [
				'words' => StopWordsPresets::get( $language ),
			] );
		} else {
			wp_send_json_success( [
				'languages' => StopWordsPresets::available(),
			] );
		}
	}

	/**
	 * Get pending queue actions from Action Scheduler
	 *
	 * @return void
	 */
	public function get_queue_actions(): void {
		$this->check_access( 'manage_options' );

		if ( ! function_exists( 'as_get_scheduled_actions' ) ) {
			wp_send_json_success( [
				'actions'   => [],
				'count'     => 0,
				'available' => false,
			] );
			return;
		}

		// Query sync and delete actions separately (array of hooks not supported in all AS versions)
		$sync_actions = as_get_scheduled_actions( [
			'hook'     => 'celersearch_queue_sync_item',
			'status'   => 'pending',
			'group'    => 'celersearch',
			'per_page' => 50,
			'orderby'  => 'date',
			'order'    => 'ASC',
		] );

		$delete_actions = as_get_scheduled_actions( [
			'hook'     => 'celersearch_queue_delete_item',
			'status'   => 'pending',
			'group'    => 'celersearch',
			'per_page' => 50,
			'orderby'  => 'date',
			'order'    => 'ASC',
		] );

		$actions = $sync_actions + $delete_actions;

		$formatted = [];
		foreach ( $actions as $action_id => $action ) {
			$args        = $action->get_args();
			$formatted[] = [
				'id'        => $action_id,
				'hook'      => $action->get_hook(),
				'type'      => strpos( $action->get_hook(), 'sync' ) !== false ? 'sync' : 'delete',
				'item_type' => $args['item_type'] ?? '',
				'item_id'   => $args['item_id'] ?? '',
				'index_id'  => $args['index_id'] ?? '',
				'scheduled' => $action->get_schedule()->get_date()->format( 'Y-m-d H:i:s' ),
			];
		}

		wp_send_json_success( [
			'actions'   => $formatted,
			'count'     => count( $formatted ),
			'available' => true,
		] );
	}

	/**
	 * Returns list of views
	 *
	 * @return void
	 * @throws \Exception
	 */
	public function views_query(): void {
		$this->check_access( 'manage_options' );

		$repo = new ViewRepository();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$page     = isset( $_GET['page'] ) ? (int) $_GET['page'] : 1;
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$per_page = isset( $_GET['per_page'] ) ? (int) $_GET['per_page'] : 20;

		$counter = $repo->count( [] );
		$records = $repo->get( [], $page, $per_page );

		// Fetch index names for each view
		$index_repo = new IndexRepository();

		foreach ( $records as $key => $record ) {
			$formatted = $record->getFormatted();

			// Add index information
			if ( $record->getIndexId() ) {
				$index = $index_repo->find( $record->getIndexId() );
				if ( $index ) {
					$formatted->index_name = $index->getName();
				} else {
					$formatted->index_name = __( 'Unknown', 'celersearch' );
				}
			} else {
				$formatted->index_name = __( 'Not configured', 'celersearch' );
			}

			// Add shortcode for easy copying
			$formatted->shortcode = '[celersearch view="' . $record->getId() . '"]';

			$records[ $key ] = $formatted;
		}

		wp_send_json_success( [
			'records'    => $records,
			'pagination' => [
				'current_page' => $page,
				'total_pages'  => $counter > 0 ? ceil( $counter / $per_page ) : 1,
				'total'        => $counter,
				'per_page'     => $per_page,
			],
		] );
	}

	/**
	 * Returns single view from DB
	 *
	 * @return void
	 */
	public function views_find(): void {
		$this->check_access( 'manage_options' );

		$repo = new ViewRepository();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$record_id = isset( $_GET['id'] ) ? (int) $_GET['id'] : null;
		$record    = $record_id ? $repo->find( $record_id ) : null;

		if ( ! $record ) {
			wp_send_json_error( [ 'message' => __( 'View not found.', 'celersearch' ) ] );
		}

		wp_send_json_success( [ 'record' => $record->toArray() ] );
	}

	/**
	 * Store or update view
	 *
	 * @return void
	 */
	public function views_store(): void {
		$this->check_access( 'manage_options' );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$view_id  = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;
		$name     = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
		$index_id = isset( $_POST['index_id'] ) ? (int) $_POST['index_id'] : 0;

		// Validations
		$validations = [
			'name'     => 'required|min:2|max:255',
			'index_id' => 'required|numeric',
		];

		$validator  = new Validator();
		$validation = $validator->make( [
			'name'     => $name,
			'index_id' => $index_id,
		], $validations );
		// phpcs:enable WordPress.Security.NonceVerification.Missing
		$validation->validate();

		if ( $validation->fails() ) {
			wp_send_json_error( [ 'message' => $validation->errors()->firstOfAll() ] );
		}

		// Verify that the index exists
		$index_repo = new IndexRepository();
		$index      = $index_repo->find( $index_id );
		if ( ! $index ) {
			wp_send_json_error( [ 'message' => __( 'Selected index does not exist.', 'celersearch' ) ] );
		}

		// Build config from POST data
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below
		$raw_facet_groups = isset( $_POST['facet_groups'] ) && is_array( $_POST['facet_groups'] ) ? wp_unslash( $_POST['facet_groups'] ) : [];
		$facet_groups     = array_map( 'sanitize_text_field', $raw_facet_groups );

		$config = [
			'limit'             => isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 12,
			'placeholder'       => isset( $_POST['placeholder'] ) ? sanitize_text_field( wp_unslash( $_POST['placeholder'] ) ) : '',
			'show_facets'       => isset( $_POST['show_facets'] ) ? (bool) $_POST['show_facets'] : true,
			'highlight'         => isset( $_POST['highlight'] ) ? (bool) $_POST['highlight'] : true,
			'debounce'          => isset( $_POST['debounce'] ) ? absint( $_POST['debounce'] ) : 300,
			'min_chars'         => isset( $_POST['min_chars'] ) ? absint( $_POST['min_chars'] ) : 2,
			'mode'              => isset( $_POST['mode'] ) && in_array( $_POST['mode'], [ 'live', 'submit' ], true ) ? sanitize_text_field( $_POST['mode'] ) : 'live',
			'class'             => isset( $_POST['class'] ) ? sanitize_html_class( wp_unslash( $_POST['class'] ) ) : '',
			'initial_display'   => isset( $_POST['initial_display'] ) && in_array( $_POST['initial_display'], [ 'search_only', 'browse' ], true )
				? sanitize_text_field( $_POST['initial_display'] )
				: 'search_only',
			'default_sort'      => isset( $_POST['default_sort'] ) && in_array( $_POST['default_sort'], [ '', 'date', 'date-asc', 'title', 'title-desc' ], true )
				? sanitize_text_field( $_POST['default_sort'] )
				: '',
			'facet_groups_mode' => isset( $_POST['facet_groups_mode'] ) && in_array( $_POST['facet_groups_mode'], [ 'all', 'selected', 'none' ], true )
				? sanitize_text_field( $_POST['facet_groups_mode'] )
				: 'all',
			'facet_groups'      => $facet_groups,
		];
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		try {
			$repo = new ViewRepository();

			$data = [
				'name'     => $name,
				'type'     => 'search',
				'index_id' => $index_id,
				'config'   => $config,
			];

			if ( $view_id > 0 ) {
				$result = $repo->update( $view_id, $data );
				if ( $result === null ) {
					wp_send_json_error( [ 'message' => __( 'Unable to update view.', 'celersearch' ) ] );
				}
				wp_send_json_success( [
					'message' => __( 'View updated successfully.', 'celersearch' ),
					'record'  => $result->toArray(),
				] );
			} else {
				$result = $repo->create( $data );
				if ( ! $result ) {
					wp_send_json_error( [ 'message' => __( 'Unable to create view.', 'celersearch' ) ] );
				}
				wp_send_json_success( [
					'message' => __( 'View created successfully.', 'celersearch' ),
					'record'  => $result->toArray(),
				] );
			}
		} catch ( \Exception $e ) {
			$message = __( 'Failed to save view.', 'celersearch' );

			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Failed to save view', [
				'message' => $e->getMessage(),
				'trace'   => $e->getTraceAsString(),
			] );

			if ( EnvUtilities::is_debug() ) {
				wp_send_json_error( [ 'message' => $e->getMessage() ] );
			} else {
				wp_send_json_error( [ 'message' => $message ] );
			}
		}
	}

	/**
	 * Removes view from the database
	 *
	 * @return void
	 */
	public function views_remove(): void {
		$this->check_access( 'manage_options' );

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in check_access()
		$view_id = isset( $_POST['id'] ) ? (int) $_POST['id'] : 0;

		if ( ! $view_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid view id.', 'celersearch' ) ] );
		}

		$repo = new ViewRepository();
		if ( $repo->delete( $view_id ) ) {
			wp_send_json_success( [ 'message' => __( 'View removed successfully.', 'celersearch' ) ] );
		} else {
			wp_send_json_error( [ 'message' => __( 'Unable to remove view.', 'celersearch' ) ] );
		}
	}

	/**
	 * Get filterable attributes for an index
	 *
	 * @return void
	 */
	public function index_filterable_attributes(): void {
		$this->check_access( 'manage_options' );

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in check_access()
		$index_id = isset( $_GET['index_id'] ) ? (int) $_GET['index_id'] : 0;

		if ( ! $index_id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid index ID.', 'celersearch' ) ] );
		}

		try {
			$index    = IndexFactory::create( $index_id );
			$settings = $index->get_settings();

			$filterable = $settings->get_filterable_attributes();
			$labels     = $this->get_filterable_labels();

			$attributes = [];
			foreach ( $filterable as $key ) {
				$attributes[] = [
					'key'   => $key,
					'label' => isset( $labels[ $key ] ) ? $labels[ $key ] : $this->format_attribute_label( $key ),
				];
			}

			wp_send_json_success( [ 'attributes' => $attributes ] );

		} catch ( \Exception $e ) {
			if ( EnvUtilities::is_debug() ) {
				wp_send_json_error( [ 'message' => $e->getMessage() ] );
			} else {
				wp_send_json_error( [ 'message' => __( 'Failed to retrieve filterable attributes.', 'celersearch' ) ] );
			}
		}
	}

	/**
	 * Get human-readable labels for filterable attributes
	 *
	 * @return array
	 */
	private function get_filterable_labels(): array {
		$labels = [
			'taxonomies.category'    => __( 'Category', 'celersearch' ),
			'taxonomies.post_tag'    => __( 'Tags', 'celersearch' ),
			'taxonomies.product_cat' => __( 'Product Category', 'celersearch' ),
			'taxonomies.product_tag' => __( 'Product Tags', 'celersearch' ),
			'in_stock'               => __( 'Availability', 'celersearch' ),
			'ratings.average_rating' => __( 'Rating', 'celersearch' ),
			'price'                  => __( 'Price', 'celersearch' ),
		];

		// Add WooCommerce product attributes
		if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
			$attributes = wc_get_attribute_taxonomies();
			foreach ( $attributes as $attr ) {
				$key            = 'taxonomies.pa_' . $attr->attribute_name;
				$labels[ $key ] = $attr->attribute_label;
			}
		}

		/**
		 * Filter filterable attribute labels mapping
		 *
		 * @param array $labels The attribute labels map.
		 */
		return apply_filters( 'celersearch_filterable_attribute_labels', $labels );
	}

	/**
	 * Format attribute key into human-readable label
	 *
	 * @param string $key The attribute key.
	 *
	 * @return string
	 */
	private function format_attribute_label( string $key ): string {
		// Extract last part after dot
		$parts = explode( '.', $key );
		$last  = end( $parts );

		// Handle pa_ prefix (WooCommerce attributes)
		if ( strpos( $last, 'pa_' ) === 0 ) {
			$last = substr( $last, 3 );
		}

		// Convert snake_case/kebab-case to Title Case
		return ucfirst( str_replace( [ '-', '_' ], ' ', $last ) );
	}

	/**
	 * Check if the current user request is legit
	 * @param $capability
	 *
	 * @return void
	 */
	private function check_access( $capability ) {
		if ( ! check_ajax_referer( 'celersearch', '_wpnonce', false ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid security', 'celersearch' ) ] );
			exit;
		}
		if ( ! current_user_can( $capability ) ) {
			wp_send_json_error( [ 'message' => __( "You don't have permission to access this page.", 'celersearch' ) ] );
			exit;
		}
	}
}