<?php
namespace TeknoTok\AiTranslator\Queue;

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

use TeknoTok\AiTranslator\Database\JobRepository;
use TeknoTok\AiTranslator\Database\MemoryRepository;
use TeknoTok\AiTranslator\Providers\ProviderFactory;
use TeknoTok\AiTranslator\Utils\Logger;

/**
 * Class to manage background job queue processing.
 */
class QueueManager {
	/**
	 * Whether the current run was started from an admin shutdown hook.
	 *
	 * @var bool
	 */
	private $processing_on_shutdown = false;

	/**
	 * Constructor. Registers cron action hook and AJAX action hooks.
	 */
	public function __construct() {
		add_filter( 'cron_schedules', array( $this, 'add_cron_schedules' ) );
		add_action( 'teknotok_ait_queue_worker', array( $this, 'process_queue' ) );

		// Schedule queue cron if not active
		if ( ! wp_next_scheduled( 'teknotok_ait_queue_worker' ) ) {
			wp_schedule_event( time(), 'every_minute', 'teknotok_ait_queue_worker' );
		}

		// Hook queue execution to shutdown to guarantee background execution even if cron/loopbacks fail
		add_action( 'shutdown', array( $this, 'maybe_process_queue_on_shutdown' ) );
	}

	/**
	 * Register custom cron intervals.
	 *
	 * @param array $schedules Existing schedules.
	 * @return array
	 */
	public function add_cron_schedules( $schedules ) {
		$schedules['every_minute'] = array(
			'interval' => 60,
			'display'  => 'Every Minute',
		);
		return $schedules;
	}

	/**
	 * Check if SQLite database integration is active.
	 *
	 * @return bool True if SQLite is active, false otherwise.
	 */
	public static function is_sqlite_active() {
		global $wpdb;
		return (
			class_exists( 'WP_SQLite_DB' ) ||
			defined( 'SQLITE_DB_DIR' ) ||
			( isset( $wpdb ) && strpos( get_class( $wpdb ), 'SQLite' ) !== false )
		);
	}

	/**
	 * Trigger background queue processing asynchronously.
	 *
	 * @return void
	 */
	public static function trigger_async_queue() {
		$license = \TeknoTok\AiTranslator\Integrations\LicenseManager::get_license_data();
		$status  = $license['status'] ?? 'free';
		$is_free = ( 'free' === $status );

		if ( ! \TeknoTok\AiTranslator\Integrations\LicenseManager::is_premium_active() && ! $is_free ) {
			return;
		}

		// Prevent execution if not ready for translations (unlicensed or unconfigured)
		if ( ! \TeknoTok\AiTranslator\Plugin::is_ready_for_translations() ) {
			return;
		}

		if ( '1' === get_option( 'teknotok_ait_queue_paused', '0' ) ) {
			return;
		}

		// Prevent redundant processing requests if lock is already active
		$existing_lock = (int) get_option( 'teknotok_ait_queue_lock_expires', 0 );
		if ( $existing_lock > time() ) {
			return;
		}

		// 1. Try Action Scheduler (highly robust background library bundled with WooCommerce)
		if ( function_exists( 'as_enqueue_async_action' ) && ! self::is_sqlite_active() ) {
			as_enqueue_async_action( 'teknotok_ait_queue_worker', array(), 'teknotok-ait' );
			return;
		}

		if ( self::is_sqlite_active() ) {
			if ( ! wp_next_scheduled( 'teknotok_ait_queue_worker' ) ) {
				wp_schedule_single_event( time() + 60, 'teknotok_ait_queue_worker' );
			}
			return;
		}

		// 2. Fallback: Trigger standard WP Cron
		if ( ! wp_next_scheduled( 'teknotok_ait_queue_worker' ) ) {
			wp_schedule_single_event( time(), 'teknotok_ait_queue_worker' );
		}
		spawn_cron();
	}

