<?php
/**
 * Postomatic Plugin - Similar Titles Search
 * functionality for the WordPress AJAX API.
 *
 * PHP Version 5.6
 *
 * @category Plugin
 * @package  Postomatic
 * @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;
}

/**
 * Similar Titles Search functionality for the WordPress AJAX API.
 *
 * This class implements a similar titles search endpoint that provides
 * functionality to search for blog posts with similar titles. It uses
 * WordPress AJAX API to handle the search requests.
 *
 * @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 Similar_Titles_Search {

	/**
	 * Initialize the similar titles search functionality.
	 */
	public function __construct() {
		/*
		 * Logged-in only. The `wp_ajax_nopriv_` registration this used to carry
		 * made the title search reachable by anonymous visitors, and the search
		 * reaches into draft and private post titles.
		 */
		add_action( 'wp_ajax_postomatic_similar_titles_search', array( $this, 'search_similar_titles' ) );

		// Add new endpoints for getting posts by title and getting post thumbnails.
		add_action( 'wp_ajax_postomatic_get_post_by_title', array( $this, 'get_post_by_title' ) );
		add_action( 'wp_ajax_postomatic_get_post_thumbnail', array( $this, 'get_post_thumbnail' ) );
	}

	/**
	 * Stop the request unless the current user may edit posts.
	 *
	 * The nonce check that precedes each handler only shows the request came
	 * from one of our pages; it does not establish that the user is allowed to
	 * read the site's post list.
	 *
	 * @return void
	 */
	private function require_edit_posts() {
		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_send_json_error( array( 'message' => 'You do not have permission to perform this action.' ), 403 );
			wp_die();
		}
	}

	/**
	 * Get a post by its title.
	 *
	 * @return void
	 */
	public function get_post_by_title() {
		// Check nonce for security.
		if ( ! check_ajax_referer( 'postomatic_admin_nonce', 'nonce', false ) ) {
			wp_send_json_error( array( 'message' => 'Invalid nonce.' ), 403 );
			wp_die();
		}

		$this->require_edit_posts();

		// Get the post title.
		$post_title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '';

		if ( empty( $post_title ) ) {
			wp_send_json_error( array( 'message' => 'Post title is required' ), 403 );
			wp_die();
		}

		// Search for posts with this exact title.
		$posts = \get_posts(
			array(
				'post_type'      => 'post',
				'post_status'    => 'publish',
				'posts_per_page' => 1,
				'title'          => $post_title,
				'exact'          => true,
			)
		);

		if ( empty( $posts ) ) {
			// If no exact match, try a more flexible search.
			$like      = '%' . $post_title . '%';
			$cache_key = 'postomatic_post_by_title_' . md5( $like );
			$post_id   = wp_cache_get( $cache_key );

			if ( false === $post_id ) {
				// Use WP_Query instead of direct database query.
				$query_args = array(
					'post_type'      => 'post',
					'post_status'    => 'publish',
					'posts_per_page' => 1,
					'orderby'        => 'date',
					'order'          => 'DESC',
					's'              => $post_title,
					'fields'         => 'ids',
				);

				$query = new \WP_Query( $query_args );

				if ( $query->have_posts() ) {
					$post_id = $query->posts[0];
				} else {
					$post_id = 0;
				}

				wp_cache_set( $cache_key, $post_id, '', 3600 ); // Cache for 1 hour.
			}

			if ( ! $post_id ) {
				wp_send_json_error( array( 'message' => 'No post found with this title.' ), 403 );
				wp_die();
			}
		} else {
			$post_id = $posts[0]->ID;
		}

		// Return the post ID.
		wp_send_json_success( array( 'post_id' => $post_id ) );
		wp_die();
	}

	/**
	 * Get the featured image for a post.
	 *
	 * @return void
	 */
	public function get_post_thumbnail() {
		// Check nonce for security.
		if ( ! check_ajax_referer( 'postomatic_admin_nonce', 'nonce', false ) ) {
			wp_send_json_error( array( 'message' => 'Invalid nonce.' ), 403 );
			wp_die();
		}

		$this->require_edit_posts();

		// Get the post ID.
		$post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;

		if ( empty( $post_id ) ) {
			wp_send_json_error( array( 'message' => 'Post ID is required.' ), 403 );
			wp_die();
		}

		// Get the featured image ID.
		$thumbnail_id = \get_post_thumbnail_id( $post_id );

		if ( ! $thumbnail_id ) {
			wp_send_json_error( array( 'message' => 'No featured image found for this post.' ), 403 );
			wp_die();
		}

		// Get the image URL.
		$image_url = \wp_get_attachment_image_url( $thumbnail_id, 'full' );

		if ( ! $image_url ) {
			wp_send_json_error( array( 'message' => 'Failed to get image URL.' ), 403 );
			wp_die();
		}

		// Return the image ID and URL.
		wp_send_json_success(
			array(
				'image_id'  => $thumbnail_id,
				'image_url' => $image_url,
			)
		);
		wp_die();
	}

	/**
	 * Search for blog posts with similar titles.
	 *
	 * @return void
	 */
	public function search_similar_titles() {
		// Check nonce for security.
		if ( ! check_ajax_referer( 'postomatic_admin_nonce', 'nonce', false ) ) {
			wp_send_json_error( array( 'message' => 'Invalid nonce.' ), 403 );
			wp_die();
		}

		$this->require_edit_posts();

		// Get the search query.
		$search_query = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '';

		if ( empty( $search_query ) ) {
			wp_send_json_error( array( 'message' => 'Search title is required.' ), 403 );
			wp_die();
		}

		// Get additional parameters.
		$category         = isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : '';
		$keywords         = isset( $_POST['keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['keywords'] ) ) : '';
		$goal_description = isset( $_POST['goal_description'] ) ? sanitize_text_field( wp_unslash( $_POST['goal_description'] ) ) : '';

		// Search for similar titles.
		$similar_titles = $this->find_similar_titles( $search_query, $category, $keywords, $goal_description );

		// Return the results.
		wp_send_json_success( array( 'similar_titles' => $similar_titles ) );
		wp_die();
	}

	/**
	 * Find blog posts with similar titles.
	 *
	 * This method first tries a simple approach using a direct LIKE query,
	 * and falls back to a more complex multi-stage search algorithm if needed:
	 * 1. First, it splits the title into individual words (terms)
	 * 2. It then searches for posts where the title contains any of these terms
	 * 3. If not enough results are found, it falls back to searching in both title and content
	 * 4. If still not enough results, it searches for each term individually
	 * 5. Results are ranked by relevance, with title matches given higher priority
	 *
	 * @param string $title            The title to search for.
	 * @param string $category         The category to filter by (optional).
	 * @param string $keywords         The keywords to filter by (optional).
	 * @param string $goal_description The goal description to filter by (optional).
	 *
	 * @return array Array of similar titles found.
	 */
	protected function find_similar_titles( $title, $category = '', $keywords = '', $goal_description = '' ) {
		$similar_titles = array();

		// Process search terms from keywords and goal description if provided.
		$search_terms = array();

		if ( ! empty( $keywords ) ) {
			// Split keywords by comma and add to search terms.
			$keyword_terms = array_map( 'trim', explode( ',', $keywords ) );
			$search_terms  = array_merge( $search_terms, $keyword_terms );
		}

		if ( ! empty( $goal_description ) ) {
			// Extract important words from goal description.
			$goal_terms   = preg_split( '/[\s,\.]+/', $goal_description );
			$goal_terms   = array_filter(
				$goal_terms,
				function ( $term ) {
					return mb_strlen( $term, 'UTF-8' ) > 3; // Only include words longer than 3 characters.
				}
			);
			$search_terms = array_merge( $search_terms, $goal_terms );
		}

		// Remove duplicates and empty terms.
		$search_terms = array_unique( array_filter( $search_terms ) );

		// First try the simple approach using a direct LIKE query.
		$posts = $this->find_posts_by_title( $title, array( 'post', 'page' ), 20, $category, $search_terms );

		if ( ! empty( $posts ) ) {
			foreach ( $posts as $post ) {
				$similar_titles[] = array(
					'title'     => $post->post_title,
					'date'      => $post->post_date,
					'published' => ( 'publish' === $post->post_status ),
					'shortlink' => \wp_get_shortlink( $post->ID ),
				);
			}

			// If we found results with the simple approach, return them.
			if ( ! empty( $similar_titles ) ) {
				return $similar_titles;
			}
		}

		// If the simple approach didn't yield results, fall back to the complex implementation.

		// First approach: Split the title into keywords and search for each keyword.
		// Use Unicode-aware pattern for splitting text into terms.
		$terms = preg_split(
			'/[\p{Z}\p{P}]+/u', // Unicode-aware pattern for all separators and punctuation.
			$title,
			-1,
			PREG_SPLIT_NO_EMPTY
		);

		$terms = array_map(
			function ( $term ) {
				// Unicode-aware trimming of punctuation and spaces.
				return preg_replace( '/^[\p{Z}\p{P}]+|[\p{Z}\p{P}]+$/u', '', $term );
			},
			$terms
		);

		// For non-Latin scripts, we need to be more lenient with term filtering.
		// Check if the title contains non-Latin characters.
		$has_non_latin = preg_match( '/[^\p{Latin}\p{N}\p{P}\p{Z}]/u', $title );

		// Filter out empty terms only - no minimum length requirement.
		// to be as inclusive as possible.
		$terms = array_filter(
			$terms,
			function ( $term ) use ( $has_non_latin ) {
				// For non-Latin scripts, accept any non-empty term.
				if ( $has_non_latin ) {
					return ! empty( $term );
				}

				// For Latin scripts, require at least 1 character.
				return mb_strlen( $term, 'UTF-8' ) >= 1; // Accept terms with at least 1 character.
			}
		);

		// If we filtered out too many terms, keep at least the longest ones.
		if ( count( $terms ) < 2 && mb_strlen( $title, 'UTF-8' ) > 10 ) {
			$all_terms = preg_split(
				'/[\p{Z}\p{P}]+/u', // Unicode-aware pattern.
				$title,
				-1,
				PREG_SPLIT_NO_EMPTY
			);

			$all_terms = array_map(
				function ( $term ) {
					// Unicode-aware trimming.
					return preg_replace( '/^[\p{Z}\p{P}]+|[\p{Z}\p{P}]+$/u', '', $term );
				},
				$all_terms
			);

			// Sort by length (longest first) - using mb_strlen for Unicode support.
			usort(
				$all_terms,
				function ( $a, $b ) {
					return mb_strlen( $b, 'UTF-8' ) - mb_strlen( $a, 'UTF-8' );
				}
			);

			// Take the top 3 longest terms.
			$terms = array_slice( $all_terms, 0, 3 );
		}

		// Only search if we have terms.
		if ( ! empty( $terms ) ) {
			// First try to find matches in titles only.
			$title_results = $this->search_posts_by_title_terms( $terms, $title, $has_non_latin );

			if ( ! empty( $title_results ) ) {
				foreach ( $title_results as $post ) {
					$similar_titles[] = array(
						'title'     => $post->post_title,
						'date'      => $post->post_date,
						'published' => ( 'publish' === $post->post_status ),
						'shortlink' => \wp_get_shortlink( $post->ID ),
					);
				}
			}

			// If we don't have enough results from title-only search, try the combined title and content search.
			if ( count( $similar_titles ) < 5 ) {
				$combined_results = $this->search_posts_by_title_and_content( $terms, $title, $has_non_latin );

				if ( ! empty( $combined_results ) ) {
					// Check if this post is already in our results.
					$existing_titles = array_column( $similar_titles, 'title' );

					foreach ( $combined_results as $post ) {
						if ( ! in_array( $post->post_title, $existing_titles, true ) ) {
							$similar_titles[]  = array(
								'title'     => $post->post_title,
								'date'      => $post->post_date,
								'published' => ( 'publish' === $post->post_status ),
								'shortlink' => \wp_get_shortlink( $post->ID ),
							);
							$existing_titles[] = $post->post_title;

							// Limit the total number of results.
							if ( count( $similar_titles ) >= 20 ) {
								break;
							}
						}
					}
				}
			}

			// If we don't have enough results, try a second approach with individual keywords.
			if ( count( $similar_titles ) < 5 && count( $terms ) > 1 ) {
				$individual_results = $this->search_posts_by_individual_terms( $terms, $has_non_latin );

				if ( ! empty( $individual_results ) ) {
					// Check if this post is already in our results.
					$existing_titles = array_column( $similar_titles, 'title' );

					foreach ( $individual_results as $post ) {
						if ( ! in_array( $post->post_title, $existing_titles, true ) ) {
							$similar_titles[]  = array(
								'title'     => $post->post_title,
								'date'      => $post->post_date,
								'published' => ( 'publish' === $post->post_status ),
								'shortlink' => \wp_get_shortlink( $post->ID ),
							);
							$existing_titles[] = $post->post_title;

							// Limit the total number of results.
							if ( count( $similar_titles ) >= 20 ) {
								break;
							}
						}
					}
				}

				// Sort by date after adding individual keyword matches.
				usort(
					$similar_titles,
					function ( $a, $b ) {
						return strcmp( $b['date'], $a['date'] ); // Sort by date in descending order.
					}
				);
			}
		}

		// If we still don't have any results, try one more approach for non-Latin scripts.
		if ( empty( $similar_titles ) && $has_non_latin ) {
			$similar_titles = $this->find_content_matches( $title );
		}

		// If we still don't have any results, fall back to random posts.
		if ( empty( $similar_titles ) ) {
			$similar_titles = $this->get_random_posts( $category );
		}

		return $similar_titles;
	}

	/**
	 * Search for posts by title terms.
	 *
	 * @param array   $terms        The terms to search for.
	 * @param string  $full_title   The full title to search for.
	 * @param boolean $has_non_latin Whether the title contains non-Latin characters.
	 * @return array  Array of post objects.
	 */
	protected function search_posts_by_title_terms( $terms, $full_title, $has_non_latin = false ) {
		// Cache key for this specific query.
		$cache_key = 'postomatic_title_search_' . md5( wp_json_encode( array( $terms, $full_title, $has_non_latin ) ) );
		$results   = wp_cache_get( $cache_key );

		if ( false === $results ) {
			// Add a filter to modify the WHERE clause to search in title.
			add_filter( 'posts_where', array( $this, 'filter_search_by_title_terms' ), 10, 2 );
			add_filter( 'posts_fields', array( $this, 'filter_add_rank_field' ), 10, 2 );
			add_filter( 'posts_orderby', array( $this, 'filter_order_by_rank' ), 10, 2 );

			// Build the query arguments.
			$args = array(
				'post_type'      => 'post',
				'post_status'    => array( 'publish', 'draft', 'pending', 'future', 'private' ),
				'posts_per_page' => 20,
				'search_terms'   => $terms,
				'full_title'     => $full_title,
				'has_non_latin'  => $has_non_latin,
				'search_type'    => 'title_only',
			);

			// Run the query.
			$query = new \WP_Query( $args );

			// Remove the filters after the query.
			remove_filter( 'posts_where', array( $this, 'filter_search_by_title_terms' ), 10 );
			remove_filter( 'posts_fields', array( $this, 'filter_add_rank_field' ), 10 );
			remove_filter( 'posts_orderby', array( $this, 'filter_order_by_rank' ), 10 );

			$results = $query->posts;

			wp_cache_set( $cache_key, $results, '', 3600 ); // Cache for 1 hour.
		}

		return $results;
	}

	/**
	 * Search for posts by title and content.
	 *
	 * @param array   $terms        The terms to search for.
	 * @param string  $full_title   The full title to search for.
	 * @param boolean $has_non_latin Whether the title contains non-Latin characters.
	 * @return array  Array of post objects.
	 */
	protected function search_posts_by_title_and_content( $terms, $full_title, $has_non_latin = false ) {
		// Cache key for this specific query.
		$cache_key = 'postomatic_combined_search_' . md5( wp_json_encode( array( $terms, $full_title, $has_non_latin ) ) );
		$results   = wp_cache_get( $cache_key );

		if ( false === $results ) {
			// Add a filter to modify the WHERE clause to search in title and content.
			add_filter( 'posts_where', array( $this, 'filter_search_by_title_and_content' ), 10, 2 );
			add_filter( 'posts_fields', array( $this, 'filter_add_rank_field' ), 10, 2 );
			add_filter( 'posts_orderby', array( $this, 'filter_order_by_rank' ), 10, 2 );

			// Build the query arguments.
			$args = array(
				'post_type'      => 'post',
				'post_status'    => array( 'publish', 'draft', 'pending', 'future', 'private' ),
				'posts_per_page' => 20,
				'search_terms'   => $terms,
				'full_title'     => $full_title,
				'has_non_latin'  => $has_non_latin,
				'search_type'    => 'title_and_content',
			);

			// Run the query.
			$query = new \WP_Query( $args );

			// Remove the filters after the query.
			remove_filter( 'posts_where', array( $this, 'filter_search_by_title_and_content' ), 10 );
			remove_filter( 'posts_fields', array( $this, 'filter_add_rank_field' ), 10 );
			remove_filter( 'posts_orderby', array( $this, 'filter_order_by_rank' ), 10 );

			$results = $query->posts;

			wp_cache_set( $cache_key, $results, '', 3600 ); // Cache for 1 hour.
		}

		return $results;
	}

	/**
	 * Search for posts by individual terms.
	 *
	 * @param array   $terms        The terms to search for.
	 * @param boolean $has_non_latin Whether the title contains non-Latin characters.
	 * @return array  Array of post objects.
	 */
	protected function search_posts_by_individual_terms( $terms, $has_non_latin = false ) {
		$all_results  = array();
		$existing_ids = array();

		foreach ( $terms as $term ) {
			// For non-Latin scripts, we need to be more aggressive with matching.
			// and accept any non-empty term.
			if ( ( $has_non_latin && ! empty( $term ) ) || mb_strlen( $term, 'UTF-8' ) >= 1 ) {
				// Cache key for this specific term query.
				$cache_key    = 'postomatic_individual_term_search_' . md5( $term . '_' . $has_non_latin );
				$term_results = wp_cache_get( $cache_key );

				if ( false === $term_results ) {
					// First try to find matches in titles only.
					$title_args = array(
						'post_type'      => 'post',
						'post_status'    => array( 'publish', 'draft', 'pending', 'future', 'private' ),
						'posts_per_page' => $has_non_latin ? 15 : 10,
						'orderby'        => 'date',
						'order'          => 'DESC',
						'search_title'   => $term,
					);

					// Add a filter to modify the WHERE clause to search in title.
					add_filter( 'posts_where', array( $this, 'filter_search_by_title' ), 10, 2 );
					add_filter( 'posts_fields', array( $this, 'filter_add_term_rank_field' ), 10, 2 );

					// Run the query.
					$title_query = new \WP_Query( $title_args );

					// Remove the filters after the query.
					remove_filter( 'posts_where', array( $this, 'filter_search_by_title' ), 10 );
					remove_filter( 'posts_fields', array( $this, 'filter_add_term_rank_field' ), 10 );

					$title_results = $title_query->posts;

					// If we found results in titles, use those.
					if ( ! empty( $title_results ) ) {
						$term_results = $title_results;
					} else {
						// Otherwise, fall back to searching in both title and content.
						add_filter( 'posts_where', array( $this, 'filter_search_by_title_or_content' ), 10, 2 );
						add_filter( 'posts_fields', array( $this, 'filter_add_term_rank_field' ), 10, 2 );
						add_filter( 'posts_orderby', array( $this, 'filter_order_by_rank' ), 10, 2 );

						// Build the query arguments.
						$combined_args = array(
							'post_type'      => 'post',
							'post_status'    => array( 'publish', 'draft', 'pending', 'future', 'private' ),
							'posts_per_page' => $has_non_latin ? 15 : 10,
							'search_term'    => $term,
							'has_non_latin'  => $has_non_latin,
						);

						// Run the query.
						$combined_query = new \WP_Query( $combined_args );

						// Remove the filters after the query.
						remove_filter( 'posts_where', array( $this, 'filter_search_by_title_or_content' ), 10 );
						remove_filter( 'posts_fields', array( $this, 'filter_add_term_rank_field' ), 10 );
						remove_filter( 'posts_orderby', array( $this, 'filter_order_by_rank' ), 10 );

						$term_results = $combined_query->posts;
					}

					wp_cache_set( $cache_key, $term_results, '', 3600 ); // Cache for 1 hour.
				}

				// Add unique results to our collection.
				if ( ! empty( $term_results ) ) {
					foreach ( $term_results as $post ) {
						if ( ! in_array( $post->ID, $existing_ids, true ) ) {
							$all_results[]  = $post;
							$existing_ids[] = $post->ID;

							// Limit the total number of results.
							if ( count( $all_results ) >= 20 ) {
								break 2; // Break out of both loops.
							}
						}
					}
				}
			}
		}

		return $all_results;
	}

	/**
	 * Filter to add a rank field for title terms search.
	 *
	 * @param string   $fields The SELECT clause of the query.
	 * @param WP_Query $query  The WP_Query instance.
	 * @return string Modified SELECT clause.
	 */
	public function filter_add_rank_field( $fields, $query ) {
		global $wpdb;

		$search_type   = $query->get( 'search_type' );
		$terms         = $query->get( 'search_terms' );
		$full_title    = $query->get( 'full_title' );
		$has_non_latin = $query->get( 'has_non_latin' );

		if ( ! empty( $terms ) && ! empty( $full_title ) ) {
			$rank_formula = array();

			// Add rank formula for each term.
			foreach ( $terms as $term ) {
				if ( 'title_only' === $search_type ) {
					// For title-only search.
					$weight         = $has_non_latin ? 4 : 3;
					$rank_formula[] = $wpdb->prepare(
						"IF(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s), %d, 0)",
						'%' . $wpdb->esc_like( $term ) . '%',
						$weight
					);
				} elseif ( $has_non_latin ) {
					// For title and content search.
					$rank_formula[] = $wpdb->prepare(
						"IF(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s), %d, IF(LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s), %d, 0))",
						'%' . $wpdb->esc_like( $term ) . '%',
						'%' . $wpdb->esc_like( $term ) . '%',
						4,
						1
					);
				} else {
					$rank_formula[] = $wpdb->prepare(
						"IF(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s), %d, IF(LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s), %f, 0))",
						'%' . $wpdb->esc_like( $term ) . '%',
						'%' . $wpdb->esc_like( $term ) . '%',
						3,
						0.5
					);
				}
			}

			// Add rank formula for full title.
			if ( 'title_only' === $search_type ) {
				$rank_formula[] = $wpdb->prepare(
					"IF(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s), %d, 0)",
					'%' . $wpdb->esc_like( $full_title ) . '%',
					2
				);
			} elseif ( $has_non_latin ) {
				$rank_formula[] = $wpdb->prepare(
					"IF(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s), %d, IF(LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s), %f, 0))",
					'%' . $wpdb->esc_like( $full_title ) . '%',
					'%' . $wpdb->esc_like( $full_title ) . '%',
					2,
					0.5
				);
			} else {
				$rank_formula[] = $wpdb->prepare(
					"IF(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s), %d, IF(LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s), %f, 0))",
					'%' . $wpdb->esc_like( $full_title ) . '%',
					'%' . $wpdb->esc_like( $full_title ) . '%',
					1,
					0.2
				);
			}

			// Add the rank formula to the fields.
			if ( ! empty( $rank_formula ) ) {
				$fields .= ', (' . implode( ' + ', $rank_formula ) . ') as rank_score';
			}
		}

		return $fields;
	}

	/**
	 * Filter to add a rank field for individual term search.
	 *
	 * @param string   $fields The SELECT clause of the query.
	 * @param WP_Query $query  The WP_Query instance.
	 * @return string Modified SELECT clause.
	 */
	public function filter_add_term_rank_field( $fields, $query ) {
		global $wpdb;

		$term = empty( $query->get( 'search_term' ) ) ? $query->get( 'search_term' ) : false;
		if ( $term ) {
			$has_non_latin  = $query->get( 'has_non_latin' );
			$weight_title   = $has_non_latin ? 4 : 3;
			$weight_content = $has_non_latin ? 1 : 0.5;

			$fields .= $wpdb->prepare(
				", IF(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s), %d, IF(LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s), %f, 0)) as rank_score",
				'%' . $wpdb->esc_like( $term ) . '%',
				'%' . $wpdb->esc_like( $term ) . '%',
				$weight_title,
				$weight_content
			);
		}

		return $fields;
	}

	/**
	 * Filter the WHERE clause to search for terms in post title.
	 *
	 * @param string   $where The WHERE clause of the query.
	 * @param WP_Query $query The WP_Query instance.
	 * @return string Modified WHERE clause.
	 */
	public function filter_search_by_title_terms( $where, $query ) {
		global $wpdb;

		$terms = empty( $query->get( 'search_term' ) ) ? $query->get( 'search_term' ) : false;
		if ( $terms ) {
			$full_title   = $query->get( 'full_title' );
			$like_clauses = array();

			// Add clauses for each term.
			foreach ( $terms as $term ) {
				$like_clauses[] = $wpdb->prepare( "LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s)", '%' . $wpdb->esc_like( $term ) . '%' );
			}

			// Add clause for full title.
			if ( ! empty( $full_title ) ) {
				$like_clauses[] = $wpdb->prepare( "LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s)", '%' . $wpdb->esc_like( $full_title ) . '%' );
			}

			if ( ! empty( $like_clauses ) ) {
				$where .= ' AND (' . implode( ' OR ', $like_clauses ) . ')';
			}
		}

		return $where;
	}

	/**
	 * Filter the WHERE clause to search for terms in post title and content.
	 *
	 * @param string   $where The WHERE clause of the query.
	 * @param WP_Query $query The WP_Query instance.
	 * @return string Modified WHERE clause.
	 */
	public function filter_search_by_title_and_content( $where, $query ) {
		global $wpdb;

		$terms = empty( $query->get( 'search_term' ) ) ? $query->get( 'search_term' ) : false;
		if ( $terms ) {
			$full_title   = $query->get( 'full_title' );
			$like_clauses = array();

			// Add clauses for each term.
			foreach ( $terms as $term ) {
				$like_clauses[] = $wpdb->prepare(
					"(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s) OR LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s))",
					'%' . $wpdb->esc_like( $term ) . '%',
					'%' . $wpdb->esc_like( $term ) . '%'
				);
			}

			// Add clause for full title.
			if ( ! empty( $full_title ) ) {
				$like_clauses[] = $wpdb->prepare(
					"(LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s) OR LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s))",
					'%' . $wpdb->esc_like( $full_title ) . '%',
					'%' . $wpdb->esc_like( $full_title ) . '%'
				);
			}

			if ( ! empty( $like_clauses ) ) {
				$where .= ' AND (' . implode( ' OR ', $like_clauses ) . ')';
			}
		}

		return $where;
	}

	/**
	 * Filter the WHERE clause to search for a term in post title or content.
	 *
	 * @param string   $where The WHERE clause of the query.
	 * @param WP_Query $query The WP_Query instance.
	 * @return string Modified WHERE clause.
	 */
	public function filter_search_by_title_or_content( $where, $query ) {
		global $wpdb;

		$term = empty( $query->get( 'search_term' ) ) ? $query->get( 'search_term' ) : false;
		if ( $term ) {
			$where .= $wpdb->prepare(
				" AND (LOWER({$wpdb->posts}.post_title) LIKE LOWER(%s) OR LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s))",
				'%' . $wpdb->esc_like( $term ) . '%',
				'%' . $wpdb->esc_like( $term ) . '%'
			);
		}

		return $where;
	}

	/**
	 * Filter to order results by rank score.
	 *
	 * @param string   $orderby The ORDER BY clause of the query.
	 * @param WP_Query $query   The WP_Query instance.
	 * @return string Modified ORDER BY clause.
	 */
	public function filter_order_by_rank( $orderby, $query ) {
		if ( $query->get( 'search_terms' ) || $query->get( 'search_term' ) ) {
			$orderby = "rank_score DESC, {$orderby}";
		}

		return $orderby;
	}

	/**
	 * Find posts with similar content for non-Latin scripts.
	 *
	 * This is a specialized search method for non-Latin scripts that focuses on
	 * finding posts with similar content even if the titles don't match.
	 *
	 * @param  string $title The title to search for.
	 * @return array  Array of posts with similar content.
	 */
	protected function find_content_matches( $title ) {
		$content_matches = array();

		// Extract terms from the title.
		$terms = preg_split(
			'/[\p{Z}\p{P}]+/u', // Unicode-aware pattern.
			$title,
			-1,
			PREG_SPLIT_NO_EMPTY
		);

		$terms = array_map(
			function ( $term ) {
				return preg_replace( '/^[\p{Z}\p{P}]+|[\p{Z}\p{P}]+$/u', '', $term );
			},
			$terms
		);

		// Filter out empty terms.
		$terms = array_filter(
			$terms,
			function ( $term ) {
				return ! empty( $term );
			}
		);

		if ( empty( $terms ) ) {
			return array();
		}

		// Cache key for this specific content query.
		$content_cache_key = 'postomatic_content_search_' . md5( wp_json_encode( $terms ) );
		$results           = wp_cache_get( $content_cache_key );

		if ( false === $results ) {
			// We'll use a custom filter to search in content.
			add_filter( 'posts_where', array( $this, 'filter_search_by_content_terms' ), 10, 2 );
			add_filter( 'posts_groupby', array( $this, 'filter_group_by_id' ), 10, 2 );
			add_filter( 'posts_orderby', array( $this, 'filter_order_by_match_count' ), 10, 2 );

			// Build the query arguments.
			$args = array(
				'post_type'      => 'post',
				'post_status'    => array( 'publish', 'draft', 'pending', 'future', 'private' ),
				'posts_per_page' => 20,
				'content_terms'  => $terms, // Custom parameter for our filter.
			);

			// Run the query.
			$query = new \WP_Query( $args );

			// Remove the filters after the query.
			remove_filter( 'posts_where', array( $this, 'filter_search_by_content_terms' ), 10 );
			remove_filter( 'posts_groupby', array( $this, 'filter_group_by_id' ), 10 );
			remove_filter( 'posts_orderby', array( $this, 'filter_order_by_match_count' ), 10 );

			$results = $query->posts;

			wp_cache_set( $content_cache_key, $results, '', 3600 ); // Cache for 1 hour.
		}

		if ( ! empty( $results ) ) {
			foreach ( $results as $post ) {
				$content_matches[] = array(
					'title'     => $post->post_title,
					'date'      => $post->post_date,
					'published' => ( 'publish' === $post->post_status ),
					'shortlink' => \wp_get_shortlink( $post->ID ),
				);
			}
		}

		return $content_matches;
	}

	/**
	 * Filter the WHERE clause to search for terms in post content.
	 *
	 * @param string   $where The WHERE clause of the query.
	 * @param WP_Query $query The WP_Query instance.
	 * @return string Modified WHERE clause.
	 */
	public function filter_search_by_content_terms( $where, $query ) {
		global $wpdb;

		$terms = empty( $query->get( 'content_terms' ) ) ? $query->get( 'content_terms' ) : false;
		if ( $terms ) {
			$like_clauses = array();

			foreach ( $terms as $term ) {
				if ( ! empty( $term ) ) {
					$like_clauses[] = $wpdb->prepare( "LOWER({$wpdb->posts}.post_content) LIKE LOWER(%s)", '%' . $wpdb->esc_like( $term ) . '%' );
				}
			}

			if ( ! empty( $like_clauses ) ) {
				$where .= ' AND (' . implode( ' OR ', $like_clauses ) . ')';

				// Add a field to count matches for ordering.
				$query->query_vars['_match_count_sql'] = '(' . implode(
					' + ',
					array_map(
						function ( $clause ) {
							return "IF($clause, 1, 0)";
						},
						$like_clauses
					)
				) . ') as match_count';
			}
		}

		return $where;
	}

	/**
	 * Filter to add GROUP BY clause for content search.
	 *
	 * @param string   $groupby The GROUP BY clause of the query.
	 * @param WP_Query $query   The WP_Query instance.
	 * @return string Modified GROUP BY clause.
	 */
	public function filter_group_by_id( $groupby, $query ) {
		global $wpdb;

		if ( $query->get( 'content_terms' ) ) {
			$groupby = "{$wpdb->posts}.ID";
		}

		return $groupby;
	}

	/**
	 * Filter to order results by match count for content search.
	 *
	 * @param string   $orderby The ORDER BY clause of the query.
	 * @param WP_Query $query   The WP_Query instance.
	 * @return string Modified ORDER BY clause.
	 */
	public function filter_order_by_match_count( $orderby, $query ) {
		if ( isset( $query->query_vars['_match_count_sql'] ) ) {
			$orderby = "match_count DESC, {$orderby}";
		}

		return $orderby;
	}

	/**
	 * Get random posts as a fallback when no similar titles are found.
	 *
	 * @param  string $category The category to filter by (optional).
	 * @return array  Array of random posts.
	 */
	protected function get_random_posts( $category = '' ) {
		$random_posts = array();

		// Cache key for random posts query.
		$random_cache_key = 'postomatic_random_posts_' . md5( 'random_' . $category );
		$results          = wp_cache_get( $random_cache_key );

		if ( false === $results ) {
			// Build the WP_Query arguments.
			$args = array(
				'post_type'      => 'post',
				'post_status'    => array( 'publish', 'draft', 'pending', 'future', 'private' ),
				'posts_per_page' => 20,
				'orderby'        => 'rand',
			);

			// Add category filter if provided.
			if ( ! empty( $category ) ) {
				// Try to get category by slug first.
				$cat = \get_category_by_slug( $category );

				// If not found, try by name.
				if ( ! $cat ) {
					$cat = \get_category_by_name( $category );
				}

				// If we found a category, add it to the query.
				if ( $cat ) {
					$args['cat'] = $cat->term_id;
				}
			}

			// Run the query.
			$query   = new \WP_Query( $args );
			$results = $query->posts;

			wp_cache_set( $random_cache_key, $results, '', 3600 ); // Cache for 1 hour.
		}

		if ( ! empty( $results ) ) {
			foreach ( $results as $post ) {
				$random_posts[] = array(
					'title'     => $post->post_title,
					'date'      => $post->post_date,
					'published' => ( 'publish' === $post->post_status ),
					'shortlink' => \wp_get_shortlink( $post->ID ),
				);
			}
		}

		return $random_posts;
	}

	/**
	 * Find posts by title using a simple LIKE query.
	 *
	 * @param string $search_title The title to search for.
	 * @param array  $post_types   The post types to search in. Default is array('post').
	 * @param int    $limit        The maximum number of posts to return. Default is 20.
	 * @param string $category     The category to filter by (optional).
	 * @param array  $search_terms Additional search terms to filter by (optional).
	 *
	 * @return array Array of post objects.
	 */
	protected function find_posts_by_title( $search_title, $post_types = array( 'post' ), $limit = 20, $category = '', $search_terms = array() ) {
		if ( empty( $search_title ) ) {
			return array();
		}

		// Cache key for this specific posts by title query.
		$cache_key = 'postomatic_posts_by_title_' . md5( $search_title . wp_json_encode( $post_types ) . $limit . $category . wp_json_encode( $search_terms ) );
		$posts     = wp_cache_get( $cache_key );

		if ( false === $posts ) {
			// Build the WP_Query arguments.
			$args = array(
				'post_type'      => $post_types,
				'post_status'    => array( 'publish', 'draft', 'pending', 'future', 'private' ),
				'posts_per_page' => $limit,
				'orderby'        => 'date',
				'order'          => 'DESC',
				'search_title'   => $search_title, // Custom parameter for our filter.
			);

			// Add category filter if provided.
			if ( ! empty( $category ) ) {
				$args['category_name'] = $category;
			}

			// Add a filter to modify the WHERE clause to search in title.
			add_filter( 'posts_where', array( $this, 'filter_search_by_title' ), 10, 2 );

			// Run the query.
			$query = new \WP_Query( $args );

			// Remove the filter after the query.
			remove_filter( 'posts_where', array( $this, 'filter_search_by_title' ), 10 );

			$posts = $query->posts;

			// If we have search terms, filter the posts further.
			if ( ! empty( $search_terms ) && ! empty( $posts ) ) {
				$filtered_posts = array();
				foreach ( $posts as $post ) {
					$match = false;

					// Check if any search term is in the title or content.
					foreach ( $search_terms as $term ) {
						if ( false !== stripos( $post->post_title, $term ) || false !== stripos( $post->post_content, $term ) ) {
							$match = true;
							break;
						}
					}

					if ( $match ) {
						$filtered_posts[] = $post;
					}
				}

				// If we have filtered posts, use them, otherwise use the original posts.
				if ( ! empty( $filtered_posts ) ) {
					$posts = $filtered_posts;
				}
			}

			wp_cache_set( $cache_key, $posts, '', 3600 ); // Cache for 1 hour.
		}

		return $posts;
	}

	/**
	 * Filter the WHERE clause to search in post title.
	 *
	 * @param string   $where The WHERE clause of the query.
	 * @param WP_Query $query The WP_Query instance.
	 * @return string Modified WHERE clause.
	 */
	public function filter_search_by_title( $where, $query ) {
		global $wpdb;

		$search_title = $query->get( 'search_title' );
		if ( $search_title ) {
			$where .= $wpdb->prepare( " AND {$wpdb->posts}.post_title LIKE %s", '%' . $wpdb->esc_like( $search_title ) . '%' );
		}

		return $where;
	}

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