<?php

namespace CelerSearch\Queue;

defined( 'ABSPATH' ) || exit;

use CelerSearch\Utilities\Logger;

/**
 * Queue manager for sync operations
 *
 * Uses Action Scheduler to defer sync/delete operations, preventing
 * Meilisearch flooding during mass operations. Type-agnostic to support
 * any indexable content type (posts, products, custom tables, etc.)
 */
class Queue {

	/**
	 * Action Scheduler group name
	 */
	const GROUP = 'celersearch';

	/**
	 * Action name for sync operations
	 */
	const SYNC_ACTION = 'celersearch_queue_sync_item';

	/**
	 * Action name for delete operations
	 */
	const DELETE_ACTION = 'celersearch_queue_delete_item';

	/**
	 * Timeout threshold for stuck actions in seconds (5 minutes).
	 */
	const STUCK_THRESHOLD = 300;

	/**
	 * Check if Action Scheduler is available
	 *
	 * @return bool
	 */
	public static function is_available(): bool {
		return function_exists( 'as_schedule_single_action' );
	}

	/**
	 * Register action handlers
	 *
	 * @return void
	 */
	public static function register_handlers(): void {
		if ( ! self::is_available() ) {
			return;
		}

		add_action( self::SYNC_ACTION, [ SyncHandler::class, 'handle_sync' ], 10, 3 );
		add_action( self::DELETE_ACTION, [ SyncHandler::class, 'handle_delete' ], 10, 3 );
	}

	/**
	 * Debounce delay in seconds for sync operations.
	 * Actions scheduled within this window will be replaced by the latest one.
	 */
	const DEBOUNCE_DELAY = 5;

	/**
	 * Schedule a sync operation with debouncing.
	 *
	 * Uses a debounce pattern: schedules sync with a small delay. If another
	 * sync is requested for the same item within the delay window, the previous
	 * is cancelled and a new one scheduled. This ensures only the final state
	 * is synced while minimizing cancelled entries.
	 *
	 * @param string $item_type Type identifier (e.g., 'post', 'product', 'custom_row').
	 * @param mixed  $item_id   The item's unique identifier.
	 * @param int    $index_id  The target index ID.
	 *
	 * @return void
	 */
	public static function schedule_sync( string $item_type, $item_id, int $index_id ): void {
		if ( ! self::is_available() ) {
			return;
		}

		$args = [
			'item_type' => $item_type,
			'item_id'   => $item_id,
			'index_id'  => $index_id,
		];

		// Cancel any existing pending sync for this item (debounce)
		self::cancel_pending( self::SYNC_ACTION, $item_type, $item_id, $index_id );

		// Schedule with delay to allow debouncing
		as_schedule_single_action(
			time() + self::DEBOUNCE_DELAY,
			self::SYNC_ACTION,
			$args,
			self::GROUP
		);
	}

	/**
	 * Schedule a delete operation.
	 *
	 * Cancels any pending sync for this item (no point syncing something being deleted),
	 * then schedules immediate delete if not already pending.
	 * Deletes are scheduled immediately (no debounce) since we want items
	 * removed from the search index as quickly as possible.
	 *
	 * @param string $item_type Type identifier (e.g., 'post', 'product', 'custom_row').
	 * @param mixed  $item_id   The item's unique identifier.
	 * @param int    $index_id  The target index ID.
	 *
	 * @return void
	 */
	public static function schedule_delete( string $item_type, $item_id, int $index_id ): void {
		if ( ! self::is_available() ) {
			return;
		}

		// Cancel any pending sync - no point syncing something being deleted
		self::cancel_pending( self::SYNC_ACTION, $item_type, $item_id, $index_id );

		// Skip if delete already scheduled for this specific item
		if ( self::is_scheduled( self::DELETE_ACTION, $item_type, $item_id, $index_id ) ) {
			return;
		}

		// Schedule immediately - deletes should happen ASAP
		as_enqueue_async_action(
			self::DELETE_ACTION,
			[
				'item_type' => $item_type,
				'item_id'   => $item_id,
				'index_id'  => $index_id,
			],
			self::GROUP
		);
	}

	/**
	 * Check if an action is already scheduled for this item
	 *
	 * @param string $action    The action name.
	 * @param string $item_type Type identifier.
	 * @param mixed  $item_id   The item's unique identifier.
	 * @param int    $index_id  The target index ID.
	 *
	 * @return bool
	 */
	public static function is_scheduled( string $action, string $item_type, $item_id, int $index_id ): bool {
		if ( ! self::is_available() ) {
			return false;
		}

		$args = [
			'item_type' => $item_type,
			'item_id'   => $item_id,
			'index_id'  => $index_id,
		];

		return as_next_scheduled_action( $action, $args, self::GROUP ) !== false;
	}

	/**
	 * Cancel pending actions for an item
	 *
	 * @param string $action    The action name.
	 * @param string $item_type Type identifier.
	 * @param mixed  $item_id   The item's unique identifier.
	 * @param int    $index_id  The target index ID.
	 *
	 * @return void
	 */
	public static function cancel_pending( string $action, string $item_type, $item_id, int $index_id ): void {
		if ( ! self::is_available() ) {
			return;
		}

		$args = [
			'item_type' => $item_type,
			'item_id'   => $item_id,
			'index_id'  => $index_id,
		];

		as_unschedule_all_actions( $action, $args, self::GROUP );
	}