	/**
	 * Run queue processing on shutdown under safe conditions.
	 *
	 * @return void
	 */
	public function maybe_process_queue_on_shutdown() {
		// Never run remote translation work during synchronous frontend rendering.
		$should_run = is_admin();

		if ( $should_run ) {
			// If fastcgi_finish_request exists, close client connection first so the user doesn't wait
			if ( function_exists( 'fastcgi_finish_request' ) ) {
				fastcgi_finish_request();
			}
			$this->processing_on_shutdown = true;
			$this->process_queue();
			$this->processing_on_shutdown = false;
		}
	}

	/**
	 * Run queue processing iteration.
	 *
	 * @return void
	 */
	public function process_queue() {
		// Check if the queue is paused
		if ( '1' === get_option( 'teknotok_ait_queue_paused', '0' ) ) {
			return;
		}

		// Prevent parallel execution conflicts using a robust distributed lock verification
		$lock_session = wp_generate_password( 12, false );
		$now = time();
		$lock_expires = $now + 50; // Lock expires in 50 seconds

		$existing_lock = (int) get_option( 'teknotok_ait_queue_lock_expires', 0 );
		if ( $existing_lock > $now ) {
			return;
		}

		// Try to acquire the lock
		update_option( 'teknotok_ait_queue_lock_session', $lock_session );
		update_option( 'teknotok_ait_queue_lock_expires', $lock_expires );

		// Verify we won the race (wait 50ms to let concurrent updates settle)
		usleep( 50000 );
		if ( get_option( 'teknotok_ait_queue_lock_session' ) !== $lock_session ) {
			return; // Lost the race!
		}

		// Prevent execution if not ready for translations (unlicensed or unconfigured)
		$license = \TeknoTok\AiTranslator\Integrations\LicenseManager::get_license_data();
		$status  = $license['status'] ?? 'free';
		$is_free = ( 'free' === $status );

		if ( ( ! \TeknoTok\AiTranslator\Integrations\LicenseManager::is_premium_active() && ! $is_free ) || ! \TeknoTok\AiTranslator\Plugin::is_ready_for_translations() ) {
			delete_option( 'teknotok_ait_queue_lock_expires' );
			delete_option( 'teknotok_ait_queue_lock_session' );
			return;
		}

		try {
			// Recover/fail any stuck jobs first to keep the queue clean and reliable
			JobRepository::reset_stuck_jobs();

			$settings = get_option( 'teknotok_ait_settings', array() );
			$batch_size = min( 50, max( 1, intval( $settings['queue_batch_size'] ?? 10 ) ) );
			$jobs = JobRepository::get_pending_jobs( $batch_size );
			if ( $this->processing_on_shutdown ) {
				$jobs = array_values( array_filter(
					$jobs,
					function( $job ) {
						return 'frontend_warmup' !== $job->object_type;
					}
				) );
			}
			if ( empty( $jobs ) ) {
				delete_option( 'teknotok_ait_queue_lock_expires' );
				delete_option( 'teknotok_ait_queue_lock_session' );
				return;
			}

			$provider = ProviderFactory::get_provider();
			$source_lang = $settings['source_language'] ?? 'en';

			$string_jobs = array();
			foreach ( $jobs as $job ) {
				if ( 'string' === $job->object_type ) {
					$string_jobs[] = $job;
				}
			}

			if ( ! empty( $string_jobs ) ) {
				$this->process_string_jobs_batch( $string_jobs, $provider, $source_lang );
			}

			foreach ( $jobs as $job ) {
				if ( 'string' === $job->object_type ) {
					continue;
				}
				JobRepository::update_status( $job->id, 'processing' );

				try {
					$this->process_single_job( $job, $provider, $source_lang );
					JobRepository::update_status( $job->id, 'completed' );
				} catch ( \Exception $e ) {
					Logger::log( sprintf( 'Job ID %d failed: %s', $job->id, $e->getMessage() ), 'error' );
					JobRepository::update_status( $job->id, 'failed', $e->getMessage() );

					if ( self::is_provider_limit_error( $e ) ) {
						self::pause_for_provider_limit( $e->getMessage() );
						delete_option( 'teknotok_ait_queue_lock_expires' );
						delete_option( 'teknotok_ait_queue_lock_session' );
						return; // Stop queue processing immediately
					}
				}
			}
		} catch ( \Exception $e ) {
			Logger::log( 'Queue worker exception: ' . $e->getMessage(), 'error' );
			if ( self::is_provider_limit_error( $e ) ) {
				self::pause_for_provider_limit( $e->getMessage() );
				delete_option( 'teknotok_ait_queue_lock_expires' );
				delete_option( 'teknotok_ait_queue_lock_session' );
				return;
			}
		}

		delete_option( 'teknotok_ait_queue_lock_expires' );
		delete_option( 'teknotok_ait_queue_lock_session' );

		// If more pending jobs exist, self-spawn the queue worker to process the next batch asynchronously
		$more_jobs = JobRepository::get_pending_jobs( 1 );
		if ( ! empty( $more_jobs ) ) {
			self::trigger_async_queue();
		}
	}

