<?php

namespace ShopManagerPro\Jobs;

use ShopManagerPro\Jobs\DTO\Generated\JobStatus;
use ShopManagerPro\Jobs\DTO\Generated\JobType;
use ShopManagerPro\Products\DTO\Generated\ProductBulkUpdate;
use ShopManagerPro\Products\ProductRepository;
use ShopManagerPro\Products\ProductUpdateService;
use ShopManagerPro\Shared\DatabaseManagerService;
use ShopManagerPro\Shared\Utils\JSON;

class JobRepository {
	public static function createProductUpdateJob(ProductBulkUpdate $update) {
		global $wpdb;
		if (!$update->getProductIds()) {
			throw new \InvalidArgumentException('Items array cannot be empty');
		} $allProducts = ProductRepository::getProducts();
		$allProductsByID = [];
		foreach ($allProducts as $product) {
			$allProductsByID[$product->getBasic()->getId()] = $product;
		} $products = [];
		foreach ($update->getProductIds() as $productID) {
			if (!isset($allProductsByID[$productID])) {
				throw new \InvalidArgumentException("Product ID $productID does not exist");
			} $products[] = $allProductsByID[$productID];
		} $wpdb->query('START TRANSACTION');
		try {
			$currentUserId = get_current_user_id();
			$sql = '
				INSERT INTO '.DatabaseManagerService::$jobsTableName.'
				(type , actions , status , totalItems , user_id , created_at)
				VALUES (%s, %s, %s, %d, IF(%d = 0, NULL, %d), UTC_TIMESTAMP())';
			$sql = $wpdb->prepare($sql, JobType::productupdate->value, JSON::encode($update->getActions()->toJson()), JobStatus::pending->value, count($products), $currentUserId, $currentUserId);
			$job_result = $wpdb->query($sql);
			if ($job_result === false) {
				$error = $wpdb->last_error ? " Cause: {$wpdb->last_error}" : '';
				throw new \RuntimeException('Failed to insert job record.'.$error);
			} $jobID = $wpdb->insert_id;
			foreach ($products as $product) {
				$changes = ProductUpdateService::getProductChanges($product, $update->getActions());
				if (!$changes) {
					throw new \InvalidArgumentException("No changes found for product ID {$product->getBasic()->getId()} with the provided actions.");
				} $sql = 'INSERT INTO '.DatabaseManagerService::$jobsProductUpdatesTableName.'
					(job_id , post_id , status , changes , created_at )
					VALUES (%d , %d , %s , %s , UTC_TIMESTAMP())';
				$sql = $wpdb->prepare($sql, $jobID, $product->getBasic()->getId(), JobStatus::pending->value, JSON::encode($changes->toJson()));
				$item_result = $wpdb->query($sql);
				if ($item_result === false) {
					$wpdb->print_error();
					$error = $wpdb->last_error ? " Cause: {$wpdb->last_error}" : '';
					throw new \RuntimeException("Failed to insert job item for product ID {$product->getBasic()->getId()}. $error");
				}
			} $wpdb->query('COMMIT');
			JobProcessorService::scheduleQueueProcessor();

			return $jobID;
		} catch (\Throwable $e) {
			$wpdb->query('ROLLBACK');
			throw $e;
		}
	}

