<?php
/**
 * Postomatic Plugin - Custom search
 * functionality for the REST API.
 *
 *  PHP Version 5.6
 *
 * @package  Postomatic
 * @category Plugin
 * @license  GPL-2.0-or-later https://www.gnu.org/licenses/gpl-2.0.html
 * @link     https://buildomatics.com/postomatics
 * @since    1.0.0
 */

namespace Postomatic\Api;

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


/**
 * Custom search functionality for the WordPress REST API.
 *
 * This class implements a custom search endpoint that provides enhanced search
 * capabilities through the WordPress REST API. It supports exact phrase matching,
 * fallback to OR-based term searching, and defaults to recent posts when no
 * results are found.
 *
 * The endpoint is registered at 'custom-search/v1/posts' and requires edit_posts
 * capability to access. The search implementation includes:
 * - Exact phrase matching
 * - Individual term matching with OR logic
 * - Fallback to recent posts when no matches are found
 *
 * @category Plugin
 * @license  GPL-2.0-or-later https://www.gnu.org/licenses/gpl-2.0.html
 * @link     https://buildomatics.com/postomatics
 * @since    1.0.0
 */
class Custom_Search {

	/**
	 * Initialize the custom search functionality.
	 */
	public function __construct() {
		add_action(
			'rest_api_init',
			array( $this, 'register_custom_search_endpoint' )
		);
	}