	/**
	 * Process frontend/theme string jobs in batches grouped by target language.
	 *
	 * @param array  $jobs        String job rows.
	 * @param object $provider    Active AI translation provider instance.
	 * @param string $source_lang Source language locale code.
	 * @return void
	 * @throws \Exception When a batch fails.
	 */
	private function process_string_jobs_batch( $jobs, $provider, $source_lang ) {
		$groups = array();
		foreach ( $jobs as $job ) {
			$groups[ $job->target_lang ][] = $job;
		}

		foreach ( $groups as $target_lang => $group_jobs ) {
			$payload = array();
			foreach ( $group_jobs as $job ) {
				JobRepository::update_status( $job->id, 'processing' );
				$source_text = isset( $job->source_text ) ? trim( (string) $job->source_text ) : '';
				if ( empty( $source_text ) || false !== MemoryRepository::get_translation( $source_text, $target_lang ) ) {
					JobRepository::update_status( $job->id, 'completed' );
					continue;
				}
				$payload[ (string) $job->id ] = $source_text;
			}

			if ( empty( $payload ) ) {
				continue;
			}

			try {
				$translations = $provider->translate( $payload, $source_lang, $target_lang );
				if ( ! is_array( $translations ) ) {
					throw new \Exception( __( 'Provider returned an invalid string translation batch.', 'teknotok-ai-translator-pro' ) );
				}

				foreach ( $payload as $job_id => $source_text ) {
					if ( isset( $translations[ $job_id ] ) && is_string( $translations[ $job_id ] ) && '' !== trim( $translations[ $job_id ] ) ) {
						MemoryRepository::save_translation( $source_text, $translations[ $job_id ], $source_lang, $target_lang );
						JobRepository::update_status( (int) $job_id, 'completed' );
					} else {
						JobRepository::update_status( (int) $job_id, 'failed', __( 'Missing translated string in provider response.', 'teknotok-ai-translator-pro' ) );
					}
				}
			} catch ( \Exception $e ) {
				foreach ( array_keys( $payload ) as $job_id ) {
					JobRepository::update_status( (int) $job_id, 'failed', $e->getMessage() );
				}
				throw $e;
			}
		}
	}

	/**
	 * Determine whether an exception is a quota/license/provider limit failure.
	 *
	 * @param \Exception $exception Queue exception.
	 * @return bool True when the queue should pause.
	 */
	public static function is_provider_limit_error( $exception ) {
		$message = $exception->getMessage();

		return (
			402 === (int) $exception->getCode()
			|| ( false !== stripos( $message, 'quota' ) && false !== stripos( $message, 'exceeded' ) )
			|| ( false !== stripos( $message, 'limit' ) && false !== stripos( $message, 'exceeded' ) )
			|| ( false !== stripos( $message, 'word' ) && false !== stripos( $message, 'limit' ) && false !== stripos( $message, 'exceeded' ) )
			|| false !== stripos( $message, 'license' )
		);
	}

	/**
	 * Pause queue execution after a provider quota/license limit is reached.
	 *
	 * @param string $reason Pause reason.
	 * @return void
	 */
	public static function pause_for_provider_limit( $reason ) {
		update_option( 'teknotok_ait_queue_paused', '1', false );
		update_option( 'teknotok_ait_queue_pause_reason', sanitize_text_field( $reason ), false );
		update_option( 'teknotok_ait_queue_paused_at', current_time( 'mysql', true ), false );
	}

