<?php
/**
 * @package ACF
 * @author  WP Engine
 *
 * © 2026 Advanced Custom Fields (ACF®). All rights reserved.
 * "ACF" is a trademark of WP Engine.
 * Licensed under the GNU General Public License v2 or later.
 * https://www.gnu.org/licenses/gpl-2.0.html
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

if ( ! class_exists( 'ACF_Ajax_Query_Users' ) ) :

	class ACF_Ajax_Query_Users extends ACF_Ajax_Query {

		/** @var string The AJAX action name. */
		var $action = 'acf/ajax/query_users';

		/**
		 * Verifies the request.
		 *
		 * @since 6.3.2
		 *
		 * @param array $request The request args.
		 * @return  (bool|WP_Error) True on success, WP_Error on fail.
		 */
		public function verify_request( $request ) {
			if ( empty( $request['nonce'] ) || empty( $request['field_key'] ) ) {
				return new WP_Error( 'acf_invalid_args', __( 'Invalid request args.', 'acf' ), array( 'status' => 404 ) );
			}

			$nonce        = $request['nonce'];
			$action       = $request['field_key'];
			$field_action = true;

			if ( isset( $request['conditional_logic'] ) && true === (bool) $request['conditional_logic'] ) {
				if ( ! acf_current_user_can_admin() ) {
					return new WP_Error( 'acf_invalid_permissions', __( 'Sorry, you do not have permission to do that.', 'acf' ) );
				}

				// Use the standard ACF admin nonce.
				$nonce        = '';
				$action       = '';
				$field_action = false;
			}

			if ( ! acf_verify_ajax( $nonce, $action, $field_action ) ) {
				return new WP_Error( 'acf_invalid_nonce', __( 'Invalid nonce.', 'acf' ), array( 'status' => 404 ) );
			}

			return true;
		}

		/**
		 * init_request
		 *
		 * Called at the beginning of a request to setup properties.
		 *
		 * @date    23/5/19
		 * @since   5.8.1
		 *
		 * @param   array $request The request args.
		 * @return  void
		 */
		function init_request( $request ) {
			parent::init_request( $request );

			// Customize query.
			add_filter( 'user_search_columns', array( $this, 'filter_search_columns' ), 10, 3 );

			/**
			 * Fires when a request is made.
			 *
			 * @date    21/5/19
			 * @since   5.8.1
			 *
			 * @param   array $request The query request.
			 * @param   ACF_Ajax_Query $query The query object.
			 */
			do_action( 'acf/ajax/query_users/init', $request, $this );
		}

		/**
		 * Returns an array of args for this query.
		 *
		 * @since 5.7.2
		 *
		 * @param array $request The request args.
		 * @return array
		 */
		public function get_args( $request ) {
			$args = array(
				'number' => $this->per_page,
				'paged'  => $this->page,
			);

			if ( $this->is_search ) {
				$args['search'] = "*{$this->search}*";
			}

			/**
			 * Filters the query args.
			 *
			 * @since 5.8.1
			 *
			 * @param array          $args    The query args.
			 * @param array          $request The query request.
			 * @param ACF_Ajax_Query $query   The query object.
			 */
			return apply_filters( 'acf/ajax/query_users/args', $args, $request, $this );
		}

		/**
		 * Prepares args for the get_results() method.
		 *
		 * @date    23/3/20
		 * @since   5.8.9
		 *
		 * @param   array args The query args.
		 * @return  array
		 */
		function prepare_args( $args ) {

			// Parse pagination args that may have been modified.
			if ( isset( $args['users_per_page'] ) ) {
				$this->per_page = intval( $args['users_per_page'] );
				unset( $args['users_per_page'] );
			} elseif ( isset( $args['number'] ) ) {
				$this->per_page = intval( $args['number'] );
			}

			if ( isset( $args['paged'] ) ) {
				$this->page = intval( $args['paged'] );
				unset( $args['paged'] );
			}

			// Set pagination args for fine control.
			$args['number']      = $this->per_page;
			$args['offset']      = $this->per_page * ( $this->page - 1 );
			$args['count_total'] = true;
			return $args;
		}

		/**
		 * Returns an array of results for the given args.
		 *
		 * @since   5.7.2
		 *
		 * @param array $args The query args.
		 * @return array
		 */
		public function get_results( $args ) {
			// Prepare the $args for query.
			$args    = $this->prepare_args( $args );
			$results = array();

			// Apply field role restriction, allowing overrides via filter.
			if ( $this->field && ! empty( $this->field['role'] ) && empty( $args['role__in'] ) ) {
				$args['role__in'] = (array) $this->field['role'];
			}

			// Get result groups.
			if ( ! empty( $args['role__in'] ) ) {
				$roles = acf_get_user_role_labels( $args['role__in'] );
			} else {
				$roles = acf_get_user_role_labels();
			}

			// Return a flat array of results when searching or when querying one group only.
			if ( $this->is_search || count( $roles ) === 1 ) {

				// Query users and append to results.
				$wp_user_query = new WP_User_Query( $args );
				$users         = (array) $wp_user_query->get_results();
				$total_users   = $wp_user_query->get_total();
				foreach ( $users as $user ) {
					$results[] = $this->get_result( $user );
				}

				// Determine if more results exist.
				// As this query does not return grouped results, the calculation can be exact (">").
				$this->more = ( $total_users > count( $users ) + $args['offset'] );
				// Otherwise, group results via role.
			} else {

				// Unset args that will interfer with query results.
				unset( $args['role__in'], $args['role__not_in'] );

				$args['search'] = $this->search ? $this->search : '';

				// Loop over each role.
				foreach ( $roles as $role => $role_label ) {

					// Query users (for this role only).
					$args['role']  = $role;
					$wp_user_query = new WP_User_Query( $args );
					$users         = (array) $wp_user_query->get_results();
					$total_users   = $wp_user_query->get_total();

					// acf_log( $args );
					// acf_log( '- ', count($users) );
					// acf_log( '- ', $total_users );
					// If users were found for this query...
					if ( $users ) {

						// Append optgroup of results.
						$role_results = array();
						foreach ( $users as $user ) {
							$role_results[] = $this->get_result( $user );
						}
						$results[] = array(
							'text'     => $role_label,
							'children' => $role_results,
						);

						// End loop when enough results have been found.
						if ( count( $users ) === $args['number'] ) {

							// Determine if more results exist.
							// As this query does return grouped results, the calculation is best left fuzzy to avoid querying the next group (">=").
							$this->more = ( $total_users >= count( $users ) + $args['offset'] );
							break;

							// Otherwise, modify the args so that the next query can continue on correctly.
						} else {
							$args['offset']  = 0;
							$args['number'] -= count( $users );
						}

						// If no users were found (for the current pagination args), but there were users found for previous pages...
						// Modify the args so that the next query is offset slightly less (the number of total users) and can continue on correctly.
					} elseif ( $total_users ) {
						$args['offset'] -= $total_users;
						continue;

						// Ignore roles that will never return a result.
					} else {
						continue;
					}
				}
			}

			/**
			 * Filters the query results.
			 *
			 * @date    21/5/19
			 * @since   5.8.1
			 *
			 * @param   array $results The query results.
			 * @param   array $args The query args.
			 * @param   ACF_Ajax_Query $query The query object.
			 */
			return apply_filters( 'acf/ajax/query_users/results', $results, $args, $this );
		}

		/**
		 * get_result
		 *
		 * Returns a single result for the given item object.
		 *
		 * @date    31/7/18
		 * @since   5.7.2
		 *
		 * @param   mixed $item A single item from the queried results.
		 * @return  string
		 */
		function get_result( $user ) {
			$item = acf_get_user_result( $user );

			/**
			 * Filters the result item.
			 *
			 * @date    21/5/19
			 * @since   5.8.1
			 *
			 * @param   array $item The choice id and text.
			 * @param   ACF_User $user The user object.
			 * @param   ACF_Ajax_Query $query The query object.
			 */
			return apply_filters( 'acf/ajax/query_users/result', $item, $user, $this );
		}

		/**
		 * Filters the WP_User_Query search columns.
		 *
		 * @since 5.8.8
		 *
		 * @param array         $columns       An array of column names to be searched.
		 * @param string        $search        The search term.
		 * @param WP_User_Query $WP_User_Query The WP_User_Query instance.
		 * @return array
		 */
		public function filter_search_columns( $columns, $search, $WP_User_Query ) {
			// Restrict search columns for users that can't edit other users.
			if ( ! current_user_can( 'edit_users' ) ) {
				$columns = array( 'user_login', 'user_nicename', 'display_name' );
			}

			/**
			 * Filters the column names to be searched.
			 *
			 * @since 5.8.1
			 *
			 * @param array          $columns       An array of column names to be searched.
			 * @param string         $search        The search term.
			 * @param WP_User_Query  $WP_User_Query The WP_User_Query instance.
			 * @param ACF_Ajax_Query $query         The query object.
			 */
			return apply_filters( 'acf/ajax/query_users/search_columns', $columns, $search, $WP_User_Query, $this );
		}
	}

	acf_new_instance( 'ACF_Ajax_Query_Users' );
endif; // class_exists check