	/**
	 * Cancel all pending actions for an item across all indices
	 *
	 * @param string $item_type Type identifier.
	 * @param mixed  $item_id   The item's unique identifier.
	 *
	 * @return void
	 */
	public static function cancel_all_for_item( string $item_type, $item_id ): void {
		if ( ! self::is_available() || ! function_exists( 'as_get_scheduled_actions' ) ) {
			return;
		}

		// Get all pending actions for this item
		$actions = as_get_scheduled_actions(
			[
				'hook'   => [ self::SYNC_ACTION, self::DELETE_ACTION ],
				'status' => \ActionScheduler_Store::STATUS_PENDING,
				'group'  => self::GROUP,
			]
		);

		foreach ( $actions as $action ) {
			$args = $action->get_args();
			if ( isset( $args['item_type'], $args['item_id'] )
			     && $args['item_type'] === $item_type
			     && $args['item_id'] === $item_id ) {
				as_unschedule_action( $action->get_hook(), $args, self::GROUP );
			}
		}
	}

	/**
	 * Get pending action count for the group
	 *
	 * @return int
	 */
	public static function get_pending_count(): int {
		if ( ! self::is_available() || ! function_exists( 'as_get_scheduled_actions' ) ) {
			return 0;
		}

		$actions = as_get_scheduled_actions(
			[
				'hook'   => [ self::SYNC_ACTION, self::DELETE_ACTION ],
				'status' => \ActionScheduler_Store::STATUS_PENDING,
				'group'  => self::GROUP,
			]
		);

		return count( $actions );
	}

	/**
	 * Get in-progress actions that have been running longer than STUCK_THRESHOLD.
	 *
	 * @return array<int, \ActionScheduler_Action> Action ID => Action object.
	 */
	private static function get_stuck_actions(): array {
		if ( ! self::is_available() || ! function_exists( 'as_get_scheduled_actions' ) ) {
			return [];
		}

		// Query sync and delete separately for AS version compatibility
		$sync_actions = as_get_scheduled_actions( [
			'hook'     => self::SYNC_ACTION,
			'status'   => \ActionScheduler_Store::STATUS_RUNNING,
			'group'    => self::GROUP,
			'per_page' => 100,
		] );

		$delete_actions = as_get_scheduled_actions( [
			'hook'     => self::DELETE_ACTION,
			'status'   => \ActionScheduler_Store::STATUS_RUNNING,
			'group'    => self::GROUP,
			'per_page' => 100,
		] );

		$running_actions = $sync_actions + $delete_actions;

		if ( empty( $running_actions ) ) {
			return [];
		}

		$store     = \ActionScheduler::store();
		$threshold = time() - self::STUCK_THRESHOLD;
		$stuck     = [];

		foreach ( $running_actions as $action_id => $action ) {
			try {
				$last_attempt = $store->get_date( $action_id );
				if ( $last_attempt && $last_attempt->getTimestamp() < $threshold ) {
					$stuck[ $action_id ] = $action;
				}
			} catch ( \Exception $e ) {
				// Skip actions we can't inspect
			}
		}

		return $stuck;
	}

	/**
	 * Get count of stuck in-progress actions.
	 *
	 * @return int
	 */
	public static function get_stuck_count(): int {
		return count( self::get_stuck_actions() );
	}

	/**
	 * Detect and clean up stuck in-progress actions.
	 *
	 * Actions stuck in 'in-progress' status for longer than STUCK_THRESHOLD
	 * are marked as failed and re-scheduled so they get retried.
	 *
	 * @return int Number of stuck actions cleaned up.
	 */
	public static function cleanup_stuck_actions(): int {
		$stuck_actions = self::get_stuck_actions();

		if ( empty( $stuck_actions ) ) {
			return 0;
		}

		$store   = \ActionScheduler::store();
		$cleaned = 0;

		foreach ( $stuck_actions as $action_id => $action ) {
			try {
				$store->mark_failure( $action_id );

				Logger::channel( Logger::CHANNEL_QUEUE )->warning( 'Cleaned up stuck action', [
					'action_id' => $action_id,
					'hook'      => $action->get_hook(),
					'args'      => $action->get_args(),
				] );

				// Re-schedule so the operation is retried
				$args = $action->get_args();
				if ( str_contains( $action->get_hook(), 'sync' ) ) {
					self::schedule_sync(
						$args['item_type'] ?? '',
						$args['item_id'] ?? 0,
						$args['index_id'] ?? 0
					);
				} else {
					self::schedule_delete(
						$args['item_type'] ?? '',
						$args['item_id'] ?? 0,
						$args['index_id'] ?? 0
					);
				}

				$cleaned ++;
			} catch ( \Exception $e ) {
				Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Failed to clean stuck action', [
					'action_id' => $action_id,
					'exception' => $e->getMessage(),
				] );
			}
		}

		return $cleaned;
	}
}
