<?php

namespace JetBackup\Cron\Task;

use Exception;
use JetBackup\BackupJob\BackupJob;
use JetBackup\Cron\Cron;
use JetBackup\Entities\Util;
use JetBackup\Exception\DBException;
use JetBackup\Exception\DirIteratorException;
use JetBackup\Exception\ExecutionTimeException;
use JetBackup\Exception\IOException;
use JetBackup\Exception\JBException;
use JetBackup\Exception\TaskException;
use JetBackup\Factory;
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\Config\System;
use SleekDB\Exceptions\InvalidArgumentException;

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

abstract class Task {

	private string $_log_file_name;
	private int $_execution_memory;
	private int $_execution_start;
	private int $_execution_limit;
	private bool $_execution_die=true;
	private ?QueueItem $_queue_item=null;
	private ?LogController $_cronLogController=null;
	private LogController $_logController;
	
	public function __construct(string $log_file_name) {
		$this->_log_file_name = $log_file_name;
		$this->_execution_start = time();
		$this->_execution_memory = memory_get_usage();

		$userSetting = Factory::getSettingsPerformance()->getExecutionTime();
		if ($userSetting > 0) {
			// User configured a custom execution time, use it
			$this->_execution_limit = $userSetting;
		} else {
			// Auto-calculate from PHP's max_execution_time
			$serverTime = System::getServerExecutionTime();
			// If server has no limit (0 = CLI or unlimited), set to 0 (no limit)
			// Note: getServerExecutionTime() returns 60 as fallback, check raw ini for unlimited
			$rawTime = (int) ini_get('max_execution_time');
			// Use server time - 10 as buffer, minimum 10 seconds for work
			$this->_execution_limit = $rawTime > 0 ? max($serverTime - 10, 10) : 0;
		}
	}

	/**
	 * Errors logged through this function are also counted and saved in the DB (->addError())
	 * Using the error count we know if the job is completed with error
	 *
	 * Only use this for errors which will not break the loop (errors that just report and continue)
	 *
	 * @param $error
	 *
	 * @return void
	 */
	public function logError($error) {
		$this->getQueueItem()->addError();
		$this->getLogController()->logError($error);
	}

	public function setExecutionTimeLimit(int $limit):void {
		if(Factory::getSettingsPerformance()->getExecutionTime()) return;
		$serverTime = System::getServerExecutionTime();
		// Ensure we never exceed PHP's actual max_execution_time
		// Use server time - 10 as buffer, with a minimum of 10 seconds for work
		$calculatedLimit = max($serverTime - 10, 10);
		// If server has unlimited time (0), use the provided limit or default to 60
		if($serverTime <= 0) $calculatedLimit = $limit ?: 60;
		$this->_execution_limit = $calculatedLimit;
	}

	public function setExecutionTimeDie(bool $die):void {
		$this->_execution_die = $die;
	}

	public function getExecutionTimeLimit():int {
		return $this->_execution_limit;
	}

	public function getExecutionTimeElapsed():int {
		return time() - $this->_execution_start;
	}

	public function getExecutionMemoryUsage():int {
		return memory_get_usage() - $this->_execution_memory;
	}

	public function isExecutionTimeLimitReached():bool {
		if($this->getExecutionTimeLimit() <= 0) return false;
		return $this->getExecutionTimeElapsed() >= $this->getExecutionTimeLimit();
	}

