<?php

namespace CelerSearch\Indices;

defined( 'ABSPATH' ) || exit;

use CelerSearch\Builders\IndexSettingsBuilder;
use CelerSearch\DataTransfer\IndexCandidate;
use CelerSearch\DataTransfer\IndexSettings;
use CelerSearch\DataTransfer\IndexablePost;
use CelerSearch\Interfaces\IIndexableObject;
use CelerSearch\Utilities\PostUtilities;
use RuntimeException;
use WP_Post;

class PostsIndex extends BasePostIndex {

	/**
	 * The allowed post types
	 * @var array
	 */
	protected array $post_types;

	/**
	 * Performs additional initialization (if needed)
	 * @return void
	 */
	protected function init(): void {
		$config           = $this->details->getConfig();
		$this->post_types = isset( $config->post_types ) ? $config->post_types : array();

		// If post_types is an object (due to array to object conversion), convert back to array
		if ( is_object( $this->post_types ) ) {
			$this->post_types = (array) $this->post_types;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function get_sort_field_map(): array {
		return [ 'date' => 'created_at', 'title' => 'title' ];
	}

	/**
	 * Check whether an item is supported.
	 *
	 * @param IndexCandidate $item
	 *
	 * @return bool
	 */
	public function is_supported( IndexCandidate $item ): bool {
		return $item && is_a( $item->item, \WP_Post::class ) && in_array( $item->item->post_type, $this->post_types, true );
	}

	/**
	 * Check whether an item can be indexed.
	 *
	 * @param IndexCandidate $item
	 *
	 * @return bool
	 */
	public function should_index( IndexCandidate $item ): bool {
		if ( ! $this->is_supported( $item ) ) {
			return false;
		}

		// Index posts with meaningful statuses (exclude trash, auto-draft)
		$indexable_statuses = apply_filters( 'celersearch_indexable_post_statuses', [
			'publish',
			'draft',
			'pending',
			'private',
			'future',
		], $item->item );

		return in_array( $item->item->post_status, $indexable_statuses, true );
	}

	/**
	 * Returns items from the index
	 *
	 * @param $page
	 * @param $batch_size
	 *
	 * @return array<IndexCandidate>
	 */
	public function get_candidates( $page, $batch_size ): array {
		$args = [
			'post_type'              => $this->post_types,
			'posts_per_page'         => $batch_size,
			'post_status'            => 'any',
			'order'                  => 'ASC',
			'orderby'                => 'ID',
			'paged'                  => $page,
			// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Required to prevent recursive search filters during indexing
			'suppress_filters'       => true,
			'cache_results'          => false,
			'lazy_load_term_meta'    => false,
			'update_post_term_cache' => false,
		];

		return array_map( function ( \WP_Post $item ) {
			return new IndexCandidate( $item );
		}, get_posts( $args ) );
	}

	/**
	 * Returns specific items by IDs
	 *
	 * @param $ids
	 *
	 * @return array<IndexCandidate>
	 */
	public function get_candidates_by_ids( $ids ): array {
		$args             = [
			'post_type'              => $this->post_types,
			'posts_per_page'         => count( $ids ),
			'post_status'            => 'any',
			'order'                  => 'ASC',
			'orderby'                => 'ID',
			'paged'                  => 1,
			// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Required to prevent recursive search filters during indexing
			'suppress_filters'       => true,
			'cache_results'          => false,
			'lazy_load_term_meta'    => false,
			'update_post_term_cache' => false,
		];
		$args['post__in'] = (array) $ids;

		return array_map( function ( \WP_Post $item ) {
			return new IndexCandidate( $item );
		}, get_posts( $args ) );
	}

	/**
	 * Returns items from the index
	 *
	 * @return int
	 */
	public function get_candidate_count(): int {

		$query = new \WP_Query(
			array(
				'post_type'              => $this->post_types,
				'post_status'            => 'any',
				// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Required to prevent recursive search filters during indexing
				'suppress_filters'       => true,
				'cache_results'          => false,
				'lazy_load_term_meta'    => false,
				'update_post_term_cache' => false,
			)
		);

		return (int) $query->found_posts;
	}

	/**
	 * Returns the item records
	 *
	 * @param IndexCandidate $item
	 *
	 * @return IIndexableObject[]
	 */
	public function get_candidate_objects( IndexCandidate $item ): array {

		/* @var WP_Post $post */
		$post = $item->item;

		$attributes = $this->format_attributes( $post );

		$removed = remove_filter( 'the_content', 'wptexturize', 10 );

		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying WordPress core filter
		$content = apply_filters( 'the_content', apply_filters(
				'celersearch_searchable_post_content',
				$post->post_content,
				$post
			)
		);

		if ( true === $removed ) {
			add_filter( 'the_content', 'wptexturize', 10 );
		}

		$content = PostUtilities::prepare_content( $content );
		$parts   = PostUtilities::explode_content( $content );

		if ( defined( 'CELERSEARCH_SPLIT_POSTS' ) && false === CELERSEARCH_SPLIT_POSTS ) {
			$parts = array( array_shift( $parts ) );
		}

		$records = array();
		foreach ( $parts as $i => $part ) {
			$record                 = $attributes;
			$object_id              = PostUtilities::get_post_object_id( $post->ID, $i );
			$record['object_id']    = $object_id;
			$record['content']      = $part;
			$record['record_index'] = $i;
			$records[]              = new IndexablePost( $object_id, $record );
		}

		$records = (array) apply_filters( 'celersearch_searchable_post_records', $records, $post );

		return (array) apply_filters( 'celersearch_searchable_post_' . $post->post_type . '_records', $records, $post );
	}

	/**
	 * Returns the post attributes for indexing
	 *
	 * Inspired by WP Search with Algolia plugin
	 *
	 * @param WP_Post $post
	 *
	 * @return array<string, mixed>
	 */
	private function format_attributes( \WP_Post $post ): array {
		$attributes           = [];
		$attributes['id']      = $post->ID;
		$attributes['post_id'] = $post->ID;
		$attributes['type']   = $post->post_type;
		$attributes['status'] = $post->post_status;

		$post_type = get_post_type_object( $post->post_type );
		if ( null === $post_type ) {
			throw new RuntimeException( 'Unable to fetch the post type information.' );
		}
		$attributes['type_label']           = $post_type->labels->name;
		$attributes['title']                = $post->post_title;
		$attributes['excerpt']              = PostUtilities::prepare_content( apply_filters( 'the_excerpt', $post->post_excerpt ) ); // phpcs:ignore -- Legitimate use of Core hook.
		$attributes['created_at']           = get_post_time( 'U', false, $post );
		$attributes['created_at_formatted'] = get_the_date( '', $post );
		$attributes['updated_at']           = get_post_modified_time( 'U', false, $post );
		$attributes['comment_count']        = (int) $post->comment_count;
		$attributes['menu_order']           = (int) $post->menu_order;

		$author = get_userdata( $post->post_author );
		if ( $author ) {
			$attributes['author'] = array(
				'id'           => (int) $post->post_author,
				'display_name' => $author->display_name,
				'url'          => $author->user_url,
				'login'        => $author->user_login,
			);
		}

		$attributes['images'] = PostUtilities::get_post_images( $post->ID );

		$attributes['permalink'] = get_permalink( $post );
		$attributes['mime_type'] = $post->post_mime_type;

		// Push all taxonomies by default, including custom ones.
		$taxonomy_objects = get_object_taxonomies( $post->post_type, 'objects' );

		$attributes['taxonomies']              = array();
		$attributes['taxonomies_hierarchical'] = array();
		foreach ( $taxonomy_objects as $taxonomy ) {
			$terms = wp_get_object_terms( $post->ID, $taxonomy->name );
			$terms = is_array( $terms ) ? $terms : array();

			if ( $taxonomy->hierarchical ) {
				$hierarchical_taxonomy_values = PostUtilities::get_taxonomy_tree( $terms, $taxonomy->name );
				if ( ! empty( $hierarchical_taxonomy_values ) ) {
					$attributes['taxonomies_hierarchical'][ $taxonomy->name ] = $hierarchical_taxonomy_values;
				}
			}

			$taxonomy_values = array_map( 'html_entity_decode', wp_list_pluck( $terms, 'name' ) );
			if ( ! empty( $taxonomy_values ) ) {
				$attributes['taxonomies'][ $taxonomy->name ] = $taxonomy_values;
			}
		}

		$attributes['is_sticky'] = is_sticky( $post->ID ) ? 1 : 0;

		$attributes = (array) apply_filters( 'celersearch_searchable_post_shared_attributes', $attributes, $post );

		return (array) apply_filters( 'celersearch_searchable_post_' . $post->post_type . '_shared_attributes', $attributes, $post );

	}

	/**
	 * Create post index settings
	 *
	 * @return IndexSettings
	 */
	public function get_settings(): IndexSettings {

		$settings = ( new IndexSettingsBuilder )
			->from_stored( $this->get_stored_settings() )
			->primary_key( 'object_id' )
			->distinct_attribute( 'post_id' )
			->searchable_attributes( [
				'title:unordered',
				'taxonomies:unordered',
				'content:unordered'
			] )
			->filterable_attributes( [
				'taxonomies',
				'taxonomies_hierarchical',
				'author.display_name',
				'type_label',
				'status',
				'post_id',
			] )
			->sortable_attributes( [
				'is_sticky:desc',
				'created_at:desc',
				'record_index:asc',
				'title:asc',
			] )
			->snippet_attributes( [
				'title:30',
				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying WordPress core filter
				'content:' . intval( apply_filters( 'excerpt_length', 55 ) ),
			] );


		$settings = apply_filters( 'celersearch_posts_index_settings', $settings, $this );

		return $settings->build();

	}
}