	public static function getJobs() {
		global $wpdb;
		$sql = 'SELECT
				j.* , u.display_name as user_display_name
			FROM
				'.DatabaseManagerService::$jobsTableName.' j
			LEFT JOIN
				'.$wpdb->users.' u
			ON
				j.user_id = u.ID
			ORDER BY
				j.created_at DESC';
		$jobRows = $wpdb->get_results($sql, ARRAY_A);
		$jobs = [];
		foreach ($jobRows as $row) {
			try {
				$job = self::createJobFromRow($row);
			} catch (\Throwable $e) {
				\ShopManagerPro\Shared\Utils\Logger::exception('Failed to create job from row', $e, ['row' => $row]);
				continue;
			} if ($job->getStatus() === JobStatus::pending || $job->getStatus() === JobStatus::running) {
				if (!as_has_scheduled_action('shopmanagerpro_process_queue')) {
					\ShopManagerPro\Shared\Utils\Logger::info('Found idling job, scheduling queue processor', ['job_id' => $job->getId()]);
					JobProcessorService::scheduleQueueProcessor();
				}
			} $jobs[$job->getId()] = $job;
		} $jobIdsWithProducts = [];
		foreach ($jobs as $job) {
			if (!JobUtil::isFinished($job)) {
				$jobIdsWithProducts[] = $job->getId();
			}
		} if (!empty($jobIdsWithProducts)) {
			self::enrichJobsWithProducts($jobs, $jobIdsWithProducts);
		}

return $jobs;
	}

	public static function getJob(int $jobId, ?bool $expand = false) {
		global $wpdb;
		$sql = $wpdb->prepare('SELECT j.*, u.display_name as user_display_name FROM '.DatabaseManagerService::$jobsTableName.' j LEFT JOIN '.$wpdb->users.' u ON j.user_id = u.ID WHERE j.id = %d', $jobId);
		$row = $wpdb->get_row($sql, ARRAY_A);
		if (!$row) {
			return null;
		} $productRows = null;
		if ($expand) {
			$productSql = $wpdb->prepare('SELECT * FROM '.DatabaseManagerService::$jobsProductUpdatesTableName.' WHERE job_id = %d', $jobId);
			$productRows = $wpdb->get_results($productSql, ARRAY_A);
		}

return self::createJobFromRow($row, $productRows);
	}

