<?php

namespace JetBackup\Cron;

if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');

use JetBackup\BackupJob\BackupJob;
use JetBackup\Cache\CacheHandler;
use JetBackup\Cron\Task\Backup;
use JetBackup\Cron\Task\DownloadBackupLog;
use JetBackup\Cron\Task\RetentionCleanup;
use JetBackup\Cron\Task\Download;
use JetBackup\Cron\Task\Export;
use JetBackup\Cron\Task\Extract;
use JetBackup\Cron\Task\Reindex;
use JetBackup\Cron\Task\PreRestore;
use JetBackup\Cron\Task\System;
use JetBackup\Exception\CronException;
use JetBackup\Exception\DBException;
use JetBackup\Exception\IOException;
use JetBackup\Exception\JBException;
use JetBackup\Exception\LogException;
use JetBackup\Exception\QueueException;
use JetBackup\Exception\ReindexException;
use JetBackup\Factory;
use JetBackup\IO\Lock;
use JetBackup\JetBackup;
use JetBackup\Log\FileLogger;
use JetBackup\Log\LogController;
use JetBackup\Log\Logger;
use JetBackup\Log\StdLogger;
use JetBackup\Queue\Queue;
use JetBackup\Queue\QueueItem;
use JetBackup\Wordpress\Helper;
use SleekDB\Exceptions\InvalidArgumentException;
use WP_REST_Response;

class Cron {

	const LOCK_FILE = 'cron.lock';
	const LAST_FILE = 'cron.last';

	private const CRON_LOG_FILE = 'cron.log';

	private LogController $_logController;
	private string $_data_dir;

	/** @var QueueItem|null Current queue item being processed (for fatal error handling) */
	private static ?QueueItem $_currentQueueItem = null;


	/**
	 * @throws CronException
	 * @throws LogException
	 */
	private function __construct() {
		if(!$this->canRun()) throw new CronException('Cron system disabled, you can only execute via wp-cli');

		$this->_data_dir = Factory::getLocations()->getDataDir();
		$this->_setLastRun();

		if (!Lock::LockFile($this->_data_dir . JetBackup::SEP . self::LOCK_FILE)) throw new CronException('Cron is already running', 501);

		$logFile = Factory::getLocations()->getLogsDir() . JetBackup::SEP . self::CRON_LOG_FILE;
		$level = Logger::LOG_LEVEL_ERROR | Logger::LOG_LEVEL_WARNING | Logger::LOG_LEVEL_NOTICE | Logger::LOG_LEVEL_MESSAGE;
		if(Factory::getSettingsLogging()->isDebugEnabled()) $level |= Logger::LOG_LEVEL_DEBUG;

		$this->_logController = new LogController();
		$this->_logController->addLogger(new FileLogger($logFile, $level));
		if(Cron::inDebug() || Helper::isWPCli()) $this->_logController->addLogger(new StdLogger($level));

	}

	/**
	 * @throws \SleekDB\Exceptions\IOException
	 * @throws IOException
	 * @throws InvalidArgumentException
	 * @throws QueueException|DBException|JBException
	 */
	public static function main() {
		// Register shutdown handler to catch fatal errors (like max_execution_time)
		register_shutdown_function([self::class, 'handleFatalError']);

		CacheHandler::pre();
		$cron = new Cron();
		$cron->execute();
		CacheHandler::post();
	}

