<?php
/**
 * Batch processor for product sync via WP CRON.
 *
 * @package KwiroAISalesAgent
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class Kwiro_Batch_Processor
 *
 * Processes product sync in batches via WP CRON with adaptive batch sizing.
 */
class Kwiro_Batch_Processor {

	/**
	 * Product Sync instance.
	 *
	 * @var Kwiro_Product_Sync
	 */
	private $product_sync;

	/**
	 * Minimum batch size.
	 */
	const MIN_BATCH_SIZE = 10;

	/**
	 * Maximum batch size.
	 */
	const MAX_BATCH_SIZE = 200;

	/**
	 * Target processing time in seconds.
	 */
	const TARGET_TIME = 10;

	/**
	 * Maximum processing time in seconds.
	 */
	const MAX_TIME = 25;

	/**
	 * Constructor.
	 *
	 * @param Kwiro_Product_Sync $product_sync Product sync instance.
	 */
	public function __construct( $product_sync ) {
		$this->product_sync = $product_sync;
	}

	/**
	 * Process the next batch of products.
	 *
	 * Called by WP CRON via the 'kwiro_batch_sync' action.
	 */
	public function process_batch() {
		// Prevent concurrent sync execution (race condition guard).
		if ( get_transient( 'kwiro_sync_lock' ) ) {
			return;
		}
		set_transient( 'kwiro_sync_lock', true, 120 );

		$sync_status = get_option( 'kwiro_sync_status', array() );

		// Only process if a sync is in progress.
		if ( ( $sync_status['status'] ?? '' ) !== 'syncing' ) {
			delete_transient( 'kwiro_sync_lock' );

			// Emit a heartbeat to prove the cron is alive — but throttle to
			// at most one per 6 hours so the sync_events table doesn't grow
			// linearly with cron ticks. Without this, a healthy cron is
			// invisible in Sync History because there's nothing to do.
			$last_heartbeat = (int) get_option( 'kwiro_cron_last_heartbeat', 0 );
			if ( ( time() - $last_heartbeat ) > ( 6 * 3600 ) ) {
				update_option( 'kwiro_cron_last_heartbeat', time(), false );
				$client = new Kwiro_API_Client();
				$client->log_sync_event(
					'cron_fired',
					array(
						'trigger_source' => 'cron',
						'message'        => 'Hourly sync poll — no products needed updating',
					)
				);
			}
			return;
		}

		// Back-compat: if the sync_status was set by an older plugin build that
		// used `current_page`, translate it into `offset` on the fly.
		$offset     = isset( $sync_status['offset'] )
			? (int) $sync_status['offset']
			: ( isset( $sync_status['current_page'] )
				? max( 0, ( (int) $sync_status['current_page'] - 1 ) * 50 )
				: 0 );
		$batch_size = $this->get_adaptive_batch_size();
		$start_time = microtime( true );

		// Log the CRON fire once per sync run, so the dashboard can show
		// "WP CRON fired" entries in the sync history.
		$trigger_source = $sync_status['trigger_source'] ?? 'cron';
		if ( 0 === $offset && 'cron' === $trigger_source ) {
			$client = new Kwiro_API_Client();
			$client->log_sync_event( 'cron_fired', array( 'trigger_source' => 'cron' ) );
		}

		$result = $this->product_sync->get_products_by_offset( $offset, $batch_size );
		$total  = (int) $result['total'];

		if ( empty( $result['products'] ) ) {
			$this->complete_sync( (int) ( $sync_status['products_count'] ?? $total ) );
			return;
		}

		$total_batches = max( 1, (int) ceil( $total / max( 1, $batch_size ) ) );
		$batch_number  = (int) floor( $offset / max( 1, $batch_size ) ) + 1;
		$sync_result   = $this->product_sync->sync_batch(
			$result['products'],
			array(
				'batch_number'   => $batch_number,
				'total_batches'  => $total_batches,
				'trigger_source' => $trigger_source,
			)
		);
		$elapsed       = microtime( true ) - $start_time;

		// Update adaptive batch size based on processing time.
		$this->update_batch_size( $batch_size, $elapsed );

		if ( is_wp_error( $sync_result ) ) {
			$this->handle_sync_error( $sync_status, $sync_result );
			return;
		}

		$synced_count = ( $sync_status['products_count'] ?? 0 ) + count( $result['products'] );

		if ( $result['has_more'] ) {
			// Schedule next batch.
			update_option(
				'kwiro_sync_status',
				array(
					'last_sync'      => $sync_status['last_sync'] ?? null,
					'products_count' => $synced_count,
					'status'         => 'syncing',
					'offset'         => $offset + count( $result['products'] ),
					'total'          => $total,
					'trigger_source' => $trigger_source,
				)
			);

			delete_transient( 'kwiro_sync_lock' );
			wp_schedule_single_event( time() + 5, 'kwiro_batch_sync' );
		} else {
			delete_transient( 'kwiro_sync_lock' );
			$this->complete_sync( $synced_count );
		}
	}

