<?php
/**
 * Integrate with WP_Comment_Query
 *
 * @since   3.6.0
 * @package elasticprobe
 */

namespace ElasticProbe\Indexable\Comment;

use WP_Comment_Query;
use ElasticProbe\Indexables;
use ElasticProbe\Utils;

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

/**
 * Query integration class
 */
class QueryIntegration {

	/**
	 * Comment indexable
	 *
	 * @var Comment
	 */
	public $indexable = '';

	/**
	 * Index name
	 *
	 * @var string
	 */
	public $index = '';

	/**
	 * Sets up the appropriate actions and filters.
	 *
	 * @param string $indexable_slug Indexable slug. Optional.
	 *
	 * @since 3.6.0
	 */
	public function __construct( $indexable_slug = 'comment' ) {
		/**
		 * Filter whether to enable query integration during indexing
		 *
		 * @since 4.5.2
		 * @hook eprobe_enable_query_integration_during_indexing
		 *
		 * @param {bool} $enable To allow query integration during indexing
		 * @param {string} $indexable_slug Indexable slug
		 * @return {bool} New value
		 */
		$allow_query_integration_during_indexing = apply_filters( 'eprobe_enable_query_integration_during_indexing', false, $indexable_slug );

		// Check if we are currently indexing
		if ( Utils\is_indexing() && ! $allow_query_integration_during_indexing ) {
			return;
		}

		// Add header
		add_action( 'pre_get_comments', array( $this, 'action_pre_get_comments' ), 5 );

		// Filter comment query
		add_filter( 'comments_pre_query', [ $this, 'maybe_filter_query' ], 10, 2 );
	}

	/**
	 * Add EP header
	 *
	 * @param  WP_Comment_Query $query Query object
	 * @since  3.6.0
	 * @return void
	 */
	public function action_pre_get_comments( WP_Comment_Query $query ) {
		/**
		 * Filter to skip WP_Comment_Query integration
		 *
		 * @hook eprobe_skip_comment_query_integration
		 * @since 3.6.0
		 * @param  {bool} $skip True to skip
		 * @param  {WP_Comment_Query} $query WP_Comment_Query to evaluate
		 * @return {bool} New skip value
		 */
		if ( ! Indexables::factory()->get( 'comment' )->elasticpress_enabled( $query ) || apply_filters( 'eprobe_skip_comment_query_integration', false, $query ) ) {
			return;
		}

		if ( ! headers_sent() ) {
			/**
			 * Manually setting a header as $wp_query isn't yet initialized
			 * when we call: add_filter('wp_headers', 'filter_wp_headers');
			 */
			header( 'X-ElasticProbe-Search: true' );
		}
	}

	/**
	 * If WP_Comment_Query meets certain conditions, query results from ES
	 *
	 * @param  array            $results Query results.
	 * @param  WP_Comment_Query $query   Current query.
	 * @since  3.6.0
	 * @return array
	 */
	public function maybe_filter_query( $results, WP_Comment_Query $query ) {
		$this->indexable = Indexables::factory()->get( 'comment' );

		if ( ! $this->indexable->elasticpress_enabled( $query ) || apply_filters( 'eprobe_skip_comment_query_integration', false, $query ) ) {
			return $results;
		}

		/**
		 * Filter cached comments pre-post query
		 *
		 * @hook eprobe_wp_query_cached_comments
		 * @since 3.6.0
		 * @param  {mixed} $comments Comments or null
		 * @param  {WP_Comment_Query} $query WP_Comment_Query object
		 * @return {array} New cached comments
		 */
		$new_comments = apply_filters( 'eprobe_wp_query_cached_comments', null, $query );

		if ( null !== $new_comments ) {
			return $new_comments;
		}

		$new_comments = [];

		$formatted_args = $this->indexable->format_args( $query->query_vars );

		$scope = 'current';

		$site__in     = [];
		$site__not_in = [];

		if ( ! empty( $query->query_vars['sites'] ) ) {
			_deprecated_argument( __FUNCTION__, '0.1.0', esc_html__( 'sites is deprecated. Use site__in instead.', 'elasticprobe' ) );
		}

		if ( ! empty( $query->query_vars['site__in'] ) || ! empty( $query->query_vars['sites'] ) ) {
			$site__in = ! empty( $query->query_vars['site__in'] ) ? (array) $query->query_vars['site__in'] : (array) $query->query_vars['sites'];

			if ( in_array( 'all', $site__in, true ) ) {
				$scope = 'all';
			} elseif ( in_array( 'current', $site__in, true ) ) {
				$site__in = (array) get_current_blog_id();
			}
		}

		if ( ! empty( $query->query_vars['site__not_in'] ) ) {
			$site__not_in = (array) $query->query_vars['site__not_in'];
		}

		/**
		 * Filter search scope
		 *
		 * @since 3.6.0
		 *
		 * @param mixed $scope The search scope. Accepts `all` (string), a single
		 *                     site id (int or string), or an array of site ids (array).
		 */
		$scope = apply_filters( 'eprobe_comment_search_scope', $scope );

		if ( ! defined( 'EPROBE_IS_NETWORK' ) || ! EPROBE_IS_NETWORK ) {
			$scope = 'current';
		}

		$this->index = null;

		if ( 'all' === $scope ) {
			$this->index = $this->indexable->get_network_alias();
		} elseif ( ! empty( $site__in ) ) {
			$this->index = [];

			foreach ( $site__in as $site_id ) {
				$this->index[] = $this->indexable->get_index_name( $site_id );
			}

			$this->index = implode( ',', $this->index );
		} elseif ( ! empty( $site__not_in ) ) {

			$sites = \get_sites(
				array(
					'fields'       => 'ids',
					'site__not_in' => $site__not_in,
				)
			);
			foreach ( $sites as $site_id ) {
				if ( ! Utils\is_site_indexable( $site_id ) ) {
					continue;
				}
				$index[] = Indexables::factory()->get( 'comment' )->get_index_name( $site_id );
			}

			$this->index = implode( ',', $index );

		}

		$ep_query = $this->indexable->query_es( $formatted_args, $query->query_vars, $this->index, $query );

		if ( false === $ep_query ) {
			$query->elasticsearch_success = false;
			return $results;
		}

		if ( ! empty( $query->query_vars['count'] ) ) {
			return count( $ep_query['documents'] );
		}

		$query->found_comments        = $ep_query['found_documents']['value'];
		$query->max_num_pages         = $query->query_vars['number'] <= 0 ? 1 : max( 1, ceil( $query->found_comments / absint( $query->query_vars['number'] ) ) );
		$query->elasticsearch_success = true;

		// Determine how we should format the results from ES based on the fields parameter.
		$fields = $query->query_vars['fields'];

		switch ( $fields ) {
			case 'count':
				$new_comments = count( $ep_query['documents'] );
				break;

			case 'ids':
				$new_comments = $this->format_hits_as_ids( $ep_query['documents'], $new_comments );
				break;

			default:
				$new_comments = $this->format_hits_as_comments( $ep_query['documents'], $new_comments, $query->query_vars );
				break;
		}

		return $new_comments;
	}