	private static function enrichJobsWithProducts(array &$jobs, array $jobIds) {
		global $wpdb;
		$sql = $wpdb->prepare('
			SELECT
				*
			FROM
				'.DatabaseManagerService::$jobsProductUpdatesTableName.'
			WHERE
				job_id IN ( '.\ShopManagerPro\Shared\Utils\SQL::stringPlaceholders(count($jobIds)).' )', ...$jobIds);
		foreach ($wpdb->get_results($sql, ARRAY_A) as $row) {
			$jobId = (int) $row['job_id'];
			if (!isset($jobs[$jobId])) {
				continue;
			} try {
				$productDetails = self::createProductDetailsFromRow($row);
			} catch (\Throwable $e) {
				\ShopManagerPro\Shared\Utils\Logger::exception('Failed to create product details from row', $e, ['row' => $row]);
				continue;
			} $job = $jobs[$jobId];
			$products = $job->getProducts() ?: [];
			$products[$productDetails->getId()] = $productDetails;
			$jobs[$jobId] = $job->withProducts($products);
		}
	}

	private static function createJobFromRow(array $row, ?array $productRows = null) {
		$finishTime = $row['finished_at'] ? \ShopManagerPro\Shared\Utils\Time::mySqlDateTimeToUTCISO8601($row['finished_at']) : null;
		if ($row['status'] === 'running' && $row['processedItems'] > 0 && $row['totalItems'] > $row['processedItems']) {
			$startTime = strtotime($row['created_at'].' UTC');
			$currentTime = time();
			$timeElapsed = $currentTime - $startTime;
			$timePerItem = $timeElapsed / $row['processedItems'];
			$remainingItems = $row['totalItems'] - $row['processedItems'];
			$remainingTime = $timePerItem * $remainingItems;
			$finishTime = (new \DateTimeImmutable())->add(new \DateInterval('PT'.intval($remainingTime).'S'))->format(\DateTime::ATOM);
		} $products = null;
		if ($productRows !== null) {
			$products = [];
			foreach ($productRows as $productRow) {
				try {
					$product = self::createProductDetailsFromRow($productRow);
				} catch (\Throwable $e) {
					\ShopManagerPro\Shared\Utils\Logger::exception('Failed to create product details from row', $e, ['row' => $productRow]);
					continue;
				} $products[$product->getId()] = $product;
			}
		} $user = null;
		if (isset($row['user_id']) && $row['user_id']) {
			$user = new DTO\Generated\JobUserAlternative1(id: (int) $row['user_id'], name: $row['user_display_name'] ?: (string) $row['user_id']);
		} $actions = DTO\Generated\JobActions::buildFromInput(json_decode($row['actions'], flags: JSON_THROW_ON_ERROR));
		$job = new DTO\Generated\Job(id: $row['id'], type: JobType::from($row['type']), actions: $actions, status: JobStatus::from($row['status']), totalItems: $row['totalItems'], processedItems: $row['processedItems'], startTime: \ShopManagerPro\Shared\Utils\Time::mySqlDateTimeToPHPDateTime($row['created_at']), finishTime: $finishTime, user: $user);
		if ($products !== null) {
			$job = $job->withProducts($products);
		}

return $job;
	}

	private static function createProductDetailsFromRow(array $row) {
		$productStatus = new DTO\Generated\JobProductsItem(id: (int) $row['post_id'], status: DTO\Generated\JobProductsItemStatus::from($row['status']), changes: DTO\Generated\JobProductsItemChanges::buildFromInput(json_decode($row['changes'], flags: JSON_FORCE_OBJECT | JSON_THROW_ON_ERROR)), creationTime: \ShopManagerPro\Shared\Utils\Time::mySqlDateTimeToPHPDateTime($row['created_at']), finishTime: $row['finished_at'] ? \ShopManagerPro\Shared\Utils\Time::mySqlDateTimeToUTCISO8601($row['finished_at']) : null);
		if ($row['error']) {
			$error = json_decode($row['error']);
			if (DTO\Generated\JobProductsItemErrorAlternative1::validateInput($error, true)) {
				$error = DTO\Generated\JobProductsItemErrorAlternative1::buildFromInput($error);
			} elseif (DTO\Generated\JobProductsItemErrorAlternative2::validateInput($error, true)) {
				$error = DTO\Generated\JobProductsItemErrorAlternative2::buildFromInput($error);
			} else {
				throw new \InvalidArgumentException('Invalid error format in job product item');
			} $productStatus = $productStatus->withError($error);
		}

return $productStatus;
	}

	public static function findNextRunnableJob() {
		global $wpdb;
		$jobId = self::getOneJobByStatus('running');
		if ($jobId) {
			return $jobId;
		} $jobId = self::getOneJobByStatus('pending');
		if ($jobId) {
			return $jobId;
		}

return null;
	}

	public static function updateJobStatus(int $jobId, JobStatus $status) {
		global $wpdb;
		$updateData = ['status' => $status->value];
		if (JobUtil::isFinishedStatus($status)) {
			$updateData['finished_at'] = current_time('mysql', true);
		} $result = $wpdb->update(DatabaseManagerService::$jobsTableName, $updateData, ['id' => $jobId], array_fill(0, count($updateData), '%s'), ['%d']);

		return $result !== false;
	}

	private static function getOneJobByStatus(string $status) {
		global $wpdb;
		$jobId = $wpdb->get_var($wpdb->prepare('SELECT id FROM '.DatabaseManagerService::$jobsTableName.' WHERE status = %s ORDER BY created_at ASC LIMIT 1', $status));

		return $jobId ? (int) $jobId : null;
	}

	public static function stopJob(int $jobId) {
		global $wpdb;
		$jobResult = $wpdb->update(DatabaseManagerService::$jobsTableName, ['status' => 'stopped', 'finished_at' => current_time('mysql', true)], ['id' => $jobId], ['%s', '%s'], ['%d']);
		if ($jobResult === false) {
			throw new \RuntimeException("Failed to stop job with ID: $jobId");
		}
	}
}