	/**
	 * Mark sync as complete.
	 *
	 * @param int $products_count Total products synced. When omitted, keeps
	 *                            whatever count was already in sync_status so
	 *                            finalize calls don't overwrite the real count
	 *                            with 0.
	 */
	private function complete_sync( $products_count = null ) {
		$prev = get_option( 'kwiro_sync_status', array() );
		if ( null === $products_count ) {
			$products_count = (int) ( $prev['products_count'] ?? 0 );
		}
		update_option(
			'kwiro_sync_status',
			array(
				'last_sync'      => current_time( 'mysql', true ),
				'products_count' => $products_count,
				'status'         => 'complete',
				'total'          => (int) ( $prev['total'] ?? $products_count ),
			)
		);

		// Tell the cloud the batch loop ended so it flips the store to complete
		// and runs a final embedding pass. Silent on failure — the cloud also
		// embeds after every batch, so the catalog won't stay unindexed.
		// Piggyback the store's free-shipping threshold (if any) so the
		// widget's MiniCart can show the right "X away from free shipping"
		// number. Sync is the natural place to push it — same trigger the
		// admin already uses, no extra round-trip.
		$client    = new Kwiro_API_Client();
		$threshold = $this->product_sync->get_free_shipping_threshold();
		$client->sync_finalize( array( 'free_shipping_threshold' => $threshold ) );
	}

	/**
	 * Handle a sync error.
	 *
	 * @param array    $sync_status Current sync status.
	 * @param WP_Error $error       The error.
	 */
	private function handle_sync_error( $sync_status, $error ) {
		delete_transient( 'kwiro_sync_lock' );
		$retry_count = ( $sync_status['retry_count'] ?? 0 ) + 1;

		if ( $retry_count >= 3 ) {
			update_option(
				'kwiro_sync_status',
				array(
					'last_sync'      => $sync_status['last_sync'] ?? null,
					'products_count' => $sync_status['products_count'] ?? 0,
					'status'         => 'error',
					'error_message'  => $error->get_error_message(),
				)
			);
			return;
		}

		// Retry with exponential backoff.
		$sync_status['retry_count'] = $retry_count;
		$sync_status['status']      = 'syncing';
		update_option( 'kwiro_sync_status', $sync_status );

		$delay = pow( 2, $retry_count ) * 30; // 60s, 120s, 240s.
		wp_schedule_single_event( time() + $delay, 'kwiro_batch_sync' );
	}

	/**
	 * Get adaptive batch size based on previous performance.
	 *
	 * @return int Batch size.
	 */
	public function get_adaptive_batch_size() {
		$settings   = get_option( 'kwiro_settings', array() );
		$batch_size = $settings['batch_size'] ?? 50;

		return max( self::MIN_BATCH_SIZE, min( self::MAX_BATCH_SIZE, (int) $batch_size ) );
	}

	/**
	 * Update batch size based on processing time.
	 *
	 * @param int   $current_size Current batch size.
	 * @param float $elapsed      Processing time in seconds.
	 */
	private function update_batch_size( $current_size, $elapsed ) {
		if ( $elapsed <= 0 ) {
			return;
		}

		if ( $elapsed > self::MAX_TIME ) {
			// Too slow, reduce batch size.
			$new_size = max( self::MIN_BATCH_SIZE, (int) ( $current_size * 0.5 ) );
		} elseif ( $elapsed < self::TARGET_TIME * 0.5 ) {
			// Very fast, increase batch size.
			$new_size = min( self::MAX_BATCH_SIZE, (int) ( $current_size * 1.5 ) );
		} elseif ( $elapsed < self::TARGET_TIME ) {
			// Slightly fast, increase a little.
			$new_size = min( self::MAX_BATCH_SIZE, (int) ( $current_size * 1.2 ) );
		} else {
			// Within target range.
			return;
		}

		$settings               = get_option( 'kwiro_settings', array() );
		$settings['batch_size'] = $new_size;
		update_option( 'kwiro_settings', $settings );
	}

	/**
	 * Check if a sync is currently in progress.
	 *
	 * @return bool
	 */
	public function is_syncing() {
		$status = get_option( 'kwiro_sync_status', array() );
		return ( $status['status'] ?? '' ) === 'syncing';
	}

	/**
	 * Get sync progress as a percentage.
	 *
	 * @return float Progress percentage (0-100).
	 */
	public function get_progress() {
		$status = get_option( 'kwiro_sync_status', array() );

		if ( ( $status['status'] ?? '' ) !== 'syncing' ) {
			return ( $status['status'] ?? '' ) === 'complete' ? 100.0 : 0.0;
		}

		$total = $status['total'] ?? 0;
		$done  = $status['products_count'] ?? 0;

		if ( $total <= 0 ) {
			return 0.0;
		}

		return round( ( $done / $total ) * 100, 1 );
	}
}
