<?php
/**
 * Background generation queue.
 *
 * @package AIAltTextGenerator
 * @since 1.4.0
 */

namespace AIAltTextGenerator;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * WP-Cron backed queue for auto-upload and media library jobs.
 *
 * @since 1.4.0
 */
class Queue {

	public const CRON_HOOK       = 'wpps_ai_alt_process_queue';
	public const MAX_QUEUE_ITEMS = 500;

	/**
	 * @return array<string, mixed>
	 */
	public static function getState(): array {
		self::bustQueueCache();
		$state = get_option( Settings::QUEUE_OPTION, [] );
		if ( ! is_array( $state ) ) {
			$state = [];
		}

		return wp_parse_args(
			$state,
			[
				'status'       => 'idle',
				'items'        => [],
				'processed'    => 0,
				'failed'       => 0,
				'skipped'      => 0,
				'failed_items' => [],
				'keywords'     => [ 'seo' => [], 'negative' => [] ],
				'user_id'      => 0,
				'created'      => 0,
			]
		);
	}

	/**
	 * @param list<int> $attachment_ids
	 * @param bool      $force          Regenerate even when alt text exists.
	 * @param int|null  $schedule_delay Seconds until the queue cron runs (null = default).
	 * @param array{seo?: list<string>, negative?: list<string>}|null $keywords Optional SEO keywords for this queue job.
	 */
	public static function enqueue( array $attachment_ids, bool $force = false, ?int $schedule_delay = null, ?array $keywords = null ): int {
		$attachment_ids = array_values( array_unique( array_filter( array_map( 'intval', $attachment_ids ) ) ) );
		if ( $attachment_ids === [] ) {
			return 0;
		}

		$state = self::getState();
		$existing = [];
		foreach ( $state['items'] as $item ) {
			if ( is_array( $item ) && isset( $item['id'] ) ) {
				$existing[ (int) $item['id'] ] = true;
			}
		}

		$added = 0;
		foreach ( $attachment_ids as $id ) {
			if ( isset( $existing[ $id ] ) ) {
				continue;
			}
			if ( count( $state['items'] ) >= self::MAX_QUEUE_ITEMS ) {
				break;
			}
			$state['items'][] = [
				'id'       => $id,
				'attempts' => 0,
				'force'    => $force,
			];
			++$added;
		}

		if ( $keywords !== null ) {
			$state['keywords'] = [
				'seo'      => is_array( $keywords['seo'] ?? null ) ? array_values( $keywords['seo'] ) : [],
				'negative' => is_array( $keywords['negative'] ?? null ) ? array_values( $keywords['negative'] ) : [],
			];
		}

		if ( $added > 0 ) {
			$state['status']  = 'running';
			$state['created'] = time();
			if ( empty( $state['user_id'] ) ) {
				$state['user_id'] = get_current_user_id();
			}
			self::persistState( $state );
			self::schedule( $schedule_delay ?? 30 );
		}

		return $added;
	}

	/**
	 * @return list<array{id: int, message: string, filename?: string}>
	 */
	public static function getFailedItems(): array {
		$state = self::getState();
		$items = is_array( $state['failed_items'] ?? null ) ? $state['failed_items'] : [];

		return array_values(
			array_filter(
				array_map(
					static function ( $item ): array {
						if ( ! is_array( $item ) ) {
							return [];
						}

						return [
							'id'       => (int) ( $item['id'] ?? 0 ),
							'message'  => (string) ( $item['message'] ?? '' ),
							'filename' => (string) ( $item['filename'] ?? '' ),
						];
					},
					$items
				),
				static fn( array $item ): bool => ( $item['id'] ?? 0 ) > 0
			)
		);
	}

	public static function retryFailed(): int {
		$state        = self::getState();
		$failed_items = self::getFailedItems();
		if ( $failed_items === [] ) {
			return 0;
		}

		$ids = array_values(
			array_filter(
				array_map(
					static fn( array $item ): int => (int) ( $item['id'] ?? 0 ),
					$failed_items
				)
			)
		);

		$state['failed_items'] = [];
		$state['status']       = 'running';
		self::persistState( $state );

		return self::enqueue( $ids, true );
	}

	public static function cancel(): void {
		delete_option( Settings::QUEUE_OPTION );
		self::bustQueueCache();
		wp_clear_scheduled_hook( self::CRON_HOOK );
	}

