<?php
/**
 * QueryBuilder
 *
 * Builds and executes WP_Query calls for related posts.
 * Never uses ORDER BY RAND() — that was the primary performance problem in v1.9.x.
 *
 * Strategy:
 *  1. Query a pool of 30 posts ordered by date DESC (uses the post_date index).
 *  2. Cache the pool per post + context with a configurable TTL.
 *  3. On each render, shuffle the cached pool in PHP and slice to the needed count.
 *
 * Result: 1 indexed DB query per post (cached), zero filesorts, fast on any site size.
 *
 * @package Relatedly\Core
 * @since   2.0.0
 */

namespace Relatedly\Core;

use Relatedly\Premium\FeatureGate;

defined( 'ABSPATH' ) || exit;

/**
 * Fetches related posts using indexed date-ordered pools.
 *
 * @since 2.0.0
 */
class QueryBuilder {

	/** How many posts to fetch into the pool. Always more than the display limit. */
	const POOL_SIZE = 30;

	/**
	 * Plugin settings repository.
	 *
	 * @since 2.0.0
	 * @var Options
	 */
	private Options $options;

	/**
	 * Cache service for related post pools.
	 *
	 * @since 2.0.0
	 * @var Cache
	 */
	private Cache $cache;

	/**
	 * Create the query builder.
	 *
	 * @since 2.0.0
	 * @param Options $options Plugin settings repository.
	 * @param Cache   $cache   Cache service for related post pools.
	 */
	public function __construct( Options $options, Cache $cache ) {
		$this->options = $options;
		$this->cache   = $cache;
	}


	// -------------------------------------------------------------------------
	// Public API
	// -------------------------------------------------------------------------

	/**
	 * Get related posts for a given post.
	 *
	 * @param int    $post_id Source post ID.
	 * @param int    $limit   Number of posts to return.
	 * @param string $context 'inline' | 'end_content' | 'rss' | 'rss_end' | 'floating'.
	 * @return \WP_Post[]
	 */
	public function get_related_posts( int $post_id, int $limit, string $context ): array {
		if ( ! $this->options->get( 'enable_cache', true ) ) {
			return $this->fetch_and_shuffle( $post_id, $limit );
		}

		$cache_key = "pool_{$post_id}_{$context}";
		$pool      = $this->cache->get( $cache_key );

		if ( false === $pool ) {
			$pool = $this->fetch_pool( $post_id );
			$ttl  = (int) $this->options->get( 'cache_duration', HOUR_IN_SECONDS );
			$this->cache->set( $cache_key, $pool, $ttl );
		}

		// Shuffle in PHP — fast, no DB cost, gives variety on each page load.
		shuffle( $pool );
		return array_slice( $pool, 0, $limit );
	}


	// -------------------------------------------------------------------------
	// Internal — pool fetching
	// -------------------------------------------------------------------------

	/**
	 * Fetch a pool without caching and return a shuffled slice.
	 *
	 * @param int $post_id Source post ID.
	 * @param int $limit   Number of posts to return.
	 * @return \WP_Post[]
	 */
	private function fetch_and_shuffle( int $post_id, int $limit ): array {
		$pool = $this->fetch_pool( $post_id );
		shuffle( $pool );
		return array_slice( $pool, 0, $limit );
	}

	/**
	 * Route to the correct query method based on the related_method option.
	 *
	 * @param int $post_id Source post ID.
	 * @return \WP_Post[]
	 */
	private function fetch_pool( int $post_id ): array {
		switch ( $this->options->get( 'related_method', 'both' ) ) {
			case 'category':
				return $this->query_by_categories( $post_id );
			case 'tags':
				return $this->query_by_tags( $post_id );
			default:
				return $this->query_by_both( $post_id );
		}
	}

	/**
	 * Base WP_Query args shared by all three query methods.
	 * orderby => date uses the post_date index — no filesort.
	 *
	 * @param int $post_id Source post ID.
	 * @return array<string,mixed>
	 */
	private function base_args( int $post_id ): array {
		$requested_types = (array) $this->options->get( 'post_types', [ 'post' ] );
		$allowed_types   = FeatureGate::allowed_post_types( $requested_types );

		$args = [
			'post_type'           => $allowed_types,
			'posts_per_page'      => self::POOL_SIZE,
			// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
			'post__not_in'        => [ $post_id ],
			'orderby'             => 'date',
			'order'               => 'DESC',
			'ignore_sticky_posts' => true,
			'no_found_rows'       => true, // Skip SQL_CALC_FOUND_ROWS — we don't need pagination.
		];

		$exclude_cats = (array) $this->options->get( 'exclude_categories', [] );
		$exclude_tags = (array) $this->options->get( 'exclude_tags', [] );

		if ( ! empty( $exclude_cats ) ) {
			$args['category__not_in'] = $exclude_cats;
		}
		if ( ! empty( $exclude_tags ) ) {
			$args['tag__not_in'] = $exclude_tags;
		}

		return $args;
	}


	// -------------------------------------------------------------------------
	// Internal — query methods
	// -------------------------------------------------------------------------

	/**
	 * Query posts that share categories with the source post.
	 *
	 * @param int $post_id Source post ID.
	 * @return \WP_Post[]
	 */
	private function query_by_categories( int $post_id ): array {
		$cats = wp_get_post_categories( $post_id );
		if ( empty( $cats ) ) {
			return [];
		}
		$args                 = $this->base_args( $post_id );
		$args['category__in'] = $cats;
		return ( new \WP_Query( $args ) )->posts;
	}

	/**
	 * Query posts that share tags with the source post.
	 *
	 * @param int $post_id Source post ID.
	 * @return \WP_Post[]
	 */
	private function query_by_tags( int $post_id ): array {
		$tags = wp_get_post_tags( $post_id, [ 'fields' => 'ids' ] );
		if ( empty( $tags ) ) {
			return [];
		}
		$args            = $this->base_args( $post_id );
		$args['tag__in'] = $tags;
		return ( new \WP_Query( $args ) )->posts;
	}

	/**
	 * Query posts that share categories or tags with the source post.
	 *
	 * @param int $post_id Source post ID.
	 * @return \WP_Post[]
	 */
	private function query_by_both( int $post_id ): array {
		$cats = wp_get_post_categories( $post_id );
		$tags = wp_get_post_tags( $post_id, [ 'fields' => 'ids' ] );

		if ( empty( $cats ) && empty( $tags ) ) {
			return [];
		}

		$tax_query = [ 'relation' => 'OR' ];

		if ( ! empty( $cats ) ) {
			$tax_query[] = [
				'taxonomy' => 'category',
				'field'    => 'term_id',
				'terms'    => $cats,
			];
		}

		if ( ! empty( $tags ) ) {
			$tax_query[] = [
				'taxonomy' => 'post_tag',
				'field'    => 'term_id',
				'terms'    => $tags,
			];
		}

		$args = $this->base_args( $post_id );
		// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
		$args['tax_query'] = $tax_query;

		return ( new \WP_Query( $args ) )->posts;
	}
}