	/**
	 * Format the ES hits/results as comments objects.
	 *
	 * @param  array $comments The comments that should be formatted.
	 * @param  array $new_comments Array of comments from cache.
	 * @param  array $query_vars Query variables.
	 * @since  3.6.0
	 * @return array
	 */
	protected function format_hits_as_comments( $comments, $new_comments, $query_vars ) {
		$hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : false;

		foreach ( $comments as $comment_array ) {
			$comment = new \WP_Comment( (object) $comment_array );
			if ( ! empty( $comment_array['site_id'] ) ) {
				$comment->site_id = $comment_array['site_id'];
			} else {
				$comment->site_id = get_current_blog_id();
			}

			$comment->elasticsearch = true; // Super useful for debugging

			if ( $comment ) {
				$new_comments[] = $comment;
			}
		}

		if ( $hierarchical ) {
			$new_comments = $this->fill_descendants( $new_comments, $query_vars );
		}

		return $new_comments;
	}

	/**
	 * Format the ES hits/results as an array of ids.
	 *
	 * @param  array $comments The comments that should be formatted.
	 * @param  array $new_comments Array of comments from cache.
	 * @since  3.6.0
	 * @return array
	 */
	protected function format_hits_as_ids( $comments, $new_comments ) {
		foreach ( $comments as $comment_array ) {
			$new_comments[] = $comment_array['comment_ID'];
		}

		return $new_comments;
	}

	/**
	 * Fetch descendants for located comments.
	 *
	 * @param array $comments Array of top-level comments whose descendants should be filled in.
	 * @param array $query_vars Current query vars.
	 * @since  3.6.0
	 * @return array
	 */
	protected function fill_descendants( $comments, $query_vars ) {
		$levels = [
			0 => $comments,
		];

		// Fetch an entire level of the descendant tree at a time.
		$level        = 0;
		$exclude_keys = [ 'parent', 'parent__in', 'parent__not_in' ];

		do {
			$child_comments = [];
			$_parent_ids    = wp_list_pluck( $levels[ $level ], 'comment_ID' );

			if ( $_parent_ids ) {
				$parent_query_args = $query_vars;

				foreach ( $exclude_keys as $exclude_key ) {
					$parent_query_args[ $exclude_key ] = '';
				}

				$parent_query_args['parent__in']   = $_parent_ids;
				$parent_query_args['hierarchical'] = false;
				$parent_query_args['offset']       = 0;
				$parent_query_args['number']       = 0;

				$formatted_args = $this->indexable->format_args( $parent_query_args );
				$ep_query       = $this->indexable->query_es( $formatted_args, $query_vars, $this->index );

				if ( false === $ep_query ) {
					$level_comments = [];
				} else {
					$level_comments = $this->format_hits_as_comments( $ep_query['documents'], [], [] );
				}

				foreach ( $level_comments as $level_comment ) {
					$child_comments[] = $level_comment;
				}
			}

			++$level;
			$levels[ $level ] = $child_comments;
		} while ( $child_comments );

		// Pull out just the descendant comments
		$descendants = [];
		for ( $i = 1, $c = count( $levels ); $i < $c; $i++ ) {
			$descendants = array_merge( $descendants, $levels[ $i ] );
		}

		// Assemble a flat array of all comments + descendants.
		$all_comments = $comments;
		foreach ( $descendants as $descendant ) {
			$all_comments[] = $descendant;
		}

		// If a threaded representation was requested, build the tree.
		if ( 'threaded' === $query_vars['hierarchical'] ) {
			$threaded_comments = [];
			$ref               = [];

			foreach ( $all_comments as $c ) {
				// If the comment isn't in the reference array, it goes in the top level of the thread.
				if ( ! isset( $ref[ $c->comment_parent ] ) ) {
					$threaded_comments[ $c->comment_ID ] = $c;
					$ref[ $c->comment_ID ]               = $threaded_comments[ $c->comment_ID ];

					// Otherwise, set it as a child of its parent.
				} else {
					$ref[ $c->comment_parent ]->add_child( $c );
					$ref[ $c->comment_ID ] = $c;
				}
			}

			$comments = $threaded_comments;
		} else {
			$comments = $all_comments;
		}

		return $comments;
	}
}