	/**
	 * Register REST route for custom search.
	 *
	 * @return void
	 */
	public function register_custom_search_endpoint() {
		register_rest_route(
			'custom-search/v1',
			'/posts',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_searched_posts' ),
				'permission_callback' => array( $this, 'custom_search_permission' ),
			)
		);
	}

	/**
	 * Permission callback to restrict access to certain users.
	 *
	 * @return bool Whether the user has permission to access the endpoint.
	 */
	public function custom_search_permission() {
		return current_user_can( 'edit_posts' );
	}

	/**
	 * Callback function to handle the GET request and perform custom search.
	 *
	 * @param \WP_REST_Request $request The REST request object.
	 *
	 * @throws \WP_Error         If search term is not provided.
	 * @return \WP_REST_Response The search results.
	 */
	public function get_searched_posts( \WP_REST_Request $request ) {
		$search_query = $request->get_param( 'search' );
		$min_results  = 5; // Define minimum number of results.

		if ( empty( $search_query ) ) {
			return new \WP_Error(
				'no_search_term',
				'Search term is required',
				array( 'status' => 400 )
			);
		}

		// Sanitize the search query.
		$search_query  = sanitize_text_field( $search_query );
		$related_posts = array();

		// Attempt exact phrase search across all post types.
		$args = array(
			's'              => $search_query,
			'post_type'      => 'any', // Search all post types.
			'posts_per_page' => 10,
			'post_status'    => 'publish',
			'orderby'        => 'relevance',
			'order'          => 'DESC',
		);

		$query = new \WP_Query( $args );

		if ( $query->have_posts() ) {
			while ( $query->have_posts() ) {
				$query->the_post();
				$related_posts[] = array(
					'title'     => get_the_title(),
					'link'      => get_permalink(),
					'post_type' => get_post_type(),
				);
			}
			wp_reset_postdata();
		}

		// If we don't have enough results, try fallback search.
		if ( count( $related_posts ) < $min_results ) {
			$fallback_results = $this->fallback_search( $search_query );

			// Merge results, avoiding duplicates.
			if ( ! empty( $fallback_results ) ) {
				$existing_links = array_column( $related_posts, 'link' );
				foreach ( $fallback_results as $result ) {
					if ( ! in_array( $result['link'], $existing_links, true ) ) {
						$related_posts[]  = $result;
						$existing_links[] = $result['link'];
					}
				}
			}
		}

		// Still don't have enough results? Get recent posts from all post types.
		if ( count( $related_posts ) < $min_results ) {
			$recent_posts = $this->get_recent_posts();

			// Merge results, avoiding duplicates.
			if ( ! empty( $recent_posts ) ) {
				$existing_links = array_column( $related_posts, 'link' );
				foreach ( $recent_posts as $post ) {
					if ( ! in_array( $post['link'], $existing_links, true ) ) {
						$related_posts[]  = $post;
						$existing_links[] = $post['link'];

						// Stop adding once we reach the minimum.
						if ( count( $related_posts ) >= $min_results ) {
							break;
						}
					}
				}
			}
		}

		return rest_ensure_response( $related_posts );
	}

	/**
	 * Handle fallback search with individual terms and OR logic.
	 *
	 * @param string $search_query The original search query string.
	 *
	 * @return array Array of related posts found.
	 */
	protected function fallback_search( $search_query ) {
		global $wpdb;

		$related_posts = array();
		$terms         = preg_split(
			'/[\s\-,;]+/',
			$search_query,
			-1,
			PREG_SPLIT_NO_EMPTY
		);

		$terms = array_map(
			function ( $term ) {
				return trim( $term, " .!?\"'(){}[]<>" );
			},
			$terms
		);
		$terms = array_filter( $terms );

		if ( ! empty( $terms ) ) {
			// Add OR search filter.
			add_filter( 'posts_search', array( $this, 'custom_or_search' ), 10, 2 );

			$args = array(
				's'              => implode( ' ', $terms ),
				'post_type'      => 'any', // Search all post types.
				'posts_per_page' => 10,
				'post_status'    => 'publish',
				'orderby'        => 'relevance',
				'order'          => 'DESC',
			);

			$query = new \WP_Query( $args );
			remove_filter( 'posts_search', array( $this, 'custom_or_search' ), 10 );

			if ( $query->have_posts() ) {
				while ( $query->have_posts() ) {
					$query->the_post();
					$related_posts[] = array(
						'title'     => get_the_title(),
						'link'      => get_permalink(),
						'post_type' => get_post_type(),
					);
				}
				wp_reset_postdata();
			}
		}

		return $related_posts;
	}

	/**
	 * Retrieve recent posts if no search results are found.
	 *
	 * @return array Array of recent posts.
	 */
	protected function get_recent_posts() {
		$related_posts = array();
		$args          = array(
			'post_type'      => 'any', // Get all post types.
			'posts_per_page' => 10,
			'post_status'    => 'publish',
			'orderby'        => 'date',
			'order'          => 'DESC',
		);

		$query = new \WP_Query( $args );

		if ( $query->have_posts() ) {
			while ( $query->have_posts() ) {
				$query->the_post();
				$related_posts[] = array(
					'title'     => get_the_title(),
					'link'      => get_permalink(),
					'post_type' => get_post_type(),
				);
			}
			wp_reset_postdata();
		}

		return $related_posts;
	}

	/**
	 * Handle custom OR search logic for WordPress queries.
	 *
	 * @param string    $search   Current search SQL for WHERE clause.
	 * @param \WP_Query $wp_query The current WP_Query object.
	 *
	 * @return string Modified search SQL for WHERE clause.
	 */
	public function custom_or_search( $search, $wp_query ) {
		global $wpdb;

		if ( ! empty( $wp_query->query_vars['s'] ) ) {
			$search_terms = $wp_query->query_vars['search_terms'];
			$search       = '';

			foreach ( $search_terms as $term ) {
				$like    = '%' . $wpdb->esc_like( $term ) . '%';
				$search .= $wpdb->prepare(
					" OR ({$wpdb->posts}.post_title LIKE %s OR 
                    {$wpdb->posts}.post_content LIKE %s)",
					$like,
					$like
				);
			}

			if ( $search ) {
				$search = " AND ({$wpdb->posts}.post_status = 'publish' AND 
                (1=1" . $search . ')) ';
			}
		}

		return $search;
	}


	/**
	 * Initialize the Custom_Search class.
	 *
	 * @return void
	 */
	public static function init() {
		new self();
	}
}
