<?php

namespace ShopManagerPro\Jobs;

use ShopManagerPro\Jobs\DTO\Generated\JobProductsItemChanges;
use ShopManagerPro\Jobs\DTO\Generated\JobProductsItemErrorAlternative1;
use ShopManagerPro\Jobs\DTO\Generated\JobProductsItemErrorAlternative2;
use ShopManagerPro\Jobs\DTO\Generated\JobProductsItemStatus;
use ShopManagerPro\Jobs\DTO\Generated\JobStatus;
use ShopManagerPro\Products\DTO\ProductUpdateException;
use ShopManagerPro\Products\ProductUpdateService;
use ShopManagerPro\Shared\DatabaseManagerService;
use ShopManagerPro\Shared\Utils\JSON;

class JobProcessorService {
	private const BATCH_SIZE = 20;
	private const MAX_EXECUTION_TIME_SECONDS = 15;

	public static function init() {
		add_action('shopmanagerpro_process_queue', [self::class, 'processQueue']);
	}

	public static function processQueue() {
		$nextJobId = JobRepository::findNextRunnableJob();
		if (!$nextJobId) {
			\ShopManagerPro\Shared\Utils\Logger::info('No runnable jobs found in JobProcessor');

			return;
		} JobRepository::updateJobStatus($nextJobId, JobStatus::running);
		self::processJob($nextJobId);
	}

	public static function processJob(int $jobId) {
		$startTime = intval($_SERVER['REQUEST_TIME']);
		$maxExecutionTime = self::MAX_EXECUTION_TIME_SECONDS;
		try {
			$job = JobRepository::getJob($jobId);
			if (!$job) {
				\ShopManagerPro\Shared\Utils\Logger::error("Job with ID $jobId not found", ['job_id' => $jobId]);

				return;
			} if ($job->getStatus() === JobStatus::stopped) {
				\ShopManagerPro\Shared\Utils\Logger::info("Job $jobId has been stopped, stopping processing", ['job_id' => $jobId]);

				return;
			} while (true) {
				$currentJob = JobRepository::getJob($jobId);
				if ($currentJob && $currentJob->getStatus() === JobStatus::stopped) {
					\ShopManagerPro\Shared\Utils\Logger::info("Job $jobId has been stopped during processing, stopping immediately", ['job_id' => $jobId]);

					return;
				} $batch = self::getBatch($jobId);
				if (empty($batch)) {
					self::finalizeJob($jobId);
					break;
				} \ShopManagerPro\Shared\Utils\Logger::info('Processing batch for job', ['job_id' => $jobId, 'batch_size' => count($batch)]);
				foreach ($batch as $item) {
					$changesArray = JSON::decode($item->changes);
					$changes = JobProductsItemChanges::buildFromInput($changesArray);
					try {
						ProductUpdateService::updateProduct($item->post_id, $changes);
						self::markItemAsSuccess($item->id);
					} catch (ProductUpdateException $e) {
						self::markItemAsError($item->id, $e->error);
						\ShopManagerPro\Shared\Utils\Logger::error('Product update error', ['post_id' => $item->post_id, 'error' => $e->getMessage()]);
					} catch (\Throwable $e) {
						self::markItemAsError($item->id, new JobProductsItemErrorAlternative2(message: $e->getMessage()));
						\ShopManagerPro\Shared\Utils\Logger::exception("Error updating product {$item->post_id}", $e);
					} if ((time() - $startTime) >= $maxExecutionTime) {
						\ShopManagerPro\Shared\Utils\Logger::info('Reached max execution time, shifting remaining work to next run', ['job_id' => $jobId]);
						self::scheduleQueueProcessor();

						return;
					}
				} self::updateJobProgress($jobId);
			}
		} catch (\Throwable $e) {
			\ShopManagerPro\Shared\Utils\Logger::exception("Critical error processing job $jobId", $e);
			JobRepository::updateJobStatus($jobId, JobStatus::finishedwitherrors);
		}
	}

	private static function getBatch(int $jobId) {
		global $wpdb;
		$sql = $wpdb->prepare('SELECT * FROM '.DatabaseManagerService::$jobsProductUpdatesTableName.' 
			WHERE job_id = %d AND status = %s
			ORDER BY id
			LIMIT %d', $jobId, JobProductsItemStatus::pending->value, self::BATCH_SIZE);

		return $wpdb->get_results($sql);
	}

	private static function markItemAsSuccess(int $itemId) {
		global $wpdb;
		$wpdb->update(DatabaseManagerService::$jobsProductUpdatesTableName, ['status' => JobProductsItemStatus::success->value, 'finished_at' => current_time('mysql', true)], ['id' => $itemId], ['%s', '%s'], ['%d']);
	}

	private static function markItemAsError(int $itemId, JobProductsItemErrorAlternative1|JobProductsItemErrorAlternative2 $error) {
		global $wpdb;
		$wpdb->update(DatabaseManagerService::$jobsProductUpdatesTableName, ['status' => JobProductsItemStatus::error->value, 'finished_at' => current_time('mysql', true), 'error' => JSON::encode($error->toJson())], ['id' => $itemId], ['%s', '%s', '%s'], ['%d']);
	}

	private static function updateJobProgress(int $jobId) {
		global $wpdb;
		$sql = $wpdb->prepare('SELECT 
				COUNT(*) as total,
				SUM(CASE WHEN status != %s THEN 1 ELSE 0 END) as processed
			FROM '.DatabaseManagerService::$jobsProductUpdatesTableName.' 
			WHERE job_id = %d', JobProductsItemStatus::pending->value, $jobId);
		$progress = $wpdb->get_row($sql);
		$wpdb->query($wpdb->prepare('UPDATE '.DatabaseManagerService::$jobsTableName.' 
			SET totalItems = %d, processedItems = %d 
			WHERE id = %d', $progress->total, $progress->processed, $jobId));
	}

	private static function finalizeJob(int $jobId) {
		global $wpdb;
		$errorCount = $wpdb->get_var($wpdb->prepare('SELECT COUNT(*) FROM '.DatabaseManagerService::$jobsProductUpdatesTableName.' 
			WHERE job_id = %d AND status = %s', $jobId, JobProductsItemStatus::error->value));
		$finalStatus = $errorCount > 0 ? JobStatus::finishedwitherrors : JobStatus::finishedsuccessfully;
		JobRepository::updateJobStatus($jobId, $finalStatus);
		self::scheduleQueueProcessor();
	}

	public static function scheduleQueueProcessor() {
		as_schedule_single_action(time() + 1, 'shopmanagerpro_process_queue', priority: 5);
	}
}
