<?php
App::uses('HandlerRegistry', 'Cronner.Handler');

class Runner {

	var $model = null;

	/**
	 * Run cronner
	 */
	public function run() {

		// Check running ones
		$runing = $this->checkRunningOnes();

		// Trigger jobs
		$this->triggerJobs($runing);
	}

	/**
	 * Trigger jobs
	 */
	public function triggerJobs($running) {
		$jobs = $this->getAllJobs();

		foreach ($jobs as $job) {

			// If it's already running - do not trigger it
			if (in_array($job['CronJob']['id'], $running)) {
				continue;
			}

			if ($this->isDue($job)) {
				$this->trigger($job);
			}
		}
	}

	/**
	 * Check running ones - if something broke
	 */
	public function checkRunningOnes() {
		$runningJobs = [];

		$inProress = $this->getRunning();
		foreach ($inProress as $log) {
			if ($this->isFailed($log)) {
				$this->failedInBackground($log);
			}
			$runningJobs[] = $log['CronJobLog']['cron_job_id'];
		}

		return $runningJobs;
	}

	/**
	 * Get all jobs from database
	 *
	 * @return mixed
	 */
	private function getAllJobs() {
		$jobs = $this->getModel()->find('all', [
			'conditions' => [
				$this->getModel()->alias . '.is_active' => true
			]
		]);
		return $jobs;
	}

	/**
	 * Get job by id
	 *
	 * @param $jobId
	 *
	 * @return mixed
	 */
	private function getJob($jobId) {
		return $this->getModel()->find('first', $jobId);
	}

	/**
	 * Get one time job
	 *
	 * @param $jobId
	 *
	 * @return mixed
	 */
	private function getBackgroundJob($jobId) {
		return ClassRegistry::init('Cronner.BackgroundJob')->find('first', $jobId);
	}

	/**
	 * Check if job is due
	 *
	 * @param $job
	 *
	 * @return bool
	 */
	private function isDue($job) {
		$expression = $this->buildExpression($job);
		$cron = Cron\CronExpression::factory($expression);
		return $cron->isDue();
	}

	/**
	 * Trigger job - triggering via cli, in order to spawn to new process
	 *
	 * @param $job
	 */
	public function trigger($job) {

		// Log both standard and error outputs to files
		$hash = $job['CronJob']['id'] . '-' . md5(getRandomString()) . md5(getRandomString());

		$logFolder = PLUGGIN_CRONNER_LOG . $hash;
		mkdir($logFolder, 0777);

		$stdOut = $logFolder . DS . 'stdout.log';
		$stdErr = $logFolder . DS . 'stderr.log';

		// Execute job
		shell_exec('php -f ' . APP . 'Console' . DS . 'cake.php Cronner.trigger ' . $job[$this->getModel()->alias]['id'] . ' ' . $hash . ' >> "' . $stdOut . '" 2>>"' . $stdErr . '" &');

	}

	/**
	 * Trigger job - triggering via cli, in order to spawn to new process
	 *
	 * @param $handler
	 * @param $parameters
	 *
	 * @return array
	 */
	public static function triggerBackgroundJob($handler, $parameters) {

		$job = [
			'handler' => $handler,
			'params'  => serialize($parameters),
		];

		ClassRegistry::init('Cronner.BackgroundJob')->create();
		$job = ClassRegistry::init('Cronner.BackgroundJob')->save($job);

		// Log both standard and error outputs to files
		$hash = $job['BackgroundJob']['id'] . '-' . md5(getRandomString()) . md5(getRandomString());

		$logFolder = PLUGGIN_CRONNER_BACKGROUND_JOB_LOGS . $hash;
		mkdir($logFolder, 0777);

		$stdOut = $logFolder . DS . 'stdout.log';
		$stdErr = $logFolder . DS . 'stderr.log';

		// Execute job
		shell_exec('php -f ' . APP . 'Console' . DS . 'cake.php Cronner.trigger_one_time ' . $job['BackgroundJob']['id'] . ' ' . $hash . ' >> "' . $stdOut . '" 2>>"' . $stdErr . '" &');

		return $job;
	}