	/**
	 * Resume queue execution after license/quota has been fixed.
	 *
	 * @return int Number of quota failed jobs reset to pending.
	 */
	public static function resume_after_provider_limit() {
		$reset_jobs = JobRepository::reset_quota_failed_jobs();
		delete_option( 'teknotok_ait_queue_paused' );
		delete_option( 'teknotok_ait_queue_pause_reason' );
		delete_option( 'teknotok_ait_queue_paused_at' );
		delete_option( 'teknotok_ait_queue_lock_expires' );
		delete_option( 'teknotok_ait_queue_lock_session' );
		self::trigger_async_queue();

		return (int) $reset_jobs;
	}

	/**
	 * Retry failed jobs that were not caused by quota or license limits.
	 *
	 * @return int Number of reset jobs.
	 */
	public static function retry_failed_jobs() {
		$reset_jobs = JobRepository::reset_retryable_failed_jobs();
		delete_option( 'teknotok_ait_queue_lock_expires' );
		delete_option( 'teknotok_ait_queue_lock_session' );
		self::trigger_async_queue();

		return (int) $reset_jobs;
	}

	/**
	 * Process a single job based on its object type.
	 *
	 * @param object $job             Job row details.
	 * @param object $provider        Active AI translation provider instance.
	 * @param string $source_lang     Source language locale code.
	 * @return void
	 * @throws \Exception
	 */
	private function process_single_job( $job, $provider, $source_lang ) {
		$target_lang = $job->target_lang;

		if ( 'frontend_warmup' === $job->object_type ) {
			$this->process_frontend_warmup_job( $job );
			return;
		}

		if ( 'post' === $job->object_type ) {
			$post = get_post( $job->object_id );
			if ( ! $post ) {
				return;
			}

			// Gather fields that need translating (plain text fields)
			$payload = array();
			$fields = array(
				'title'   => $post->post_title,
				'excerpt' => $post->post_excerpt,
			);

			foreach ( $fields as $key => $value ) {
				if ( ! empty( $value ) && false === MemoryRepository::get_translation( $value, $target_lang ) ) {
					$payload[ $key ] = $value;
				}
			}

			if ( ! empty( $payload ) ) {
				$translations = $provider->translate( $payload, $source_lang, $target_lang );
				foreach ( $payload as $key => $original_value ) {
					if ( isset( $translations[ $key ] ) ) {
						MemoryRepository::save_translation( $original_value, $translations[ $key ], $source_lang, $target_lang );
					}
				}
			}

			// Parse and translate HTML content block-by-block
			if ( ! empty( $post->post_content ) && false === MemoryRepository::get_translation( $post->post_content, $target_lang ) ) {
				$translated_content = \TeknoTok\AiTranslator\Utils\HtmlParser::translate_html( $post->post_content, $source_lang, $target_lang, $provider );
				// Save macro-block translation to cache
				MemoryRepository::save_translation( $post->post_content, $translated_content, $source_lang, $target_lang );
			}

			// Post metadata translation
			$seo_whitelist = array(
				'_yoast_wpseo_title',
				'_yoast_wpseo_metadesc',
				'_metaseo_description',
				'rank_math_title',
				'rank_math_description',
				'rank_math_focus_keyword',
			);

			$all_meta = get_post_meta( $job->object_id );
			$meta_strings = array();

			if ( is_array( $all_meta ) ) {
				foreach ( $all_meta as $meta_key => $values ) {
					// Skip keys starting with _ unless whitelisted
					if ( strpos( $meta_key, '_' ) === 0 ) {
						if ( ! in_array( $meta_key, $seo_whitelist, true ) ) {
							continue;
						}
					}

					// Use single meta helper to get deserialized value
					$meta_val = get_post_meta( $job->object_id, $meta_key, true );

					if ( is_array( $meta_val ) ) {
						// Check if single-level (flat) array
						$is_flat = true;
						foreach ( $meta_val as $sub_val ) {
							if ( is_array( $sub_val ) || is_object( $sub_val ) ) {
								$is_flat = false;
								break;
							}
						}
						if ( $is_flat ) {
							foreach ( $meta_val as $sub_val ) {
								if ( is_string( $sub_val ) && ! empty( trim( $sub_val ) ) && ! is_numeric( $sub_val ) ) {
									$meta_strings[] = $sub_val;
								}
							}
						}
					} elseif ( is_string( $meta_val ) && ! empty( trim( $meta_val ) ) && ! is_numeric( $meta_val ) ) {
						$meta_strings[] = $meta_val;
					}
				}
			}

			$meta_strings = array_unique( $meta_strings );
			$meta_payload = array();
			$idx = 0;

			foreach ( $meta_strings as $str ) {
				if ( false === MemoryRepository::get_translation( $str, $target_lang ) ) {
					$meta_payload[ 'meta_' . $idx++ ] = $str;
				}
			}

			if ( ! empty( $meta_payload ) ) {
				$meta_translations = $provider->translate( $meta_payload, $source_lang, $target_lang );
				if ( is_array( $meta_translations ) ) {
					foreach ( $meta_payload as $key => $original_value ) {
						if ( isset( $meta_translations[ $key ] ) ) {
							MemoryRepository::save_translation( $original_value, $meta_translations[ $key ], $source_lang, $target_lang );
						}
					}
				}
			}
		}

		if ( 'taxonomy' === $job->object_type ) {
			$term = get_term( $job->object_id );
			if ( ! $term || is_wp_error( $term ) ) {
				return;
			}

			$payload = array();
			$fields = array(
				'name'        => $term->name,
				'description' => $term->description,
			);

			foreach ( $fields as $key => $value ) {
				if ( ! empty( $value ) && false === MemoryRepository::get_translation( $value, $target_lang ) ) {
					$payload[ $key ] = $value;
				}
			}

			if ( ! empty( $payload ) ) {
				$translations = $provider->translate( $payload, $source_lang, $target_lang );
				foreach ( $payload as $key => $original_value ) {
					if ( isset( $translations[ $key ] ) ) {
						MemoryRepository::save_translation( $original_value, $translations[ $key ], $source_lang, $target_lang );
					}
				}
			}
		}

		if ( 'string' === $job->object_type ) {
			$source_text = isset( $job->source_text ) ? trim( (string) $job->source_text ) : '';
			if ( empty( $source_text ) || false !== MemoryRepository::get_translation( $source_text, $target_lang ) ) {
				return;
			}

			$translated = $provider->translate( $source_text, $source_lang, $target_lang );
			if ( is_string( $translated ) && '' !== trim( $translated ) ) {
				MemoryRepository::save_translation( $source_text, $translated, $source_lang, $target_lang );
			}
		}
	}