	/**
	 * @return array{processed: int, failed: int, skipped: int, remaining: int}
	 */
	public static function processBatch(): array {
		$settings   = Settings::get();
		$batch_size = Settings::clampBatchSize( (int) ( $settings['batch_size'] ?? 5 ) );
		$state      = self::getState();

		if ( ( $state['status'] ?? 'idle' ) === 'paused' || empty( $state['items'] ) ) {
			return [
				'processed' => 0,
				'failed'    => 0,
				'skipped'   => 0,
				'remaining' => count( $state['items'] ?? [] ),
			];
		}

		$keywords = is_array( $state['keywords'] ?? null ) ? $state['keywords'] : [ 'seo' => [], 'negative' => [] ];
		BulkGenerator::applyKeywords(
			[
				'seo'      => is_array( $keywords['seo'] ?? null ) ? array_values( $keywords['seo'] ) : [],
				'negative' => is_array( $keywords['negative'] ?? null ) ? array_values( $keywords['negative'] ) : [],
			]
		);

		$result = [
			'processed' => 0,
			'failed'    => 0,
			'skipped'   => 0,
			'remaining' => 0,
		];

		$items = array_slice( $state['items'], 0, $batch_size );
		$state['items'] = array_slice( $state['items'], $batch_size );

		$owner_id = (int) ( $state['user_id'] ?? 0 );

		foreach ( $items as $item ) {
			$id = (int) ( $item['id'] ?? 0 );
			if ( $id <= 0 ) {
				continue;
			}

			// Recheck object capability for the user who queued the job (cron has no current user).
			if ( $owner_id > 0 && ! user_can( $owner_id, 'edit_post', $id ) ) {
				++$result['skipped'];
				++$state['skipped'];
				continue;
			}

			$outcome = GenerationService::generateForAttachment( $id, ! empty( $item['force'] ) );
			if ( $outcome['status'] === 'success' ) {
				++$result['processed'];
				++$state['processed'];
			} elseif ( $outcome['status'] === 'skipped' ) {
				++$result['skipped'];
				++$state['skipped'];
			} else {
				++$result['failed'];
				++$state['failed'];

				$code       = (string) ( $outcome['code'] ?? '' );
				$message    = (string) ( $outcome['message'] ?? __( 'Generation failed.', 'wp-powersuite' ) );
				$retryable  = array_key_exists( 'retryable', $outcome )
					? ! empty( $outcome['retryable'] )
					: ! GenerationError::isPermanent( $code ) && ! GenerationError::isPermanentMessage( $message );
				$attempts   = (int) ( $item['attempts'] ?? 0 ) + 1;

				if ( $retryable && $attempts < 3 ) {
					$state['items'][] = [
						'id'       => $id,
						'attempts' => $attempts,
						'force'    => ! empty( $item['force'] ),
					];
				} else {
					if ( ! isset( $state['failed_items'] ) || ! is_array( $state['failed_items'] ) ) {
						$state['failed_items'] = [];
					}
					$state['failed_items'][] = [
						'id'       => $id,
						'message'  => $message,
						'code'     => $code,
						'filename' => basename( (string) get_attached_file( $id ) ),
					];
					Diagnostics::recordBulkError(
						[
							'code'          => $code,
							'message'       => $message,
							'attachment_id' => $id,
						]
					);
				}
			}
		}

		BulkGenerator::clearActiveKeywords();

		$state['status'] = empty( $state['items'] ) ? 'idle' : 'running';
		self::persistState( $state );

		$result['remaining'] = count( $state['items'] );

		if ( $result['remaining'] > 0 && $state['status'] === 'running' ) {
			self::schedule( 15 );
		} else {
			wp_clear_scheduled_hook( self::CRON_HOOK );
		}

		Scanner::invalidateCache();

		return $result;
	}

	public static function schedule( int $delay = 30 ): void {
		$when     = time() + max( 1, $delay );
		$existing = wp_next_scheduled( self::CRON_HOOK );

		if ( ! $existing ) {
			wp_schedule_single_event( $when, self::CRON_HOOK );
			return;
		}

		if ( $existing > $when ) {
			wp_clear_scheduled_hook( self::CRON_HOOK );
			wp_schedule_single_event( $when, self::CRON_HOOK );
		}
	}

	/**
	 * @return array<string, mixed>
	 */
	public static function getStatus(): array {
		$state     = self::getState();
		$remaining = count( $state['items'] ?? [] );
		$total     = (int) ( $state['processed'] ?? 0 ) + (int) ( $state['failed'] ?? 0 ) + (int) ( $state['skipped'] ?? 0 ) + $remaining;
		$done      = max( 0, $total - $remaining );

		return [
			'status'       => (string) ( $state['status'] ?? 'idle' ),
			'total'        => $total,
			'done'         => $done,
			'current'      => $done,
			'remaining'    => $remaining,
			'processed'    => (int) ( $state['processed'] ?? 0 ),
			'failed'       => (int) ( $state['failed'] ?? 0 ),
			'skipped'      => (int) ( $state['skipped'] ?? 0 ),
			'percent'      => $total > 0 ? min( 100, (int) round( ( $done / $total ) * 100 ) ) : 100,
			'limit_reached'  => Usage::isLimitReached(),
		];
	}

	/**
	 * @param array<string, mixed> $state
	 */
	private static function persistState( array $state ): void {
		update_option( Settings::QUEUE_OPTION, $state, false );
		self::bustQueueCache();
	}

	private static function bustQueueCache(): void {
		wp_cache_delete( Settings::QUEUE_OPTION, 'options' );
		wp_cache_delete( 'notoptions', 'options' );
		wp_cache_delete( 'alloptions', 'options' );
	}
}
