<?php
/**
 * Advanced Conditions Rest API functions
 *
 * @package GenerateBlocks
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

require_once GENERATEBLOCKS_PRO_DIR . 'includes/utils/trait-usage-search.php';

/**
 * Class GenerateBlocks_Pro_Conditions_REST
 */
class GenerateBlocks_Pro_Conditions_REST extends WP_REST_Controller {

	use GenerateBlocks_Pro_Usage_Search;
	/**
	 * Instance.
	 *
	 * @access private
	 * @var object Instance
	 */
	private static $instance;

	/**
	 * Namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'generateblocks-pro/advanced-conditions/v';

	/**
	 * Version.
	 *
	 * @var string
	 */
	protected $version = '1';

	/**
	 * Initiator.
	 *
	 * @return object initialized object of class.
	 */
	public static function get_instance() {
		if ( ! isset( self::$instance ) ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register rest routes.
	 */
	public function register_routes() {
		$namespace = $this->namespace . $this->version;

		register_rest_route(
			$namespace,
			'/get_condition_types/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_condition_types' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
			]
		);

		register_rest_route(
			$namespace,
			'/get_condition_categories/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_condition_categories' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
			]
		);

		register_rest_route(
			$namespace,
			'/manage_category/',
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'create_category' ],
				'permission_callback' => [ $this, 'manage_permission' ],
			]
		);

		register_rest_route(
			$namespace,
			'/manage_category/(?P<id>[\d]+)',
			[
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'update_category' ],
				'permission_callback' => [ $this, 'manage_permission' ],
			]
		);

		register_rest_route(
			$namespace,
			'/manage_category/(?P<id>[\d]+)',
			[
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => [ $this, 'delete_category' ],
				'permission_callback' => [ $this, 'manage_permission' ],
			]
		);

		register_rest_route(
			$namespace,
			'/get_condition_rules/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_condition_rules' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'type' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/get_condition_operators/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_condition_operators' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'type' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'rule' => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/search_posts/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'search_posts' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'post_type' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'search' => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'id' => [
						'required'          => false,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/search_terms/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'search_terms' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'taxonomy' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'search' => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'id' => [
						'required'          => false,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/validate_conditions/',
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'validate_conditions' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'conditions' => [
						'required' => true,
						'type'     => 'object',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/get_rule_metadata/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_rule_metadata' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'type' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'rule' => [
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/batch_object_titles/',
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'batch_object_titles' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'requests' => [
						'required' => true,
						'type'     => 'array',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/conditions/',
			[
				[
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => [ $this, 'get_conditions' ],
					'permission_callback' => [ $this, 'edit_posts_permission' ],
					'args'                => [
						'page' => [
							'required'          => false,
							'type'              => 'integer',
							'default'           => 1,
							'sanitize_callback' => 'absint',
						],
						'per_page' => [
							'required'          => false,
							'type'              => 'integer',
							'default'           => 10,
							'sanitize_callback' => 'absint',
						],
					],
				],
				[
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => [ $this, 'create_condition' ],
					'permission_callback' => [ $this, 'edit_posts_permission' ],
					'args'                => [
						'title' => [
							'required'          => true,
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
						],
						'conditions' => [
							'required' => false,
							'type'     => 'object',
							'default'  => [
								'logic' => 'OR',
								'groups' => [],
							],
						],
						'status' => [
							'required'          => false,
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
							'default'           => 'publish',
							'enum'              => [ 'publish', 'draft' ],
						],
						'category' => [
							'required' => false,
							'type'     => [ 'array', 'string' ],
						],
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/conditions/(?P<id>\d+)',
			[
				[
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => [ $this, 'get_condition' ],
					'permission_callback' => [ $this, 'edit_posts_permission' ],
					'args'                => [
						'id' => [
							'required'          => true,
							'type'              => 'integer',
							'sanitize_callback' => 'absint',
						],
					],
				],
				[
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => [ $this, 'update_condition' ],
					'permission_callback' => [ $this, 'edit_posts_permission' ],
					'args'                => [
						'id' => [
							'required'          => true,
							'type'              => 'integer',
							'sanitize_callback' => 'absint',
						],
						'title' => [
							'required'          => false,
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
						],
						'conditions' => [
							'required' => false,
							'type'     => 'object',
						],
						'status' => [
							'required'          => false,
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
						],
					],
				],
				[
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => [ $this, 'delete_condition' ],
					'permission_callback' => [ $this, 'edit_posts_permission' ],
					'args'                => [
						'id' => [
							'required'          => true,
							'type'              => 'integer',
							'sanitize_callback' => 'absint',
						],
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/search_hierarchical_posts/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'search_hierarchical_posts' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'search' => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'id' => [
						'required'          => false,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/get_day_options/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_day_options' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
			]
		);

		register_rest_route(
			$namespace,
			'/conditions/(?P<id>\d+)/usage',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_condition_usage' ],
				'permission_callback' => [ $this, 'manage_permission' ],
				'args'                => [
					'id' => [
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					],
					'limit' => [
						'required'          => false,
						'type'              => 'integer',
						'default'           => 50,
						'sanitize_callback' => 'absint',
					],
					'refresh' => [
						'required'          => false,
						'type'              => 'boolean',
						'default'           => false,
						'sanitize_callback' => 'rest_sanitize_boolean',
					],
				],
			]
		);

		register_rest_route(
			$namespace,
			'/search_users/',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'search_users' ],
				'permission_callback' => [ $this, 'edit_posts_permission' ],
				'args'                => [
					'search' => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'id' => [
						'required'          => false,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					],
				],
			]
		);
	}

	/**
	 * Batch object titles endpoint
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function batch_object_titles( WP_REST_Request $request ) {
		$requests = $request->get_param( 'requests' );

		if ( empty( $requests ) || ! is_array( $requests ) ) {
			return $this->error( 'invalid_requests', __( 'Invalid requests parameter.', 'generateblocks-pro' ) );
		}

		$results = [];

		// Group requests by type for efficient batch processing.
		$grouped_requests = [
			'posts' => [],
			'terms' => [],
			'hierarchical_posts' => [],
			'days' => [],
		];

		foreach ( $requests as $request_item ) {
			if ( ! isset( $request_item['type'], $request_item['rule'], $request_item['value'] ) ) {
				continue;
			}

			$type = sanitize_text_field( $request_item['type'] );
			$rule = sanitize_text_field( $request_item['rule'] );
			$value = sanitize_text_field( $request_item['value'] );
			$cache_key = "{$type}_{$rule}_{$value}";

			// Handle day of week specially.
			if ( 'date_time' === $type && 'day_of_week' === $rule ) {
				$grouped_requests['days'][] = [
					'cache_key' => $cache_key,
					'value' => $value,
				];
				continue;
			}

			// Handle hierarchical posts.
			if ( in_array( $rule, [ 'child_of', 'parent_of' ], true ) ) {
				$grouped_requests['hierarchical_posts'][] = [
					'cache_key' => $cache_key,
					'value' => $value,
				];
				continue;
			}

			// Handle post types.
			if ( 0 === strpos( $rule, 'post:' ) ) {
				$post_type = str_replace( 'post:', '', $rule );
				$grouped_requests['posts'][] = [
					'cache_key' => $cache_key,
					'post_type' => $post_type,
					'value' => $value,
				];
				continue;
			}

			// Handle taxonomies.
			if ( 0 === strpos( $rule, 'taxonomy:' ) || 0 === strpos( $rule, 'post_terms:' ) ) {
				$taxonomy = str_replace( [ 'taxonomy:', 'post_terms:' ], '', $rule );
				$grouped_requests['terms'][] = [
					'cache_key' => $cache_key,
					'taxonomy' => $taxonomy,
					'value' => $value,
				];
				continue;
			}
		}

		// Process days.
		foreach ( $grouped_requests['days'] as $day_request ) {
			$results[ $day_request['cache_key'] ] = $this->get_day_name( $day_request['value'] );
		}

		// Process posts in batches.
		if ( ! empty( $grouped_requests['posts'] ) ) {
			$post_results = $this->batch_load_post_titles( $grouped_requests['posts'] );
			$results = array_merge( $results, $post_results );
		}

		// Process terms in batches.
		if ( ! empty( $grouped_requests['terms'] ) ) {
			$term_results = $this->batch_load_term_names( $grouped_requests['terms'] );
			$results = array_merge( $results, $term_results );
		}

		// Process hierarchical posts in batches.
		if ( ! empty( $grouped_requests['hierarchical_posts'] ) ) {
			$hierarchical_results = $this->batch_load_hierarchical_post_titles( $grouped_requests['hierarchical_posts'] );
			$results = array_merge( $results, $hierarchical_results );
		}

		return $this->success( $results );
	}

	/**
	 * Batch load post titles
	 *
	 * @param array $requests Post requests.
	 * @return array
	 */
	private function batch_load_post_titles( $requests ) {
		$results = [];
		$posts_by_type = [];

		// Group by post type.
		foreach ( $requests as $request ) {
			$post_type = $request['post_type'];
			if ( ! isset( $posts_by_type[ $post_type ] ) ) {
				$posts_by_type[ $post_type ] = [];
			}
			$posts_by_type[ $post_type ][] = $request;
		}

		// Query each post type.
		foreach ( $posts_by_type as $post_type => $type_requests ) {
			$post_ids = array_column( $type_requests, 'value' );
			$post_ids = array_map( 'intval', $post_ids );
			$post_ids = array_unique( $post_ids );

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

			$posts = get_posts(
				[
					'post_type' => $post_type,
					'post__in' => $post_ids,
					'post_status' => [ 'publish', 'private', 'draft' ],
					'posts_per_page' => -1,
				]
			);

			$posts_by_id = [];
			foreach ( $posts as $post ) {
				$posts_by_id[ $post->ID ] = $post;
			}

			// Map results back to cache keys.
			foreach ( $type_requests as $request ) {
				$post_id = intval( $request['value'] );
				if ( isset( $posts_by_id[ $post_id ] ) ) {
					$post = $posts_by_id[ $post_id ];
					$title = $post->post_title ? $post->post_title : sprintf(
						/* translators: %1$s: post type, %2$d: post ID */
						__( '%1$s #%2$d', 'generateblocks-pro' ),
						get_post_type_object( $post_type )->labels->singular_name,
						$post->ID
					);
					$results[ $request['cache_key'] ] = $title;
				} else {
					$results[ $request['cache_key'] ] = "ID: {$post_id}";
				}
			}
		}

		return $results;
	}

	/**
	 * Batch load term names
	 *
	 * @param array $requests Term requests.
	 * @return array
	 */
	private function batch_load_term_names( $requests ) {
		$results = [];
		$terms_by_taxonomy = [];

		// Group by taxonomy.
		foreach ( $requests as $request ) {
			$taxonomy = $request['taxonomy'];
			if ( ! isset( $terms_by_taxonomy[ $taxonomy ] ) ) {
				$terms_by_taxonomy[ $taxonomy ] = [];
			}
			$terms_by_taxonomy[ $taxonomy ][] = $request;
		}

		// Query each taxonomy.
		foreach ( $terms_by_taxonomy as $taxonomy => $taxonomy_requests ) {
			$term_ids = array_column( $taxonomy_requests, 'value' );
			$term_ids = array_map( 'intval', $term_ids );
			$term_ids = array_unique( $term_ids );

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

			$terms = get_terms(
				[
					'taxonomy' => $taxonomy,
					'include' => $term_ids,
					'hide_empty' => false,
				]
			);

			if ( is_wp_error( $terms ) ) {
				continue;
			}

			$terms_by_id = [];
			foreach ( $terms as $term ) {
				$terms_by_id[ $term->term_id ] = $term;
			}

			// Map results back to cache keys.
			foreach ( $taxonomy_requests as $request ) {
				$term_id = intval( $request['value'] );
				if ( isset( $terms_by_id[ $term_id ] ) ) {
					$results[ $request['cache_key'] ] = $terms_by_id[ $term_id ]->name;
				} else {
					$results[ $request['cache_key'] ] = "ID: {$term_id}";
				}
			}
		}

		return $results;
	}

	/**
	 * Batch load hierarchical post titles
	 *
	 * @param array $requests Hierarchical post requests.
	 * @return array
	 */
	private function batch_load_hierarchical_post_titles( $requests ) {
		$results = [];
		$post_ids = array_column( $requests, 'value' );
		$post_ids = array_map( 'intval', $post_ids );
		$post_ids = array_unique( $post_ids );

		if ( empty( $post_ids ) ) {
			return $results;
		}

		// Get hierarchical post types.
		$hierarchical_types = get_post_types(
			[
				'public' => true,
				'hierarchical' => true,
			],
			'names'
		);

		if ( empty( $hierarchical_types ) ) {
			return $results;
		}

		$posts = get_posts(
			[
				'post_type' => array_values( $hierarchical_types ),
				'post__in' => $post_ids,
				'post_status' => [ 'publish', 'private' ],
				'posts_per_page' => -1,
			]
		);

		$posts_by_id = [];
		foreach ( $posts as $post ) {
			$ancestors = get_post_ancestors( $post->ID );
			$hierarchy_indicator = str_repeat( '— ', count( $ancestors ) );
			$title = $hierarchy_indicator . ( $post->post_title ? $post->post_title : sprintf(
				/* translators: %1$s: post type, %2$d: post ID */
				__( '%1$s #%2$d', 'generateblocks-pro' ),
				get_post_type_object( $post->post_type )->labels->singular_name,
				$post->ID
			) );
			$posts_by_id[ $post->ID ] = $title;
		}

		// Map results back to cache keys.
		foreach ( $requests as $request ) {
			$post_id = intval( $request['value'] );
			if ( isset( $posts_by_id[ $post_id ] ) ) {
				// Strip hierarchy indicators for display.
				$clean_title = preg_replace( '/^(—\s*)+/', '', $posts_by_id[ $post_id ] );
				$results[ $request['cache_key'] ] = $clean_title;
			} else {
				$results[ $request['cache_key'] ] = "ID: {$post_id}";
			}
		}

		return $results;
	}

	/**
	 * Get day name
	 *
	 * @param string $day_number Day number.
	 * @return string
	 */
	private function get_day_name( $day_number ) {
		$days = [
			'1' => __( 'Monday', 'generateblocks-pro' ),
			'2' => __( 'Tuesday', 'generateblocks-pro' ),
			'3' => __( 'Wednesday', 'generateblocks-pro' ),
			'4' => __( 'Thursday', 'generateblocks-pro' ),
			'5' => __( 'Friday', 'generateblocks-pro' ),
			'6' => __( 'Saturday', 'generateblocks-pro' ),
			'7' => __( 'Sunday', 'generateblocks-pro' ),
		];

		return $days[ $day_number ] ?? $day_number;
	}

	/**
	 * Enhanced but safe permission callback.
	 *
	 * @return bool
	 */
	public function edit_posts_permission() {
		// For modifying operations, check manage permission.
		$request_method = $_SERVER['REQUEST_METHOD'] ?? '';

		if ( in_array( $request_method, [ 'POST', 'PUT', 'PATCH', 'DELETE' ], true ) ) {
			// Creating, updating, or deleting requires manage permission.
			return GenerateBlocks_Pro_Conditions::current_user_can_use_conditions( 'manage' );
		}

		// Reading/listing only requires use permission.
		return GenerateBlocks_Pro_Conditions::current_user_can_use_conditions( 'use' );
	}

	/**
	 * Permission callback for category management operations.
	 * Categories should only be manageable by administrators.
	 *
	 * @return bool
	 */
	public function manage_permission() {
		return GenerateBlocks_Pro_Conditions::current_user_can_use_conditions( 'manage' );
	}

	/**
	 * Verify nonce for state-changing operations.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return bool|WP_Error True if valid, WP_Error if invalid.
	 */
	private function verify_nonce( WP_REST_Request $request ) {
		$nonce = $request->get_header( 'X-WP-Nonce' );

		if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
			return new WP_Error(
				'rest_cookie_invalid_nonce',
				__( 'Cookie check failed.', 'generateblocks-pro' ),
				[ 'status' => 403 ]
			);
		}

		return true;
	}

	/**
	 * Get all condition types.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_condition_types( WP_REST_Request $request ) {
		$types = GenerateBlocks_Pro_Conditions::get_condition_types();

		$formatted_types = [];
		foreach ( $types as $key => $type ) {
			$formatted_types[ $key ] = [
				'label'     => $type['label'],
				'operators' => $type['operators'],
			];
		}

		return $this->success( $formatted_types );
	}

	/**
	 * Get all condition categories.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_condition_categories( WP_REST_Request $request ) {
		$categories = get_terms(
			[
				'taxonomy'   => 'gblocks_condition_cat',
				'hide_empty' => false,
			]
		);

		if ( is_wp_error( $categories ) ) {
			return $this->error( 'taxonomy_error', __( 'Could not retrieve categories.', 'generateblocks-pro' ) );
		}

		$formatted_categories = [];
		foreach ( $categories as $category ) {
			$formatted_categories[ $category->slug ] = [
				'slug'  => $category->slug,
				'name'  => $category->name,
				'count' => $category->count,
			];
		}

		return $this->success( $formatted_categories );
	}

	/**
	 * Get rules for a specific condition type.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_condition_rules( WP_REST_Request $request ) {
		$type = $request->get_param( 'type' );

		if ( empty( $type ) ) {
			return $this->error( 'missing_type', __( 'Condition type is required.', 'generateblocks-pro' ) );
		}

		$rules = GenerateBlocks_Pro_Conditions::get_condition_rules( $type );

		return $this->success( $rules );
	}

	/**
	 * Get operators for a specific condition type.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_condition_operators( WP_REST_Request $request ) {
		$type = $request->get_param( 'type' );
		$rule = $request->get_param( 'rule' );

		if ( empty( $type ) ) {
			return $this->error( 'missing_type', __( 'Condition type is required.', 'generateblocks-pro' ) );
		}

		$operators = [];

		// Get the condition instance to check for rule-specific operators.
		$instance = GenerateBlocks_Pro_Conditions_Registry::get_instance( $type );

		if ( $instance ) {
			// Try to get rule-specific operators if rule is provided and method exists.
			if ( ! empty( $rule ) && method_exists( $instance, 'get_operators_for_rule' ) ) {
				$operators = $instance->get_operators_for_rule( $rule );
			}
		}

		// Fallback to all operators for the type if no rule-specific operators found.
		if ( empty( $operators ) ) {
			$types = GenerateBlocks_Pro_Conditions::get_condition_types();
			if ( isset( $types[ $type ] ) ) {
				$operators = $types[ $type ]['operators'];
			}
		}

		if ( empty( $operators ) ) {
			return $this->error( 'no_operators', __( 'No operators found for this condition type.', 'generateblocks-pro' ) );
		}

		$formatted_operators = [];
		foreach ( $operators as $operator ) {
			$formatted_operators[ $operator ] = $this->get_operator_label( $operator );
		}

		return $this->success( $formatted_operators );
	}

	/**
	 * Search posts for a specific post type.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function search_posts( WP_REST_Request $request ) {
		$post_type = $request->get_param( 'post_type' );
		$search    = $request->get_param( 'search' );
		$id        = $request->get_param( 'id' );

		if ( empty( $post_type ) ) {
			return $this->error( 'missing_post_type', __( 'Post type is required.', 'generateblocks-pro' ) );
		}

		// Handle ID search.
		if ( ! empty( $id ) ) {
			$post = get_post( $id );
			if ( $post && $post_type === $post->post_type ) {
				// Exclude the Posts Page from page results.
				if ( 'page' === $post_type ) {
					$page_for_posts = get_option( 'page_for_posts' );
					if ( $page_for_posts && intval( $page_for_posts ) === $post->ID ) {
						return $this->success( [] );
					}
				}

				$formatted_posts = [
					[
						'id'    => $post->ID,
						'title' => $post->post_title ? $post->post_title : sprintf(
							/* translators: %1$s: post type, %2$d: post ID */
							__( '%1$s #%2$d', 'generateblocks-pro' ),
							get_post_type_object( $post_type )->labels->singular_name,
							$post->ID
						),
					],
				];
				return $this->success( $formatted_posts );
			} else {
				return $this->success( [] );
			}
		}

		$args = [
			'post_type'      => $post_type,
			'post_status'    => [ 'publish', 'private', 'draft' ],
			'posts_per_page' => empty( $search ) ? 10 : 50, // phpcs:ignore -- WordPress.WP.PostsPerPage.posts_per_page
			'orderby'        => empty( $search ) ? 'date' : 'title',
			'order'          => empty( $search ) ? 'DESC' : 'ASC',
		];

		if ( ! empty( $search ) ) {
			$args['s'] = $search;
		}

		// Exclude the Posts Page from page results.
		if ( 'page' === $post_type ) {
			$page_for_posts = get_option( 'page_for_posts' );
			if ( $page_for_posts ) {
				$args['post__not_in'] = [ $page_for_posts ];
			}
		}

		$posts = get_posts( $args );

		$formatted_posts = [];
		foreach ( $posts as $post ) {
			$formatted_posts[] = [
				'id'    => $post->ID,
				// translators: %1$s is the post type singular name, %2$d is the post ID.
				'title' => $post->post_title ? $post->post_title : sprintf( __( '%1$s #%2$d', 'generateblocks-pro' ), get_post_type_object( $post_type )->labels->singular_name, $post->ID ),
			];
		}

		return $this->success( $formatted_posts );
	}

	/**
	 * Search terms for a specific taxonomy.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function search_terms( WP_REST_Request $request ) {
		$taxonomy = $request->get_param( 'taxonomy' );
		$search   = $request->get_param( 'search' );
		$id       = $request->get_param( 'id' );

		if ( empty( $taxonomy ) ) {
			return $this->error( 'missing_taxonomy', __( 'Taxonomy is required.', 'generateblocks-pro' ) );
		}

		// Handle ID search.
		if ( ! empty( $id ) ) {
			$term = get_term( $id, $taxonomy );
			if ( ! is_wp_error( $term ) && $term ) {
				$formatted_terms = [
					[
						'id'   => $term->term_id,
						'name' => $term->name,
					],
				];
				return $this->success( $formatted_terms );
			} else {
				return $this->success( [] );
			}
		}

		$args = [
			'taxonomy'   => $taxonomy,
			'hide_empty' => false,
			'number'     => empty( $search ) ? 10 : 50, // Recent items vs search results.
			'orderby'    => empty( $search ) ? 'count' : 'name',
			'order'      => empty( $search ) ? 'DESC' : 'ASC',
		];

		if ( ! empty( $search ) ) {
			$args['search'] = $search;
		}

		$terms = get_terms( $args );

		if ( is_wp_error( $terms ) ) {
			return $this->error( 'terms_error', $terms->get_error_message() );
		}

		$formatted_terms = [];
		foreach ( $terms as $term ) {
			$formatted_terms[] = [
				'id'   => $term->term_id,
				'name' => $term->name,
			];
		}

		return $this->success( $formatted_terms );
	}

	/**
	 * Validate conditions structure.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function validate_conditions( WP_REST_Request $request ) {
		$conditions = $request->get_param( 'conditions' );

		$validation_result = $this->validate_conditions_structure( $conditions );

		if ( true === $validation_result ) {
			return $this->success( [ 'valid' => true ] );
		}

		return $this->error( 'validation_failed', $validation_result );
	}

	/**
	 * Get metadata for a specific rule.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_rule_metadata( WP_REST_Request $request ) {
		$type = $request->get_param( 'type' );
		$rule = $request->get_param( 'rule' );

		if ( empty( $type ) || empty( $rule ) ) {
			return $this->error( 'missing_parameters', __( 'Both type and rule parameters are required.', 'generateblocks-pro' ) );
		}

		$metadata = GenerateBlocks_Pro_Conditions::get_rule_metadata( $type, $rule );

		return $this->success( $metadata );
	}

	/**
	 * Get all conditions.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_conditions( WP_REST_Request $request ) {
		$page     = $request->get_param( 'page' );
		$per_page = $request->get_param( 'per_page' );

		$args = [
			'post_type'      => 'gblocks_condition',
			'post_status'    => [ 'publish', 'draft' ],
			'posts_per_page' => $per_page,
			'paged'          => $page,
			'orderby'        => 'date',
			'order'          => 'DESC',
		];

		$query = new WP_Query( $args );
		$posts = $query->posts;

		$conditions = [];
		foreach ( $posts as $post ) {
			$conditions[] = $this->format_condition_response( $post );
		}

		$response = [
			'conditions' => $conditions,
			'total'      => $query->found_posts,
			'pages'      => $query->max_num_pages,
		];

		return $this->success( $response );
	}

	/**
	 * Get a single condition.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_condition( WP_REST_Request $request ) {
		$id = $request->get_param( 'id' );

		$post = get_post( $id );

		if ( ! $post || 'gblocks_condition' !== $post->post_type ) {
			return $this->error( 'condition_not_found', __( 'Condition not found.', 'generateblocks-pro' ) );
		}

		return $this->success( $this->format_condition_response( $post ) );
	}

	/**
	 * Create a new condition.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function create_condition( WP_REST_Request $request ) {
		$nonce_check = $this->verify_nonce( $request );
		if ( is_wp_error( $nonce_check ) ) {
			return $nonce_check;
		}

		$title      = $request->get_param( 'title' );
		$conditions = $request->get_param( 'conditions' );
		$category   = $request->get_param( 'category' );
		$status     = $request->get_param( 'status' );

		if ( empty( $title ) ) {
			return $this->error( 'missing_title', __( 'Title is required.', 'generateblocks-pro' ) );
		}

		// Validate status.
		if ( ! empty( $status ) && ! in_array( $status, [ 'publish', 'draft' ], true ) ) {
			$status = 'publish';
		}

		$post_data = [
			'post_title'   => $title,
			'post_type'    => 'gblocks_condition',
			'post_status'  => $status ? $status : 'publish',
			'post_content' => '',
		];

		$post_id = wp_insert_post( $post_data );

		if ( is_wp_error( $post_id ) ) {
			return $this->error( 'creation_failed', $post_id->get_error_message() );
		}

		// Set categories if provided.
		if ( ! empty( $category ) ) {
			// Handle both single category (string) and multiple categories (array) for future compatibility
			// Currently UI only supports single category selection, but backend stores as array
			// for potential future multi-category support without data migration.
			$categories = is_array( $category ) ? $category : [ $category ];
			$term_ids = [];

			foreach ( $categories as $cat_slug ) {
				if ( empty( $cat_slug ) ) {
					continue;
				}

				// Check if the term exists, if not create it.
				$term = get_term_by( 'slug', $cat_slug, 'gblocks_condition_cat' );
				if ( ! $term ) {
					// Create the term if it doesn't exist.
					$term_result = wp_insert_term( $cat_slug, 'gblocks_condition_cat', [ 'slug' => $cat_slug ] );
					if ( ! is_wp_error( $term_result ) ) {
						$term_ids[] = $term_result['term_id'];
					} else {
						// If creation fails, try to get by name.
						$term = get_term_by( 'name', $cat_slug, 'gblocks_condition_cat' );
						if ( $term ) {
							$term_ids[] = $term->term_id;
						}
					}
				} else {
					$term_ids[] = $term->term_id;
				}
			}

			if ( ! empty( $term_ids ) ) {
				wp_set_post_terms( $post_id, $term_ids, 'gblocks_condition_cat' );
			}
		}

		// Use the main class sanitization method.
		$conditions_instance = GenerateBlocks_Pro_Conditions::get_instance();
		$sanitized_conditions = $conditions_instance->sanitize_conditions( $conditions );
		update_post_meta( $post_id, '_gb_conditions', $sanitized_conditions );

		$post = get_post( $post_id );
		return $this->success( $this->format_condition_response( $post ) );
	}

	/**
	 * Update an existing condition.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function update_condition( WP_REST_Request $request ) {
		$nonce_check = $this->verify_nonce( $request );
		if ( is_wp_error( $nonce_check ) ) {
			return $nonce_check;
		}

		$id         = $request->get_param( 'id' );
		$title      = $request->get_param( 'title' );
		$conditions = $request->get_param( 'conditions' );
		$status     = $request->get_param( 'status' );
		$category   = $request->get_param( 'category' );

		$post = get_post( $id );

		if ( ! $post || 'gblocks_condition' !== $post->post_type ) {
			return $this->error( 'condition_not_found', __( 'Condition not found.', 'generateblocks-pro' ) );
		}

		$update_data = [ 'ID' => $id ];

		if ( ! empty( $title ) ) {
			$update_data['post_title'] = $title;
		}

		if ( ! empty( $status ) && in_array( $status, [ 'publish', 'draft' ], true ) ) {
			$update_data['post_status'] = $status;
		}

		if ( ! empty( $update_data ) && 1 < count( $update_data ) ) {
			$result = wp_update_post( $update_data );
			if ( is_wp_error( $result ) ) {
				return $this->error( 'update_failed', $result->get_error_message() );
			}
		}

		// Update categories if provided.
		if ( isset( $category ) ) {
			if ( ! empty( $category ) ) {
				// Handle both single category (string) and multiple categories (array) for future compatibility
				// Currently UI only supports single category selection, but backend stores as array
				// for potential future multi-category support without data migration.
				$categories = is_array( $category ) ? $category : [ $category ];
				$term_ids = [];

				foreach ( $categories as $cat_slug ) {
					if ( empty( $cat_slug ) ) {
						continue;
					}

					// Check if the term exists, if not create it.
					$term = get_term_by( 'slug', $cat_slug, 'gblocks_condition_cat' );
					if ( ! $term ) {
						// Create the term if it doesn't exist.
						$term_result = wp_insert_term( $cat_slug, 'gblocks_condition_cat', [ 'slug' => $cat_slug ] );
						if ( ! is_wp_error( $term_result ) ) {
							$term_ids[] = $term_result['term_id'];
						} else {
							// If creation fails, try to get by name.
							$term = get_term_by( 'name', $cat_slug, 'gblocks_condition_cat' );
							if ( $term ) {
								$term_ids[] = $term->term_id;
							}
						}
					} else {
						$term_ids[] = $term->term_id;
					}
				}

				wp_set_post_terms( $id, $term_ids, 'gblocks_condition_cat' );
			} else {
				// Clear all categories.
				wp_set_post_terms( $id, [], 'gblocks_condition_cat' );
			}
		}

		if ( ! empty( $conditions ) ) {
			// Use the main class sanitization method.
			$conditions_instance = GenerateBlocks_Pro_Conditions::get_instance();
			$sanitized_conditions = $conditions_instance->sanitize_conditions( $conditions );
			update_post_meta( $id, '_gb_conditions', $sanitized_conditions );
		}

		$updated_post = get_post( $id );
		return $this->success( $this->format_condition_response( $updated_post ) );
	}

	/**
	 * Delete a condition.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function delete_condition( WP_REST_Request $request ) {
		$nonce_check = $this->verify_nonce( $request );
		if ( is_wp_error( $nonce_check ) ) {
			return $nonce_check;
		}

		$id = $request->get_param( 'id' );

		$post = get_post( $id );

		if ( ! $post || 'gblocks_condition' !== $post->post_type ) {
			return $this->error( 'condition_not_found', __( 'Condition not found.', 'generateblocks-pro' ) );
		}

		$result = wp_delete_post( $id, true );

		if ( ! $result ) {
			return $this->error( 'deletion_failed', __( 'Failed to delete condition.', 'generateblocks-pro' ) );
		}

		return $this->success( [ 'deleted' => true ] );
	}

	/**
	 * Format condition response.
	 *
	 * @param WP_Post $post The post object.
	 * @return array
	 */
	private function format_condition_response( $post ) {
		$conditions = get_post_meta( $post->ID, '_gb_conditions', true );
		if ( ! $conditions ) {
			$conditions = [
				'logic' => 'OR',
				'groups' => [],
			];
		}

		// Get category terms.
		$category_terms = wp_get_post_terms( $post->ID, 'gblocks_condition_cat', [ 'fields' => 'ids' ] );
		$category_terms = is_wp_error( $category_terms ) ? [] : $category_terms;

		return [
			'id'                     => $post->ID,
			'title'                  => [
				'rendered' => $post->post_title,
			],
			'status'                 => $post->post_status,
			'date'                   => $post->post_date,
			'modified'               => $post->post_modified,
			'gbConditions'           => $conditions,
			'gblocks_condition_cat'  => $category_terms,
		];
	}

	/**
	 * Validate the structure of conditions.
	 *
	 * @param array $conditions The conditions to validate.
	 * @return bool|string True if valid, error message if invalid.
	 */
	private function validate_conditions_structure( $conditions ) {
		if ( ! is_array( $conditions ) ) {
			return __( 'Conditions must be an array.', 'generateblocks-pro' );
		}

		if ( ! isset( $conditions['logic'] ) || ! in_array( $conditions['logic'], [ 'AND', 'OR' ], true ) ) {
			return __( 'Invalid or missing logic operator.', 'generateblocks-pro' );
		}

		if ( ! isset( $conditions['groups'] ) || ! is_array( $conditions['groups'] ) ) {
			return __( 'Groups must be an array.', 'generateblocks-pro' );
		}

		$available_types = array_keys( GenerateBlocks_Pro_Conditions::get_condition_types() );

		foreach ( $conditions['groups'] as $group_index => $group ) {
			if ( ! is_array( $group ) ) {
				// translators: %d is the group index.
				return sprintf( __( 'Group %d must be an array.', 'generateblocks-pro' ), $group_index );
			}

			if ( ! isset( $group['logic'] ) || ! in_array( $group['logic'], [ 'AND', 'OR' ], true ) ) {
				// translators: %d is the group index.
				return sprintf( __( 'Invalid or missing logic operator for group %d.', 'generateblocks-pro' ), $group_index );
			}

			if ( ! isset( $group['conditions'] ) || ! is_array( $group['conditions'] ) ) {
				// translators: %d is the group index.
				return sprintf( __( 'Conditions for group %d must be an array.', 'generateblocks-pro' ), $group_index );
			}

			foreach ( $group['conditions'] as $condition_index => $condition ) {
				if ( ! is_array( $condition ) ) {
					// translators: %1$d is the condition index, %2$d is the group index.
					return sprintf( __( 'Condition %1$d in group %2$d must be an array.', 'generateblocks-pro' ), $condition_index, $group_index );
				}

				$required_fields = [ 'type', 'rule', 'operator' ];
				foreach ( $required_fields as $field ) {
					if ( ! isset( $condition[ $field ] ) ) {
						// translators: %1$s is the missing field, %2$d is the condition index, %3$d is the group index.
						return sprintf( __( 'Missing required field "%1$s" in condition %2$d of group %3$d.', 'generateblocks-pro' ), $field, $condition_index, $group_index );
					}
				}

				if ( ! in_array( $condition['type'], $available_types, true ) ) {
					// translators: %1$s is the invalid condition type, %2$d is the condition index, %3$d is the group index.
					return sprintf( __( 'Invalid condition type "%1$s" in condition %2$d of group %3$d.', 'generateblocks-pro' ), $condition['type'], $condition_index, $group_index );
				}
			}
		}

		return true;
	}

	/**
	 * Get human-readable label for an operator.
	 *
	 * @param string $operator The operator.
	 * @return string
	 */
	private function get_operator_label( $operator ) {
		$labels = [
			'is'           => __( 'Is', 'generateblocks-pro' ),
			'is_not'       => __( 'Is Not', 'generateblocks-pro' ),
			'exists'       => __( 'Exists', 'generateblocks-pro' ),
			'not_exists'   => __( 'Does Not Exist', 'generateblocks-pro' ),
			'has_value'    => __( 'Has Value', 'generateblocks-pro' ),
			'no_value'     => __( 'Has No Value', 'generateblocks-pro' ),
			'equals'       => __( 'Equals', 'generateblocks-pro' ),
			'contains'     => __( 'Contains', 'generateblocks-pro' ),
			'not_contains' => __( 'Does Not Contain', 'generateblocks-pro' ),
			'starts_with'  => __( 'Starts With', 'generateblocks-pro' ),
			'ends_with'    => __( 'Ends With', 'generateblocks-pro' ),
			'before'       => __( 'Before', 'generateblocks-pro' ),
			'after'        => __( 'After', 'generateblocks-pro' ),
			'between'      => __( 'Between', 'generateblocks-pro' ),
			'on'           => __( 'On', 'generateblocks-pro' ),
			'greater_than' => __( 'Greater Than', 'generateblocks-pro' ),
			'less_than'    => __( 'Less Than', 'generateblocks-pro' ),
			'includes_any' => __( 'Includes Any', 'generateblocks-pro' ),
			'includes_all' => __( 'Includes All', 'generateblocks-pro' ),
			'excludes_any' => __( 'Excludes Any', 'generateblocks-pro' ),
			'excludes_all' => __( 'Excludes All', 'generateblocks-pro' ),
		];

		return $labels[ $operator ] ?? ucfirst( str_replace( '_', ' ', $operator ) );
	}

	/**
	 * Search hierarchical posts (pages and hierarchical custom post types).
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function search_hierarchical_posts( WP_REST_Request $request ) {
		$search = $request->get_param( 'search' );
		$id     = $request->get_param( 'id' );

		// Get all hierarchical post types.
		$hierarchical_types = [];
		$post_types = get_post_types(
			[
				'public' => true,
				'hierarchical' => true,
			],
			'names'
		);

		if ( empty( $post_types ) ) {
			return $this->success( [] );
		}

		// Handle ID search.
		if ( ! empty( $id ) ) {
			$post = get_post( $id );
			if ( $post && in_array( $post->post_type, $post_types, true ) ) {
				$ancestors = get_post_ancestors( $post->ID );
				$hierarchy_indicator = str_repeat( '— ', count( $ancestors ) );
				$title = $hierarchy_indicator . ( $post->post_title ? $post->post_title : sprintf(
					/* translators: %1$s: post type, %2$d: post ID */
					__( '%1$s #%2$d', 'generateblocks-pro' ),
					get_post_type_object( $post->post_type )->labels->singular_name,
					$post->ID
				) );

				$formatted_posts = [
					[
						'id'    => $post->ID,
						'title' => $title,
					],
				];
				return $this->success( $formatted_posts );
			} else {
				return $this->success( [] );
			}
		}

		$args = [
			'post_type'      => array_values( $post_types ),
			'post_status'    => [ 'publish', 'private' ],
			'posts_per_page' => empty( $search ) ? 10 : 50, // phpcs:ignore -- WordPress.WP.PostsPerPage.posts_per_page
			'orderby'        => empty( $search ) ? 'date' : 'menu_order title',
			'order'          => empty( $search ) ? 'DESC' : 'ASC',
		];

		if ( ! empty( $search ) ) {
			$args['s'] = $search;
		}

		$posts = get_posts( $args );

		$formatted_posts = [];
		foreach ( $posts as $post ) {
			$ancestors = get_post_ancestors( $post->ID );
			$hierarchy_indicator = str_repeat( '— ', count( $ancestors ) );

			$formatted_posts[] = [
				'id'    => $post->ID,
				'title' => $hierarchy_indicator . ( $post->post_title ? $post->post_title : sprintf(
					/* translators: %1$s: post type singular name, %2$d: post ID */
					__( '%1$s #%2$d', 'generateblocks-pro' ),
					get_post_type_object( $post->post_type )->labels->singular_name,
					$post->ID
				) ),
			];
		}

		// Sort to maintain hierarchy when showing recent items.
		if ( ! empty( $search ) ) {
			// For search results, sort to maintain hierarchy.
			usort(
				$formatted_posts,
				function( $a, $b ) {
					return strnatcasecmp( $a['title'], $b['title'] );
				}
			);
		}

		return $this->success( $formatted_posts );
	}

	/**
	 * Get day options for day_of_week rule.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_day_options( WP_REST_Request $request ) {
		$instance = GenerateBlocks_Pro_Conditions_Registry::get_instance( 'date_time' );

		if ( ! $instance || ! method_exists( $instance, 'get_day_options' ) ) {
			return $this->error( 'not_available', __( 'Day options not available.', 'generateblocks-pro' ) );
		}

		$options = $instance->get_day_options();

		return $this->success( $options );
	}

	/**
	 * Get condition usage across the site.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function get_condition_usage( WP_REST_Request $request ) {
		$condition_id = $request->get_param( 'id' );
		$limit = $request->get_param( 'limit' );
		$refresh = $request->get_param( 'refresh' );

		if ( empty( $condition_id ) ) {
			return $this->error( 'missing_condition_id', __( 'Condition ID is required.', 'generateblocks-pro' ) );
		}

		// Verify the condition exists.
		$condition_post = get_post( $condition_id );
		if ( ! $condition_post || 'gblocks_condition' !== $condition_post->post_type ) {
			return $this->error( 'condition_not_found', __( 'Condition not found.', 'generateblocks-pro' ) );
		}

		// Check cache unless refresh is requested.
		$cache_key = sprintf( 'gb_condition_usage_%d_limit%d', $condition_id, $limit );
		if ( ! $refresh ) {
			$cached_data = $this->get_usage_cache( $cache_key );
			if ( false !== $cached_data ) {
				$cached_data['cached'] = true;
				$cached_data['cached_at'] = $cached_data['cached_at'] ?? time();
				return $this->success( $cached_data );
			}
		}

		$usage_data = $this->search_condition_usage( $condition_id, $limit );

		// Cache for 15 minutes (900 seconds).
		// Note: Cache invalidation is intentionally manual (via refresh button in UI).
		// Automatic invalidation on post save/delete was deemed too complex and risky
		// due to the need to clear caches across all conditions when any post changes.
		// Users can click refresh to get updated results when needed.
		$usage_data['cached_at'] = time();
		$this->set_usage_cache( $cache_key, $usage_data, 900 );

		$usage_data['cached'] = false;

		return $this->success( $usage_data );
	}

	/**
	 * Search for condition usage across different data sources.
	 *
	 * @param int $condition_id The condition ID to search for.
	 * @param int $limit        Maximum results per handler to return (default: 50).
	 * @return array
	 */
	private function search_condition_usage( $condition_id, $limit = 50 ) {
		$all_usage = [];
		$total_found = 0;
		$has_more = false;
		$limitations = [];
		$has_limitations = false;

		$limit = max( 1, absint( $limit ) );

		// Define search handlers - easily extensible for future use cases.
		$search_handlers = [
			'overlay_display_conditions' => [
				'method' => 'search_overlay_display_conditions',
				'label'  => __( 'Overlay Panel Display Conditions', 'generateblocks-pro' ),
			],
		];

		// Apply filters to allow other plugins/themes to add search handlers.
		$search_handlers = apply_filters( 'generateblocks_condition_usage_handlers', $search_handlers, $condition_id );

		foreach ( $search_handlers as $handler_key => $handler_config ) {
			if ( ! method_exists( $this, $handler_config['method'] ) ) {
				continue;
			}

			$handler_results = $this->{$handler_config['method']}( $condition_id, $limit );

			if ( empty( $handler_results['items'] ) && empty( $handler_results['has_more'] ) && empty( $handler_results['limited'] ) ) {
				continue;
			}

			$items = array_slice( $handler_results['items'], 0, $limit );

			$all_usage[ $handler_key ] = [
				'label'    => $handler_config['label'],
				'items'    => $items,
				'has_more' => ! empty( $handler_results['has_more'] ),
			];
			$total_found += count( $items );

			// If any handler has more results, set the overall has_more flag.
			if ( ! empty( $handler_results['has_more'] ) ) {
				$has_more = true;
			}

			if ( ! empty( $handler_results['limited'] ) ) {
				$has_limitations = true;
			}

			if ( ! empty( $handler_results['limits'] ) && is_array( $handler_results['limits'] ) ) {
				foreach ( $handler_results['limits'] as $limit_key => $limit_data ) {
					if ( ! isset( $limitations[ $limit_key ] ) ) {
						$limitations[ $limit_key ] = $limit_data;
						continue;
					}

					// Merge limitation metadata, keeping the highest values where appropriate.
					$existing = $limitations[ $limit_key ];

					if ( isset( $limit_data['max_posts'] ) ) {
						$existing['max_posts'] = isset( $existing['max_posts'] )
							? max( $existing['max_posts'], $limit_data['max_posts'] )
							: $limit_data['max_posts'];
					}

					if ( isset( $limit_data['scanned_posts'] ) ) {
						$existing['scanned_posts'] = isset( $existing['scanned_posts'] )
							? max( $existing['scanned_posts'], $limit_data['scanned_posts'] )
							: $limit_data['scanned_posts'];
					}

					if ( isset( $limit_data['total_posts'] ) ) {
						$existing['total_posts'] = isset( $existing['total_posts'] )
							? max( $existing['total_posts'], $limit_data['total_posts'] )
							: $limit_data['total_posts'];
					}

					$limitations[ $limit_key ] = $existing;
				}
			}
		}

		return [
			'usage'       => $all_usage,
			'total'       => $total_found,
			'has_more'    => $has_more,
			'limited'     => $has_limitations,
			'limitations' => $limitations,
		];
	}

	/**
	 * Search for posts using this condition in overlay panel display conditions.
	 *
	 * @param int $condition_id The condition ID.
	 * @param int $limit        Maximum results to return.
	 * @return array
	 */
	private function search_overlay_display_conditions( $condition_id, $limit ) {
		global $wpdb;

		// Validate and sanitize inputs.
		$condition_id = absint( $condition_id );
		$limit = absint( $limit );

		if ( ! $condition_id ) {
			return [
				'items'    => [],
				'has_more' => false,
			];
		}

		// Query one more than requested to determine if there are more results.
		$query_limit = $limit + 1;

		$results_query = $wpdb->prepare(
			"SELECT DISTINCT p.ID, p.post_title, p.post_type, p.post_status
		FROM {$wpdb->posts} p
		INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
		WHERE pm.meta_key = %s
		AND pm.meta_value = %s
		AND p.post_status IN ('publish', 'private', 'draft')
		ORDER BY p.post_modified DESC
		LIMIT %d",
			'_gb_overlay_display_condition',
			$condition_id,
			$query_limit
		);

		$results = $wpdb->get_results( $results_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		// Check if there are more results.
		$has_more = count( $results ) > $limit;

		// Remove the extra result if we got it.
		if ( $has_more ) {
			array_pop( $results );
		}

		$formatted_results = [];
		foreach ( $results as $result ) {
			$post_type_object = get_post_type_object( $result->post_type );
			$post_type_label = $post_type_object ? $post_type_object->labels->singular_name : $result->post_type;

			// Check if post type is publicly viewable.
			$is_public = $post_type_object && ( $post_type_object->public || $post_type_object->publicly_queryable );

			// translators: %1$s: Post type label, %2$d: Post ID.
			$fallback_title = $result->post_title ? $result->post_title : sprintf( __( '%1$s #%2$d', 'generateblocks-pro' ), $post_type_label, $result->ID );

			$formatted_results[] = [
				'id'          => absint( $result->ID ),
				'title'       => $fallback_title,
				'type'        => $result->post_type,
				'type_label'  => $post_type_label,
				'status'      => $result->post_status,
				'edit_url'    => get_edit_post_link( $result->ID, 'raw' ),
				'view_url'    => ( 'publish' === $result->post_status && $is_public ) ? get_permalink( $result->ID ) : null,
				'usage_type'  => 'overlay_display_condition',
			];
		}

		return [
			'items'    => $formatted_results,
			'has_more' => $has_more,
			'limited'  => false,
			'limits'   => [],
		];
	}

	/**
	 * Search for blocks using this condition.
	 *
	 * @param int $condition_id The condition ID.
	 * @param int $limit        Maximum results to return.
	 * @return array
	 */
	private function search_block_conditions_usage( $condition_id, $limit ) {
		global $wpdb;

		// Validate and sanitize inputs.
		$condition_id = absint( $condition_id );
		$limit = absint( $limit );

		if ( ! $condition_id ) {
			return [
				'items'    => [],
				'has_more' => false,
			];
		}

		// Get post types that support the block editor.
		$post_types = get_post_types_by_support( 'editor' );
		$excluded_types = [ 'revision', 'attachment', 'nav_menu_item' ];
		$post_types = array_diff( $post_types, $excluded_types );
		$post_types = apply_filters( 'generateblocks_usage_search_post_types', $post_types, 'conditions' );

		if ( empty( $post_types ) ) {
			return [
				'items'    => [],
				'has_more' => false,
			];
		}

		$post_statuses = [ 'publish', 'private', 'draft' ];

		// Search for the condition ID in post content as a block attribute.
		// We're looking for gbBlockCondition attribute with the condition ID.
		$search_string = '"gbBlockCondition":"' . absint( $condition_id ) . '"';
		$search_pattern = '%' . $wpdb->esc_like( $search_string ) . '%';

		// Query one more than requested to determine if there are more results.
		$query_limit = $limit + 1;

		// Cap the number of posts scanned (prevents slow queries on large sites).
		// Users can adjust this via filter if needed.
		$max_posts_to_scan = apply_filters( 'generateblocks_usage_search_max_posts', 5000 );

		$post_types_placeholders = implode( ', ', array_fill( 0, count( $post_types ), '%s' ) );
		$status_placeholders = implode( ', ', array_fill( 0, count( $post_statuses ), '%s' ) );

		// Determine overall dataset size so we can inform users when the scan is limited.
		$total_posts_sql = sprintf(
			"SELECT COUNT(ID)
			FROM {$wpdb->posts}
			WHERE post_status IN (%s)
			AND post_type IN (%s)",
			$status_placeholders,
			$post_types_placeholders
		);
		$total_posts_query = $wpdb->prepare(
			$total_posts_sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			array_merge( $post_statuses, $post_types )
		);
		$total_posts = absint( $wpdb->get_var( $total_posts_query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$scanned_posts = min( $total_posts, $max_posts_to_scan );
		$limited_scan = $total_posts > $max_posts_to_scan;

		// Two-phase query: First get recent posts, then search within them.
		// This prevents full table scans on large sites.
		// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
		$sql = sprintf(
			"SELECT ID, post_title, post_type, post_status, post_content
			FROM (
				SELECT ID, post_title, post_type, post_status, post_content, post_modified
				FROM {$wpdb->posts}
				WHERE post_status IN ('publish', 'private', 'draft')
				AND post_type IN (%s)
				ORDER BY post_modified DESC
				LIMIT %%d
			) AS recent_posts
			WHERE post_content LIKE %%s
			LIMIT %%d",
			$post_types_placeholders
		);
		$results_query = $wpdb->prepare(
			$sql, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			array_merge( $post_types, [ $max_posts_to_scan, $search_pattern, $query_limit ] )
		);
		// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

		$results = $wpdb->get_results( $results_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		// Check if there are more results.
		$has_more = count( $results ) > $limit;

		// Remove the extra result if we got it.
		if ( $has_more ) {
			array_pop( $results );
		}

		$formatted_results = [];
		foreach ( $results as $result ) {
			$post_type_object = get_post_type_object( $result->post_type );
			$post_type_label = $post_type_object ? $post_type_object->labels->singular_name : $result->post_type;

			// Check if post type is publicly viewable.
			$is_public = $post_type_object && ( $post_type_object->public || $post_type_object->publicly_queryable );

			// Count how many blocks in this post use the condition.
			$block_count = substr_count( $result->post_content, '"gbBlockCondition":"' . $condition_id . '"' );

			// translators: %1$s: Post type label, %2$d: Post ID.
			$fallback_title = $result->post_title ? $result->post_title : sprintf( __( '%1$s #%2$d', 'generateblocks-pro' ), $post_type_label, $result->ID );

			$formatted_results[] = [
				'id'          => absint( $result->ID ),
				'title'       => $fallback_title,
				'type'        => $result->post_type,
				'type_label'  => $post_type_label,
				'status'      => $result->post_status,
				'edit_url'    => get_edit_post_link( $result->ID, 'raw' ),
				'view_url'    => ( 'publish' === $result->post_status && $is_public ) ? get_permalink( $result->ID ) : null,
				'usage_type'  => 'block_conditions',
				'block_count' => $block_count,
			];
		}

		return [
			'items'    => $formatted_results,
			'has_more' => $has_more || $limited_scan,
			'limited'  => $limited_scan,
			'limits'   => $limited_scan
				? [
					'recent_post_scan' => [
						'max_posts'      => $max_posts_to_scan,
						'scanned_posts'  => $scanned_posts,
						'total_posts'    => $total_posts,
					],
				]
				: [],
		];
	}

	/**
	 * Search for menu items using this condition.
	 *
	 * @param int $condition_id The condition ID.
	 * @param int $limit        Maximum results to return.
	 * @return array
	 */
	private function search_menu_item_conditions_usage( $condition_id, $limit ) {
		global $wpdb;

		// Validate and sanitize inputs.
		$condition_id = absint( $condition_id );
		$limit = absint( $limit );

		if ( ! $condition_id ) {
			return [
				'items'    => [],
				'has_more' => false,
			];
		}

		// Query one more than requested to determine if there are more results.
		$query_limit = $limit + 1;

		$results_query = $wpdb->prepare(
			"SELECT DISTINCT p.ID, p.post_title, pm2.meta_value as menu_id
			FROM {$wpdb->posts} p
			INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
			LEFT JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id AND pm2.meta_key = '_menu_item_menu_item_parent'
			WHERE pm.meta_key = %s
			AND pm.meta_value = %s
			AND p.post_type = 'nav_menu_item'
			ORDER BY p.menu_order ASC
			LIMIT %d",
			'_gb_menu_condition',
			$condition_id,
			$query_limit
		);

		$results = $wpdb->get_results( $results_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		// Check if there are more results.
		$has_more = count( $results ) > $limit;

		// Remove the extra result if we got it.
		if ( $has_more ) {
			array_pop( $results );
		}

		$formatted_results = [];
		foreach ( $results as $result ) {
			// Get the menu this item belongs to.
			$menu_object = wp_get_object_terms( $result->ID, 'nav_menu' );
			$menu_name = ! empty( $menu_object ) && ! is_wp_error( $menu_object )
				? $menu_object[0]->name
				: __( 'Unknown Menu', 'generateblocks-pro' );

			// Format the title.
			$item_title = $result->post_title ? $result->post_title : __( 'Menu Item', 'generateblocks-pro' );

			// translators: %1$s: Menu item title, %2$s: Menu name.
			$full_title = sprintf( __( '%1$s (in %2$s)', 'generateblocks-pro' ), $item_title, $menu_name );

			$formatted_results[] = [
				'id'          => absint( $result->ID ),
				'title'       => $full_title,
				'type'        => 'nav_menu_item',
				'type_label'  => __( 'Menu Item', 'generateblocks-pro' ),
				'status'      => 'publish',
				'edit_url'    => admin_url( 'nav-menus.php' ),
				'view_url'    => null, // Menu items don't have direct view URLs.
				'usage_type'  => 'menu_item_conditions',
			];
		}

		return [
			'items'    => $formatted_results,
			'has_more' => $has_more,
			'limited'  => false,
			'limits'   => [],
		];
	}

	/**
	 * Search users for author conditions.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function search_users( WP_REST_Request $request ) {
		$search = $request->get_param( 'search' );
		$id     = $request->get_param( 'id' );

		// Handle ID search.
		if ( ! empty( $id ) ) {
			$user = get_user_by( 'ID', $id );
			if ( $user ) {
				$formatted_users = [
					[
						'id'   => $user->ID,
						'name' => $user->display_name ? $user->display_name : $user->user_login,
					],
				];
				return $this->success( $formatted_users );
			} else {
				return $this->success( [] );
			}
		}

		$args = [
			'fields'  => [ 'ID', 'display_name', 'user_login', 'user_email' ],
			'number'  => empty( $search ) ? 10 : 50,
			'orderby' => empty( $search ) ? 'registered' : 'display_name',
			'order'   => empty( $search ) ? 'DESC' : 'ASC',
		];

		if ( ! empty( $search ) ) {
			$args['search'] = '*' . esc_attr( $search ) . '*';
			$args['search_columns'] = [ 'user_login', 'user_email', 'display_name' ];
		}

		// Only get users who have published posts (actual authors).
		$args['has_published_posts'] = true;

		$users = get_users( $args );

		$formatted_users = [];
		foreach ( $users as $user ) {
			$display_name = $user->display_name ? $user->display_name : $user->user_login;

			$formatted_users[] = [
				'id'   => $user->ID,
				'name' => $display_name,
				'title' => $display_name,
			];
		}

		return $this->success( $formatted_users );
	}

	/**
	 * Success rest.
	 *
	 * @param mixed $response response data.
	 * @return mixed
	 */
	public function success( $response ) {
		return new WP_REST_Response(
			array(
				'success'  => true,
				'response' => $response,
			),
			200
		);
	}

	/**
	 * Failed rest.
	 *
	 * @param mixed $response response data.
	 * @return mixed
	 */
	public function failed( $response ) {
		return new WP_REST_Response(
			array(
				'success'  => false,
				'response' => $response,
			),
			200
		);
	}

	/**
	 * Create a new category.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function create_category( WP_REST_Request $request ) {
		$nonce_check = $this->verify_nonce( $request );
		if ( is_wp_error( $nonce_check ) ) {
			return $nonce_check;
		}

		$name = $request->get_param( 'name' );
		$slug = $request->get_param( 'slug' );

		if ( empty( $name ) ) {
			return $this->error( 'missing_name', __( 'Category name is required.', 'generateblocks-pro' ) );
		}

		$term_data = [ 'name' => $name ];
		if ( ! empty( $slug ) ) {
			$term_data['slug'] = $slug;
		}

		$term = wp_insert_term( $name, 'gblocks_condition_cat', $term_data );

		if ( is_wp_error( $term ) ) {
			return $this->error( 'creation_failed', $term->get_error_message() );
		}

		$created_term = get_term( $term['term_id'], 'gblocks_condition_cat' );
		return $this->success(
			[
				'id' => $created_term->term_id,
				'name' => $created_term->name,
				'slug' => $created_term->slug,
				'count' => $created_term->count,
			]
		);
	}

	/**
	 * Update a category.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function update_category( WP_REST_Request $request ) {
		$nonce_check = $this->verify_nonce( $request );
		if ( is_wp_error( $nonce_check ) ) {
			return $nonce_check;
		}

		$id = $request->get_param( 'id' );
		$name = $request->get_param( 'name' );

		if ( empty( $name ) ) {
			return $this->error( 'missing_name', __( 'Category name is required.', 'generateblocks-pro' ) );
		}

		$term = wp_update_term(
			$id,
			'gblocks_condition_cat',
			[
				'name' => $name,
			]
		);

		if ( is_wp_error( $term ) ) {
			return $this->error( 'update_failed', $term->get_error_message() );
		}

		$updated_term = get_term( $term['term_id'], 'gblocks_condition_cat' );
		return $this->success(
			[
				'id' => $updated_term->term_id,
				'name' => $updated_term->name,
				'slug' => $updated_term->slug,
				'count' => $updated_term->count,
			]
		);
	}

	/**
	 * Delete a category.
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_REST_Response
	 */
	public function delete_category( WP_REST_Request $request ) {
		$nonce_check = $this->verify_nonce( $request );
		if ( is_wp_error( $nonce_check ) ) {
			return $nonce_check;
		}

		$id = $request->get_param( 'id' );

		$term = get_term( $id, 'gblocks_condition_cat' );
		if ( is_wp_error( $term ) || ! $term ) {
			return $this->error( 'term_not_found', __( 'Category not found.', 'generateblocks-pro' ) );
		}

		$result = wp_delete_term( $id, 'gblocks_condition_cat' );

		if ( is_wp_error( $result ) ) {
			return $this->error( 'deletion_failed', $result->get_error_message() );
		}

		if ( ! $result ) {
			return $this->error( 'deletion_failed', __( 'Category could not be deleted.', 'generateblocks-pro' ) );
		}

		return $this->success( [ 'deleted' => true ] );
	}

	/**
	 * Error rest.
	 *
	 * @param mixed $code     error code.
	 * @param mixed $response response data.
	 * @return mixed
	 */
	public function error( $code, $response ) {
		return new WP_REST_Response(
			array(
				'error'      => true,
				'success'    => false,
				'error_code' => $code,
				'response'   => $response,
			),
			400
		);
	}
}

GenerateBlocks_Pro_Conditions_REST::get_instance();