	/**
	 * Render a translated frontend URL in the background so output-buffer parsing can queue theme strings.
	 *
	 * @param object $job Job row details.
	 * @return void
	 * @throws \Exception When the warmup request fails.
	 */
	private function process_frontend_warmup_job( $job ) {
		$source_url  = isset( $job->source_text ) ? trim( (string) $job->source_text ) : '';
		$target_lang = isset( $job->target_lang ) ? sanitize_key( $job->target_lang ) : '';

		if ( empty( $source_url ) || empty( $target_lang ) ) {
			return;
		}

		$target_url = \TeknoTok\AiTranslator\Plugin::get_translated_url_for_source( $source_url, $target_lang );
		$target_url = add_query_arg( 'teknotok_ait_warmup', '1', $target_url );

		$response = wp_remote_get(
			$target_url,
			array(
				'timeout'     => 20,
				'redirection' => 3,
				'blocking'    => true,
				'headers'     => array(
					'X-TeknoTok-Warmup' => '1',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			throw new \Exception( $response->get_error_message() );
		}

		$status = (int) wp_remote_retrieve_response_code( $response );
		if ( $status >= 400 ) {
			throw new \Exception( sprintf( 'Frontend warmup failed with HTTP %d for %s.', $status, esc_url_raw( $target_url ) ) );
		}
	}
}
