<?php

namespace CelerSearch\Observers;

use CelerSearch\DataTransfer\IndexablePost;
use CelerSearch\DataTransfer\IndexCandidate;
use CelerSearch\Indices\BaseIndex;
use CelerSearch\Queue\Queue;
use CelerSearch\Queue\QueueSettings;
use CelerSearch\Utilities\Logger;
use CelerSearch\Utilities\PostUtilities;

abstract class BaseObserver {

	/**
	 * The index this observer watches
	 *
	 * @var BaseIndex
	 */
	protected BaseIndex $index;

	/**
	 * Constructor
	 *
	 * @param BaseIndex $index
	 */
	public function __construct( BaseIndex $index ) {
		$this->index = $index;
	}

	/**
	 * Returns the index
	 *
	 * @return BaseIndex
	 */
	public function get_index(): BaseIndex {
		return $this->index;
	}

	/**
	 * Start observing for changes
	 *
	 * @return void
	 */
	abstract public function observe(): void;

	/**
	 * Sync an item to the index
	 *
	 * @param mixed $item The raw item (e.g., WP_Post)
	 *
	 * @return void
	 */
	protected function sync_item( $item ): void {
		$candidate  = new IndexCandidate( $item );
		$start_time = microtime( true );

		// Check if the index supports this item type
		if ( ! $this->index->is_supported( $candidate ) ) {
			return;
		}

		// Build context for logging
		$log_context = [
			'index_slug' => $this->index->get_slug(),
		];
		if ( $item instanceof \WP_Post ) {
			$log_context['post_id']    = $item->ID;
			$log_context['post_title'] = $item->post_title;
			$log_context['post_type']  = $item->post_type;
		}

		Logger::channel( Logger::CHANNEL_OBSERVERS )->debug( 'Sync item started', $log_context );

		try {
			// Check if item should be indexed
			if ( $this->index->should_index( $candidate ) ) {
				// Get the indexable objects and sync them
				$objects = $this->index->get_candidate_objects( $candidate );
				foreach ( $objects as $object ) {
					$this->index->sync_item( $object );
				}

				$log_context['duration_ms']   = round( ( microtime( true ) - $start_time ) * 1000, 2 );
				$log_context['objects_count'] = count( $objects );
				Logger::channel( Logger::CHANNEL_OBSERVERS )->info( 'Item synced successfully', $log_context );
			} else {
				// Item should not be indexed, remove it
				Logger::channel( Logger::CHANNEL_OBSERVERS )->info( 'Item removed - should_index returned false', $log_context );
				$this->delete_item( $item );
			}
		} catch ( \Exception $e ) {
			$log_context['exception'] = $e->getMessage();
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Observer sync failed', $log_context );
		}
	}

	/**
	 * Delete an item from the index
	 *
	 * @param mixed $item The raw item (e.g., WP_Post)
	 *
	 * @return void
	 */
	protected function delete_item( $item ): void {
		$candidate = new IndexCandidate( $item );

		// Check if the index supports this item type
		if ( ! $this->index->is_supported( $candidate ) ) {
			return;
		}

		// Build context for logging
		$log_context = [
			'index_slug' => $this->index->get_slug(),
		];
		if ( $item instanceof \WP_Post ) {
			$log_context['post_id']    = $item->ID;
			$log_context['post_title'] = $item->post_title;
			$log_context['post_type']  = $item->post_type;
		}

		try {
			// For posts that may be split into multiple records, delete all of them
			if ( is_a( $item, \WP_Post::class ) ) {
				$this->delete_post_records( $item->ID );
				Logger::channel( Logger::CHANNEL_OBSERVERS )->info( 'Item deleted from index', $log_context );
			}
		} catch ( \Exception $e ) {
			$log_context['exception'] = $e->getMessage();
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Observer delete failed', $log_context );
		}
	}

	/**
	 * Delete all records for a post ID
	 *
	 * Since posts can be split into multiple records (e.g., 123-0, 123-1, 123-2),
	 * we need to delete all possible records.
	 *
	 * @param int $post_id
	 *
	 * @return void
	 */
	protected function delete_post_records( int $post_id ): void {
		// Maximum number of splits to try deleting
		// This is a safeguard - most posts won't have more than 10 splits
		$max_splits = apply_filters( 'celersearch_max_post_splits', 10 );

		for ( $i = 0; $i < $max_splits; $i ++ ) {
			$object_id = PostUtilities::get_post_object_id( $post_id, $i );
			$object    = new IndexablePost( $object_id, [ 'post_id' => $post_id ] );

			try {
				$this->index->delete_item( $object );
			} catch ( \Exception $e ) {
				// Stop trying if we get an error (likely record doesn't exist)
				// MeiliSearch returns success even for non-existent documents,
				// so errors indicate real problems
				if ( $i > 0 ) {
					break;
				}
			}
		}
	}

	/**
	 * Public wrapper for sync_item - used by queue processing
	 *
	 * @param mixed $item The raw item (e.g., WP_Post)
	 *
	 * @return void
	 */
	public function process_sync( $item ): void {
		$this->sync_item( $item );
	}

	/**
	 * Public wrapper for delete_item - used by queue processing
	 *
	 * @param mixed $item The raw item (e.g., WP_Post)
	 *
	 * @return void
	 */
	public function process_delete( $item ): void {
		$this->delete_item( $item );
	}

	/**
	 * Check if queue processing should be used
	 *
	 * @return bool
	 */
	protected function should_use_queue(): bool {
		return QueueSettings::is_queue_enabled() && Queue::is_available();
	}

	/**
	 * Schedule a sync operation in the queue
	 *
	 * @param string $item_type Type identifier (e.g., 'post').
	 * @param mixed  $item_id   The item's unique identifier.
	 *
	 * @return void
	 */
	protected function queue_sync( string $item_type, $item_id ): void {
		Queue::schedule_sync( $item_type, $item_id, $this->index->get_id() );
	}

	/**
	 * Schedule a delete operation in the queue
	 *
	 * @param string $item_type Type identifier (e.g., 'post').
	 * @param mixed  $item_id   The item's unique identifier.
	 *
	 * @return void
	 */
	protected function queue_delete( string $item_type, $item_id ): void {
		Queue::schedule_delete( $item_type, $item_id, $this->index->get_id() );
	}
}