<?php
/**
 * AJAX search for relational field types (posts, terms, users).
 *
 * @package CustomFields
 * @since 1.9.0
 */

namespace CustomFields;

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

/**
 * Server-side search helpers for relational fields.
 */
final class RelationalSearch {

	/**
	 * Register REST search routes.
	 */
	public static function registerRoutes(): void {
		$permission = static function (): bool {
			return current_user_can( 'edit_posts' );
		};

		$users_permission = static function (): bool {
			return current_user_can( 'list_users' );
		};

		register_rest_route(
			'wp-powersuite/v1',
			'/custom-fields/search/posts',
			[
				'methods'             => 'GET',
				'callback'            => [ self::class, 'restPosts' ],
				'permission_callback' => $permission,
				'args'                => [
					's'          => [
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'post_types' => [
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'include'    => [
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					],
				],
			]
		);

		register_rest_route(
			'wp-powersuite/v1',
			'/custom-fields/search/terms',
			[
				'methods'             => 'GET',
				'callback'            => [ self::class, 'restTerms' ],
				'permission_callback' => $permission,
				'args'                => [
					's'        => [
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'taxonomy' => [
						'type'              => 'string',
						'required'          => true,
						'sanitize_callback' => 'sanitize_key',
					],
					'include'  => [
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					],
				],
			]
		);

		register_rest_route(
			'wp-powersuite/v1',
			'/custom-fields/search/users',
			[
				'methods'             => 'GET',
				'callback'            => [ self::class, 'restUsers' ],
				'permission_callback' => $users_permission,
				'args'                => [
					's'       => [
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'include' => [
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					],
				],
			]
		);
	}

	/**
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response
	 */
	public static function restPosts( \WP_REST_Request $request ): \WP_REST_Response {
		$post_types = self::parseList( (string) $request->get_param( 'post_types' ) );
		$include    = self::parseIds( (string) $request->get_param( 'include' ) );

		return new \WP_REST_Response(
			[
				'results' => self::searchPosts(
					(string) $request->get_param( 's' ),
					$post_types,
					$include
				),
			]
		);
	}

	/**
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response
	 */
	public static function restTerms( \WP_REST_Request $request ): \WP_REST_Response {
		$taxonomy = sanitize_key( (string) $request->get_param( 'taxonomy' ) );
		if ( $taxonomy === '' || ! taxonomy_exists( $taxonomy ) ) {
			return new \WP_REST_Response( [ 'results' => [] ], 400 );
		}

		return new \WP_REST_Response(
			[
				'results' => self::searchTerms(
					(string) $request->get_param( 's' ),
					$taxonomy,
					self::parseIds( (string) $request->get_param( 'include' ) )
				),
			]
		);
	}

	/**
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response
	 */
	public static function restUsers( \WP_REST_Request $request ): \WP_REST_Response {
		return new \WP_REST_Response(
			[
				'results' => self::searchUsers(
					(string) $request->get_param( 's' ),
					self::parseIds( (string) $request->get_param( 'include' ) )
				),
			]
		);
	}

	/**
	 * @param array<int, string> $post_types Post type keys.
	 * @param array<int, int>    $include    Selected IDs to always include.
	 * @return array<int, array{value: string, label: string}>
	 */
	public static function searchPosts( string $search, array $post_types, array $include = [] ): array {
		$out     = [];
		$seen    = [];
		$include = array_values( array_filter( array_map( 'absint', $include ) ) );

		foreach ( $include as $post_id ) {
			$post = get_post( $post_id );
			if ( ! $post instanceof \WP_Post ) {
				continue;
			}
			if ( $post_types !== [] && ! in_array( $post->post_type, $post_types, true ) ) {
				continue;
			}
			$out[]         = self::formatPost( $post );
			$seen[ $post_id ] = true;
		}

		$args = [
			'post_type'              => $post_types !== [] ? $post_types : 'any',
			'post_status'            => 'publish',
			'posts_per_page'         => 20,
			'orderby'                => 'title',
			'order'                  => 'ASC',
			's'                      => $search,
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'post__not_in'           => array_keys( $seen ),
		];

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

		$query = new \WP_Query( $args );
		foreach ( $query->posts as $post ) {
			if ( ! $post instanceof \WP_Post ) {
				continue;
			}
			$out[] = self::formatPost( $post );
		}

		return $out;
	}

	/**
	 * @param array<int, int> $include Selected term IDs to always include.
	 * @return array<int, array{value: string, label: string}>
	 */
	public static function searchTerms( string $search, string $taxonomy, array $include = [] ): array {
		$out     = [];
		$seen    = [];
		$include = array_values( array_filter( array_map( 'absint', $include ) ) );

		foreach ( $include as $term_id ) {
			$term = get_term( $term_id, $taxonomy );
			if ( ! $term instanceof \WP_Term || is_wp_error( $term ) ) {
				continue;
			}
			$out[]            = self::formatTerm( $term );
			$seen[ $term_id ] = true;
		}

		$args = [
			'taxonomy'   => $taxonomy,
			'hide_empty' => false,
			'number'     => 20,
			'orderby'    => 'name',
			'order'      => 'ASC',
			'exclude'    => array_keys( $seen ),
		];

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

		$terms = get_terms( $args );
		if ( ! is_array( $terms ) ) {
			return $out;
		}

		foreach ( $terms as $term ) {
			if ( ! $term instanceof \WP_Term ) {
				continue;
			}
			$out[] = self::formatTerm( $term );
		}

		return $out;
	}

	/**
	 * @param array<int, int> $include Selected user IDs to always include.
	 * @return array<int, array{value: string, label: string}>
	 */
	public static function searchUsers( string $search, array $include = [] ): array {
		$out     = [];
		$seen    = [];
		$include = array_values( array_filter( array_map( 'absint', $include ) ) );

		foreach ( $include as $user_id ) {
			$user = get_user_by( 'id', $user_id );
			if ( ! $user instanceof \WP_User ) {
				continue;
			}
			$out[]             = self::formatUser( $user );
			$seen[ $user_id ]  = true;
		}

		$args = [
			'number'  => 20,
			'orderby' => 'display_name',
			'order'   => 'ASC',
			'exclude' => array_keys( $seen ),
		];

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

		foreach ( get_users( $args ) as $user ) {
			if ( ! $user instanceof \WP_User ) {
				continue;
			}
			$out[] = self::formatUser( $user );
		}

		return $out;
	}

	/**
	 * @return array{value: string, label: string}
	 */
	private static function formatPost( \WP_Post $post ): array {
		return [
			'value' => (string) $post->ID,
			'label' => $post->post_title . ' (' . $post->post_type . ')',
		];
	}

	/**
	 * @return array{value: string, label: string}
	 */
	private static function formatTerm( \WP_Term $term ): array {
		return [
			'value' => (string) $term->term_id,
			'label' => $term->name,
		];
	}

	/**
	 * @return array{value: string, label: string}
	 */
	private static function formatUser( \WP_User $user ): array {
		return [
			'value' => (string) $user->ID,
			'label' => $user->display_name . ' (' . $user->user_login . ')',
		];
	}

	/**
	 * @return array<int, string>
	 */
	private static function parseList( string $raw ): array {
		if ( $raw === '' ) {
			return [];
		}

		return array_values(
			array_filter(
				array_map( 'sanitize_key', array_map( 'trim', explode( ',', $raw ) ) )
			)
		);
	}

	/**
	 * @return array<int, int>
	 */
	private static function parseIds( string $raw ): array {
		if ( $raw === '' ) {
			return [];
		}

		return array_values(
			array_filter(
				array_map( 'absint', array_map( 'trim', explode( ',', $raw ) ) )
			)
		);
	}
}
