<?php

namespace CelerSearch\Observers;

defined( 'ABSPATH' ) || exit;

use CelerSearch\DataTransfer\IndexableOrder;
use CelerSearch\DataTransfer\IndexCandidate;
use CelerSearch\Utilities\Logger;

/**
 * Observes WooCommerce order changes and syncs them to the search index
 * Uses HPOS-compatible hooks (not save_post)
 */
class OrderChangesObserver extends BaseObserver {

	/**
	 * Array of order IDs that have been deleted in this request
	 * Used to prevent race conditions when an order is deleted then re-saved
	 *
	 * @var array
	 */
	private array $deleted_orders = [];

	/**
	 * Start observing for changes
	 *
	 * @return void
	 */
	public function observe(): void {
		// HPOS-compatible hooks for order lifecycle
		add_action( 'woocommerce_new_order', [ $this, 'on_order_created' ], 10, 2 );
		add_action( 'woocommerce_update_order', [ $this, 'on_order_updated' ], 10, 2 );

		// Delete handlers - before_delete_order fires while data is still available
		add_action( 'woocommerce_before_delete_order', [ $this, 'on_order_deleted' ], 10, 2 );
		add_action( 'woocommerce_trash_order', [ $this, 'on_order_trashed' ] );

		// Status change hook - useful for updating status_name in index
		add_action( 'woocommerce_order_status_changed', [ $this, 'on_order_status_changed' ], 10, 4 );
	}

	/**
	 * Handle order created event
	 *
	 * @param int            $order_id Order ID.
	 * @param \WC_Order|null $order    Order object (may be null in older WC versions).
	 *
	 * @return void
	 */
	public function on_order_created( int $order_id, $order = null ): void {
		$this->handle_order_change( $order_id, $order, 'created' );
	}

	/**
	 * Handle order updated event
	 *
	 * @param int            $order_id Order ID.
	 * @param \WC_Order|null $order    Order object (may be null in older WC versions).
	 *
	 * @return void
	 */
	public function on_order_updated( int $order_id, $order = null ): void {
		$this->handle_order_change( $order_id, $order, 'updated' );
	}

	/**
	 * Handle order status changed event
	 *
	 * @param int       $order_id   Order ID.
	 * @param string    $old_status Old status.
	 * @param string    $new_status New status.
	 * @param \WC_Order $order      Order object.
	 *
	 * @return void
	 */
	public function on_order_status_changed( int $order_id, string $old_status, string $new_status, \WC_Order $order ): void {
		// Re-index on status change to update status_name
		$this->handle_order_change( $order_id, $order, 'status_changed' );
	}

	/**
	 * Handle order change (create/update)
	 *
	 * @param int            $order_id Order ID.
	 * @param \WC_Order|null $order    Order object.
	 * @param string         $action   The action (created, updated, status_changed).
	 *
	 * @return void
	 */
	private function handle_order_change( int $order_id, $order, string $action ): void {
		// Skip if this order was deleted in this request (race condition prevention)
		if ( in_array( $order_id, $this->deleted_orders, true ) ) {
			Logger::channel( Logger::CHANNEL_OBSERVERS )->debug( 'Order sync skipped - recently deleted', [
				'order_id' => $order_id,
			] );

			return;
		}

		// Get the order if not provided
		if ( ! $order || ! is_a( $order, \WC_Order::class ) ) {
			$order = wc_get_order( $order_id );
		}

		if ( ! $order ) {
			Logger::channel( Logger::CHANNEL_OBSERVERS )->warning( 'Order sync skipped - order not found', [
				'order_id' => $order_id,
			] );

			return;
		}

		// Allow filtering whether to sync this order
		if ( ! apply_filters( 'celersearch_should_sync_order', true, $order, $this->index ) ) {
			Logger::channel( Logger::CHANNEL_OBSERVERS )->debug( 'Order sync blocked by filter', [
				'order_id'   => $order->get_id(),
				'filter'     => 'celersearch_should_sync_order',
				'index_slug' => $this->index->get_slug(),
			] );

			return;
		}

		// Use queue if enabled, otherwise sync immediately
		if ( $this->should_use_queue() ) {
			Logger::channel( Logger::CHANNEL_OBSERVERS )->info( 'Order sync queued', [
				'order_id'     => $order->get_id(),
				'order_number' => $order->get_order_number(),
				'order_status' => $order->get_status(),
				'action'       => $action,
				'index_slug'   => $this->index->get_slug(),
			] );

			$this->queue_sync( 'order', $order->get_id() );
			return;
		}

		Logger::channel( Logger::CHANNEL_OBSERVERS )->info( 'Order sync triggered', [
			'order_id'     => $order->get_id(),
			'order_number' => $order->get_order_number(),
			'order_status' => $order->get_status(),
			'action'       => $action,
			'index_slug'   => $this->index->get_slug(),
		] );

		$this->sync_item( $order );
	}