	/**
	 * @param callable|null $callback
	 *
	 * @return void
	 * @throws ExecutionTimeException
	 * @throws \SleekDB\Exceptions\IOException
	 * @throws InvalidArgumentException
	 * @throws DBException
	 * @throws JBException
	 */
	public function checkExecutionTime(?callable $callback=null):void {

		if(file_exists($this->getQueueItem()->getAbortFileLocation())) {

			// Prevent infinite loop with an aborted backup job (case #862)
			if ($this->getQueueItem()->getType() == Queue::QUEUE_TYPE_BACKUP) {
				$instance = $this->getQueueItem()->getItemData();
				$backup_job = new BackupJob($instance->getJobId());
				$backup_job->setLastRun(time());
				$backup_job->calculateNextRun();
				$backup_job->save();
			}

			$this->getQueueItem()->getProgress()->setSubMessage("[Queue Item aborted, exiting]");
			$this->getLogController()->logMessage("Queue Item aborted, exiting");
			$this->getQueueItem()->save();
			$this->getLogController()->logDebug("!! Nice Exit !!");
			unlink($this->getQueueItem()->getAbortFileLocation());
			die(0);
		}

		if(!$this->isExecutionTimeLimitReached() && $this->getQueueItem()->getStatus() < Queue::STATUS_DONE) return;
		if($callback) $callback($this->getExecutionTimeElapsed(), $this->getExecutionTimeLimit(), $this->getExecutionMemoryUsage());

		$this->getLogController()->logMessage("Current memory usage: " . Util::bytesToHumanReadable(max($this->getExecutionMemoryUsage(), 0)));

		if($this->getQueueItem()->getStatus() >= Queue::STATUS_DONE) {
			$this->getQueueItem()->getProgress()->setSubMessage("[Queue Item finished, exiting]");
			$this->getLogController()->logMessage("Queue Item finished, exiting");
		} else {
			$this->getQueueItem()->getProgress()->setSubMessage("[Waiting for next cron execution]");
			$this->getLogController()->logMessage("Execution TTL [{$this->getExecutionTimeLimit()} seconds] Time reached: {$this->getExecutionTimeElapsed()} seconds, exiting to resume later");
		}

		$this->getQueueItem()->save();

		$this->getLogController()->logDebug("!! Nice Exit !!");

		if($this->_execution_die) die(0);
		throw new ExecutionTimeException("!! Nice Exit !!");
	}

	public function setQueueItem(QueueItem $item):void { $this->_queue_item = $item; }
	public function getQueueItem():?QueueItem { return $this->_queue_item; }

	public function setLogController(LogController $logController):void { $this->_logController = $logController; }
	public function getLogController():LogController { return $this->_logController; }

	public function setCronLogController(LogController $logController):void { $this->_cronLogController = $logController; }
	public function getCronLogController():LogController { return $this->_cronLogController ?? new LogController(); }

	/**
	 * @throws Exception
	 */
	public function getLogFile(): string {
		return Factory::getLocations()->getLogsDir() . JetBackup::SEP . $this->_log_file_name . '_' . $this->getQueueItem()->getUniqueId() . '.log';
	}
	
	public function execute():void {
		if(!$this->getQueueItem()) throw new TaskException("No queue item provided");

		$this->getQueueItem()->setLogFile($this->getLogFile());
		$this->getQueueItem()->save();

		$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->setLogController(new LogController());
		$this->getLogController()->addLogger(new FileLogger($this->getLogFile(), $level));

		// Log execution time settings at job start
		$this->getLogController()->logMessage("Execution time limit: {$this->_execution_limit} seconds (PHP max_execution_time: " . ini_get('max_execution_time') . ")");
		if(Cron::inDebug()) $this->getLogController()->addLogger(new StdLogger($level));
	}
	
	public function func(callable $callback, array $args=[], ?string $name=null) {
		$resumable = $this->getQueueItem()->getResumableTask();
		$resumable->setLogController($this->getLogController());
		$resumable->setTask($this);
		return $resumable->func($callback, $args, $name);
	}

	public function foreach(array $data, callable $func, ?string $name=null) {
		$resumable = $this->getQueueItem()->getResumableTask();
		$resumable->setLogController($this->getLogController());
		$resumable->setTask($this);
		$resumable->foreach($data, $func, $name);
	}

	public function foreachCallable(callable $data, array $args, callable $func, ?string $name=null) {
		$resumable = $this->getQueueItem()->getResumableTask();
		$resumable->setLogController($this->getLogController());
		$resumable->setTask($this);
		$resumable->foreachCallable($data, $args, $func, $name);
	}

	/**
	 * @throws IOException
	 * @throws DirIteratorException
	 */
	public function scan(string $source, callable $func, array $excludes=[], ?string $name=null):void {
		$resumable = $this->getQueueItem()->getResumableTask();
		$resumable->setLogController($this->getLogController());
		$resumable->setTask($this);
		$resumable->scan($source, $func, $excludes, $name);
	}
	
	public function fileRead(string $file_path, callable $func, ?string $name=null):void {
		$resumable = $this->getQueueItem()->getResumableTask();
		$resumable->setLogController($this->getLogController());
		$resumable->setTask($this);
		$resumable->fileRead($file_path, $func, $name);
	}

	public function fileMerge(string $source, string $target, ?string $name=null):void {
		$resumable = $this->getQueueItem()->getResumableTask();
		$resumable->setLogController($this->getLogController());
		$resumable->setTask($this);
		$resumable->fileMerge($source, $target, $name);
	}
}