	/**
	 * Actual job run
	 *
	 * @param $jobId
	 * @param $hash
	 *
	 * @throws CronHandlerNotFoundException
	 */
	public function executeJob($jobId, $hash) {
		// Get job
		$job = $this->getJob($jobId);

		// Get handler
		$handler = $job['CronJob']['handler'];
		$handler = HandlerRegistry::getHandler($handler);

		// Run handler
		$runStatus = $handler->run($job, $hash);

		// Write log
		$this->logRun($handler, $runStatus, $hash);
	}

	/**
	 * Actual job run
	 *
	 * @param $jobId
	 * @param $hash
	 *
	 * @throws CronHandlerNotFoundException
	 */
	public function executeBackgroundJob($jobId, $hash) {
		if (empty($jobId)) {
			return;
		}

		// Get job
		$job = $this->getBackgroundJob($jobId);

		// Get handler
		$handler = $job['BackgroundJob']['handler'];
		$handler = HandlerRegistry::getBackgroundJobHandler($handler);

		// Run handler
		$runStatus = $handler->run($job, $hash, unserialize($job['BackgroundJob']['params']));

		// Set as finished
		$job['BackgroundJob']['status'] = 1;
		ClassRegistry::init('Cronner.BackgroundJob')->save($job);

		// Write log
		$this->logBackgroundJobRun($handler, $runStatus, $hash);
	}

	/**
	 * Get cron job model
	 *
	 * @return null
	 */
	private function getModel() {
		if (!empty($this->model)) {
			return $this->model;
		}

		$this->model = ClassRegistry::init('Cronner.CronJob');

		return $this->getModel();
	}

	/**
	 * Build expression for job
	 *
	 * @param $job
	 *
	 * @return string
	 */
	private function buildExpression($job) {
		return $job['CronJob']['expression'];
	}

	/**
	 * Log run
	 *
	 * @param $handler
	 * @param $runStatus
	 * @param $hash
	 */
	private function logRun($handler, $runStatus, $hash) {
		$success = true;
		if ($runStatus === false) {
			$success = false;
		}

		$log = $handler->getLog();
		$log['status'] = $success;
		$log['finished'] = date('Y-m-d H:i:s');
		$log['hash'] = $hash;

		$saved = ClassRegistry::init('Cronner.CronJobLog')->save($log);
	}

	/**
	 * Log background job run
	 *
	 * @param $handler
	 * @param $runStatus
	 * @param $hash
	 */
	public function logBackgroundJobRun($handler, $runStatus, $hash) {
		$success = true;
		if ($runStatus === false) {
			$success = false;
		}

		$log = $handler->getLog();
		$log['status'] = $success;
		$log['finished'] = date('Y-m-d H:i:s');
		$log['hash'] = $hash;

		$logFolder = PLUGGIN_CRONNER_BACKGROUND_JOB_LOGS . $hash;
		$files = glob($logFolder . DS . '*.log');

		foreach ($files as $file) {
			$name = pathinfo($file, PATHINFO_FILENAME);
			$log[$name] = file_get_contents($file);
		}

		cleanDir($logFolder, true);

		return ClassRegistry::init('Cronner.BackgroundJobLog')->save($log);
	}

	private function isFailed($job) {
		if (!$job['CronJobLog']['finished'] && !posix_getpgid($job['CronJobLog']['pid'])) {
			return true;
		}

		return false;
	}

	/**
	 * If job failed in background
	 *
	 * @param $job
	 */
	private function failedInBackground($job) {
		$job['CronJobLog']['finished'] = date('Y-m-d H:i:s');
		$job['CronJobLog']['status'] = false;
		ClassRegistry::init('Cronner.CronJobLog')->save($job);
	}

	/**
	 * Get running jobs
	 *
	 * @return mixed
	 */
	private function getRunning() {
		return ClassRegistry::init('Cronner.CronJobLog')->getRunning();
	}
}