	/**
	 * Shutdown handler to catch fatal errors that cannot be caught with try/catch.
	 * This ensures queue items are not left in a corrupted state when PHP dies.
	 */
	public static function handleFatalError(): void {
		$error = error_get_last();

		// Only handle fatal errors
		if ($error === null || !in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE])) {
			return;
		}

		$message = sprintf(
			"[FATAL ERROR] %s in %s on line %d",
			$error['message'],
			$error['file'],
			$error['line']
		);

		try {
			$logFile = Factory::getLocations()->getLogsDir() . JetBackup::SEP . self::CRON_LOG_FILE;
			FileLogger::emergency($logFile, $message);

			// If we have a current queue item, log that it will be retried
			if (self::$_currentQueueItem !== null) {
				$itemId = self::$_currentQueueItem->getId();
				if ($itemId) {
					FileLogger::emergency($logFile, "Queue item {$itemId} will be retried on next cron run");
				}
			}
		} catch (\Throwable $e) {
			// Logging failed, ignore
		}
	}



	/**
	 * @return void
	 * @throws IOException
	 * @throws InvalidArgumentException
	 * @throws QueueException
	 * @throws \SleekDB\Exceptions\IOException|DBException|JBException
	 */
	public function execute() {

		try {
			$this->_executeNextQueue();
		} catch(\TypeError|\Error $e) {
			$message = sprintf("Cron exited due to an fatal error. Error: %s in %s on line %s", $e->getMessage(), $e->getFile(), $e->getLine());
			$this->_logController->logError($message);
			$this->_logController->logError($e->getTraceAsString());
			die($message . PHP_EOL . $e->getTraceAsString());
		}

		// Add all scheduled backup jobs to queue
		BackupJob::addToQueueScheduled();
		
		// Add system tasks to queue 
		System::addToQueue();
	}

	/**
	 * @return void
	 * @throws InvalidArgumentException
	 * @throws DBException
	 * @throws \SleekDB\Exceptions\IOException|JBException
	 */
	private function _executeNextQueue():void {
		if(!($item = Queue::next())) return;

		// Track current item for fatal error handling
		self::$_currentQueueItem = $item;

		$this->getLogController()->logMessage("Got next queue item (ID: {$item->getId()}, Type: {$item->getType()})");

		$task = null;
		try {
			switch ($item->getType()) {
				case Queue::QUEUE_TYPE_BACKUP: $task = new Backup(); break;
				case Queue::QUEUE_TYPE_DOWNLOAD: $task = new Download(); break;
                case Queue::QUEUE_TYPE_DOWNLOAD_BACKUP_LOG: $task = new DownloadBackupLog(); break;
                case Queue::QUEUE_TYPE_EXTRACT: $task = new Extract(); break;
				case Queue::QUEUE_TYPE_REINDEX: $task = new Reindex(); break;
				case Queue::QUEUE_TYPE_RETENTION_CLEANUP: $task = new RetentionCleanup(); break;
				case Queue::QUEUE_TYPE_SYSTEM: $task = new System(); break;
				case Queue::QUEUE_TYPE_EXPORT: $task = new Export(); break;
				case Queue::QUEUE_TYPE_RESTORE: $task = new PreRestore(); break;
				default: throw new CronException('Could not find queue type');
			}

			$task->setQueueItem($item);
			$task->setCronLogController($this->getLogController());
			if(Helper::isCLI()) $task->setExecutionTimeLimit(0);
			$task->setExecutionTimeDie(true);
			$task->execute();

			// Clear current item on successful completion
			self::$_currentQueueItem = null;
		} catch (\Exception $e) {
			$message = "Cron exited due to an uncaught " . get_class($e) . ". Error: " . $e->getMessage();
			$this->_logController->logError($message);

			// Also log to task's log controller (visible in GUI job log)
			if ($task !== null) {
				try {
					$task->getLogController()->logError($message);
				} catch (\Throwable $logError) {
					// Ignore logging failures
				}
			}

			$item->updateStatus(Queue::STATUS_NEVER_FINISHED);
			$progress = $item->getProgress();
			$progress->setMessage($message);

			$backup_job = null;

			if ($item->getType() == Queue::QUEUE_TYPE_BACKUP) {
				// Prevent infinite loop with a failed backup destination
				// It will fail before getting to the backup job, so the job meta will never update
				$instance = $item->getItemData();
				$backup_job = new BackupJob($instance->getJobId());
				$backup_job->setLastRun(time());
				$backup_job->calculateNextRun();
			}

			// Save operation is return void so have to try-catch them

			try {
				// Save queue item
				$item->save();
			} catch (\Exception $saveException) {
				$this->_logController->logError("Failed to save Queue item: " . $saveException->getMessage());
			}

			if ($backup_job !== null) {
				try {
					// Save the backup job
					$backup_job->save();
				} catch (\Exception $saveException) {
					$this->_logController->logError("Failed to save Backup Job: " . $saveException->getMessage());
				}
			}

			// Clear current item after exception handling
			self::$_currentQueueItem = null;
		}

	}
	
	public function getLogController(): LogController {
		return $this->_logController;
	}
	
	private function _setLastRun() {
		if (!Helper::isCLI()) return;
		touch($this->_data_dir . JetBackup::SEP . self::LAST_FILE);
	}

	private function canRun(): bool {
		$_cron_disabled = !Factory::getSettingsAutomation()->isCronsEnabled();
		if ($_cron_disabled && !Helper::isCLI()) return false; // cannot run
		// Disable the script timeout for CLI mode
		if (Helper::isCLI()) set_time_limit(0);
		return true;
	}

	public static function inDebug(): bool { return self::_argExists('debug'); }


	public function __destruct() {
		Lock::UnlockFile($this->_data_dir . JetBackup::SEP . self::LOCK_FILE);
	}

	private static function _argExists($arg): bool {
		global $argv;
		return isset($argv) && in_array('--'.$arg, $argv);
	}
}