	/**
	 * Handle order deleted event
	 *
	 * @param int            $order_id Order ID.
	 * @param \WC_Order|null $order    Order object.
	 *
	 * @return void
	 */
	public function on_order_deleted( int $order_id, $order = null ): void {
		// Get the order if not provided
		if ( ! $order || ! is_a( $order, \WC_Order::class ) ) {
			$order = wc_get_order( $order_id );
		}

		if ( ! $order ) {
			return;
		}

		// Allow filtering whether to process this deletion
		if ( ! apply_filters( 'celersearch_should_delete_order', true, $order, $this->index ) ) {
			Logger::channel( Logger::CHANNEL_OBSERVERS )->debug( 'Order delete blocked by filter', [
				'order_id'   => $order->get_id(),
				'filter'     => 'celersearch_should_delete_order',
				'index_slug' => $this->index->get_slug(),
			] );

			return;
		}

		// Track deleted orders to prevent race conditions
		$this->deleted_orders[] = $order_id;

		// Use queue if enabled, otherwise delete immediately
		if ( $this->should_use_queue() ) {
			Logger::channel( Logger::CHANNEL_OBSERVERS )->info( 'Order delete queued', [
				'order_id'     => $order->get_id(),
				'order_number' => $order->get_order_number(),
				'index_slug'   => $this->index->get_slug(),
			] );

			$this->queue_delete( 'order', $order->get_id() );
			return;
		}

		Logger::channel( Logger::CHANNEL_OBSERVERS )->info( 'Order delete triggered', [
			'order_id'     => $order->get_id(),
			'order_number' => $order->get_order_number(),
			'index_slug'   => $this->index->get_slug(),
		] );

		$this->delete_order_records( $order_id );
	}

	/**
	 * Handle order trashed event
	 *
	 * @param int $order_id Order ID.
	 *
	 * @return void
	 */
	public function on_order_trashed( int $order_id ): void {
		$order = wc_get_order( $order_id );
		if ( $order ) {
			$this->on_order_deleted( $order_id, $order );
		}
	}

	/**
	 * Delete order record from the index
	 *
	 * Unlike posts, orders don't split into multiple records,
	 * so we only need to delete a single record.
	 *
	 * @param int $order_id Order ID.
	 *
	 * @return void
	 */
	protected function delete_order_records( int $order_id ): void {
		$object_id = 'order-' . $order_id;
		$object    = new IndexableOrder( $object_id, [ 'order_id' => $order_id ] );

		try {
			$this->index->delete_item( $object );
			Logger::channel( Logger::CHANNEL_OBSERVERS )->info( 'Order deleted from index', [
				'order_id'   => $order_id,
				'index_slug' => $this->index->get_slug(),
			] );
		} catch ( \Exception $e ) {
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Order delete failed', [
				'order_id'   => $order_id,
				'exception'  => $e->getMessage(),
				'index_slug' => $this->index->get_slug(),
			] );
		}
	}

	/**
	 * Override parent's delete_item to handle orders
	 *
	 * @param mixed $item The raw item.
	 *
	 * @return void
	 */
	protected function delete_item( $item ): void {
		if ( is_a( $item, \WC_Order::class ) ) {
			$this->delete_order_records( $item->get_id() );
			return;
		}

		parent::delete_item( $item );
	}

	/**
	 * Override parent's sync_item to add order-specific logging
	 *
	 * @param mixed $item The raw item (WC_Order).
	 *
	 * @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 \WC_Order ) {
			$log_context['order_id']     = $item->get_id();
			$log_context['order_number'] = $item->get_order_number();
			$log_context['order_status'] = $item->get_status();
		}

		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 );
		}
	}
}
