<?php
/**
 * Cron Hooks
 *
 * @package    MOMCS_Sync
 * @subpackage MOMCS_Sync/includes/common/hooks
 * @author     miniOrange
 */

declare(strict_types=1);

namespace MOMCS_Sync\Includes\Common\Hooks;

use MOMCS_Sync\Includes\Common\Constants\Plugin_Constants;
use MOMCS_Sync\Includes\Common\Handlers\Feed_Handler;
use MOMCS_Sync\Includes\Common\Utils\Logger;

/**
 * Cron Hooks Class
 *
 * Registers and handles cron jobs for the plugin
 */
class Cron_Hooks {
	/**
	 * Logger instance
	 *
	 * @var Logger
	 */
	private $logger;

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->register_hooks();
	}

	/**
	 * Register hooks
	 *
	 * @return void
	 */
	private function register_hooks(): void {
		// Register cron job for checking feed status.
		// Accept feed_id, originating action_id, channel_id, and polling attempt number.
		add_action( Plugin_Constants::CRON_CHECK_FEED_STATUS, array( $this, 'check_feed_status' ), 10, 4 );

		// Register Action Scheduler hook for chunked feed building.
		add_action( Plugin_Constants::CRON_BUILD_FEED_CHUNK, array( $this, 'process_feed_build_chunk' ), 10, 1 );

		// Register cleanup hook for feed build progress transients.
		add_action( 'momcs_cleanup_feed_build_progress', array( $this, 'cleanup_feed_build_progress' ), 10, 1 );

		// Register Action Scheduler failure hook for automatic retry of failed chunk builds.
		add_action( 'action_scheduler_failed_action', array( $this, 'handle_failed_chunk_action' ), 10, 2 );
	}

	/**
	 * Check feed status with exponential backoff support.
	 *
	 * @param string      $feed_id    Feed ID.
	 * @param int         $action_id  (Optional) Action-Scheduler job ID originating the feed submission.
	 * @param string|null $channel_id (Optional) Channel UUID.
	 * @param int         $attempt    (Optional) Current polling attempt number (1-based, default: 1).
	 *
	 * @return void
	 */
	public function check_feed_status( $feed_id, $action_id = 0, $channel_id = null, $attempt = 1 ) {
		// Delegate directly to Feed_Handler.
		( new Feed_Handler() )->check_feed_status( $feed_id, (int) $action_id, $channel_id, (int) $attempt );
	}

	/**
	 * Process a chunk of products in chunked feed building.
	 *
	 * Delegates to Feed_Handler::process_feed_build_chunk() which handles
	 * loading progress, processing products, and scheduling next chunk or finalization.
	 *
	 * @param string $job_id Unique job identifier for the chunked feed build.
	 *
	 * @return void
	 */
	public function process_feed_build_chunk( string $job_id ): void {
		( new Feed_Handler() )->process_feed_build_chunk( $job_id );
	}

	/**
	 * Clean up feed build progress transient after successful completion.
	 *
	 * @param string $job_id Unique job identifier.
	 *
	 * @return void
	 */
	public function cleanup_feed_build_progress( string $job_id ): void {
		$transient_key = Plugin_Constants::TRANSIENT_FEED_BUILD_PREFIX . $job_id;
		delete_transient( $transient_key );

		if ( ! $this->logger ) {
			$this->logger = new Logger( 'cron-hooks' );
		}
		$this->logger->info( 'Cleaned up feed build progress transient for job: ' . $job_id );
	}

	/**
	 * Handle failed chunk build actions by rescheduling with exponential backoff.
	 *
	 * This hook is triggered by Action Scheduler when an action fails due to
	 * timeout, exception, or other errors. It automatically reschedules the
	 * failed chunk up to MAX_RETRIES times with increasing delay.
	 *
	 * @param int             $action_id The failed action ID.
	 * @param \Exception|null $exception The exception that caused the failure (if any).
	 *
	 * @return void
	 */
	public function handle_failed_chunk_action( int $action_id, $exception = null ): void {
		// Get the failed action details from Action Scheduler store.
		$action = \ActionScheduler::store()->fetch_action( $action_id );

		if ( ! $action ) {
			return;
		}

		// Only handle our chunk build actions.
		if ( Plugin_Constants::CRON_BUILD_FEED_CHUNK !== $action->get_hook() ) {
			return;
		}

		$args   = $action->get_args();
		$job_id = $args[0] ?? null;

		if ( ! $job_id ) {
			return;
		}

		// Initialize logger if needed.
		if ( ! $this->logger ) {
			$this->logger = new Logger( 'cron-hooks' );
		}

		// Get current progress to check/update retry count.
		$transient_key = Plugin_Constants::TRANSIENT_FEED_BUILD_PREFIX . $job_id;
		$progress      = get_transient( $transient_key );

		if ( false === $progress ) {
			$this->logger->warning( 'Cannot retry chunk build - progress not found for job: ' . $job_id );
			return;
		}

		// Check if job is still in a retriable state.
		if ( ! in_array( $progress['status'], array( 'building', 'uploading' ), true ) ) {
			$this->logger->info( 'Skipping retry - job ' . $job_id . ' is not in retriable status: ' . $progress['status'] );
			return;
		}

		$retry_count = $progress['retry_count'] ?? 0;
		$max_retries = Plugin_Constants::FEED_BUILD_MAX_RETRIES;

		// Log the failure.
		$error_message = $exception ? $exception->getMessage() : 'Unknown error (likely timeout)';
		$this->logger->warning(
			'Chunk build action failed',
			array(
				'job_id'      => $job_id,
				'action_id'   => $action_id,
				'retry_count' => $retry_count,
				'max_retries' => $max_retries,
				'error'       => $error_message,
			)
		);

		// Check if we've exceeded max retries.
		if ( $retry_count >= $max_retries ) {
			$this->logger->error(
				'Max retries exceeded for chunk build job: ' . $job_id . '. Marking as failed.'
			);

			// Update progress to failed status.
			$progress['status']    = 'failed';
			$progress['error']     = 'Max retries (' . $max_retries . ') exceeded. Last error: ' . $error_message;
			$progress['failed_at'] = current_time( 'c' );
			set_transient( $transient_key, $progress, DAY_IN_SECONDS );

			return;
		}

		// Increment retry count and update progress.
		$progress['retry_count']   = $retry_count + 1;
		$progress['last_retry_at'] = current_time( 'c' );
		$progress['last_error']    = $error_message;
		set_transient( $transient_key, $progress, DAY_IN_SECONDS );

		// Calculate delay with exponential backoff: base_delay * (retry_count + 1).
		// Retry 1: 60s, Retry 2: 120s, Retry 3: 180s.
		$delay = Plugin_Constants::FEED_BUILD_RETRY_DELAY * ( $retry_count + 1 );

		// Reschedule the chunk build action.
		as_schedule_single_action(
			time() + $delay,
			Plugin_Constants::CRON_BUILD_FEED_CHUNK,
			array( $job_id ),
			'momcs_feed_build'
		);

		$this->logger->info(
			'Rescheduled chunk build action',
			array(
				'job_id'       => $job_id,
				'retry'        => $retry_count + 1,
				'delay'        => $delay . 's',
				'scheduled_at' => gmdate( 'Y-m-d H:i:s', time() + $delay ),
			)
		);
	}
}
