<?php

namespace App\ExportData;

class QueueManager
{

	/**
	 * The Singleton instance.
	 * @var self
	 */
	static private $instance;

	/**
	 * Indicates if the process stops after one error.
	 * @var Boolean
	 */
	protected $stopOnFailed = true;

	/**
	 * The location on disk where the queue is stored.
	 * @var String
	 */
	protected $location_queue;

	/**
	 * The location on disk where the confirmed files are stored.
	 * @var String
	 */
	protected $location_confirmed;

	/**
	 * The location on disk where the failed files are stored.
	 * @var String
	 */
	protected $location_failed;

	/**
	 * The location on disk where the deleted files are stored.
	 * @var String
	 */
	protected $location_deleted;

	/**
	 * The convertor class name - must be an instance of \App\ExportData\ConvertAbstract.
	 * @var String $convertClassName
	 */
	protected $convertClassName;

	/**
	 * The output class name - must be an instance of \App\ExportData\OutputAbstract.
	 * @var String
	 */
	protected $outputClassName;

	/**
	 * The list of messages that are collected during files processing.
	 * @var \App\MessagesCollector
	 */
	protected $messagesCollector;

	/**
	 * Singleton instantiation - also attaches the two classes for conversion and output.
	 * @param String $convertClassName
	 * @param String $outputClassName
	 * @throws \Exception
	 */
	private function __construct()
	{

		// associate all the queue locations
		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();
		$this->location_queue = realpath(MODULE_REL_PATH.$CFG['exportData']['queuePath']).'/';
		$this->location_confirmed = $this->location_queue.'confirmed/';
		$this->location_failed = $this->location_queue.'failed/';
		$this->location_deleted = $this->location_queue.'deleted/';

		$this->messagesCollector = new \App\MessagesCollector();

		// the plugin extension which allows the configuring of the queue
		\Engine\Plugins\Manager::getInstance()->executeEvent('ExportData-QueueManager', ['queueManager'=>$this]);

	}

	/**
	 * Returns the main queue location on disk.
	 * @return String
	 */
	public function getLocation_queue()
	{
		return $this->location_queue;
	}

	/**
	 * Returns the "confirmed" location on disk.
	 * @return String
	 */
	public function getLocation_confirmed()
	{
		return $this->location_confirmed;
	}

	/**
	 * Returns the "failed" location on disk.
	 * @return String
	 */
	public function getLocation_failed()
	{
		return $this->location_failed;
	}

	/**
	 * Returns the "deleted" location on disk.
	 * @return String
	 */
	public function getLocation_deleted()
	{
		return $this->location_deleted;
	}


	/**
	 * Associates the class names for the converter and the output.
	 * @param string $convertClassName
	 * @param string $outputClassName
	 * @throws \Exception
	 */
	public function setClasses($convertClassName, $outputClassName)
	{
		// check the convert process class name
		if (!$convertClassName) {
			throw new \Exception('No class defined for the convert process queue manager.');
		} elseif (!class_exists($convertClassName)) {
			throw new \Exception('The defind class for the convert process does not exist - `'.$convertClassName.'`.');
		} elseif (!is_subclass_of($convertClassName, '\App\ExportData\ConvertAbstract')) {
			throw new \Exception('The defind class for the convert process does not extend \App\ExportData\ConvertAbstract.');
		}

		// check the convert output class name
		if (!$outputClassName) {
			throw new \Exception('No class defined for the output of the queue manager processor.');
		} elseif (!class_exists($outputClassName)) {
			throw new \Exception('The defind class for the output process does not exist - `'.$outputClassName.'`.');
		} elseif (!is_subclass_of($outputClassName, '\App\ExportData\OutputAbstract')) {
			throw new \Exception('The defind class for the output does not extend \App\ExportData\ConvertAbstract.');
		}

		// associate the conversion class name
		$this->convertClassName = $convertClassName;
		$this->outputClassName = $outputClassName;
	}

	/**
	 * Singleton instantiation of the current object.
	 * @param String $convertClassName
	 * @param String $outputClassName
	 * @return self
	 */
	static public function getInstance()
	{
		if (!static::$instance) {
			static::$instance = new self();
		}
		return static::$instance;
	}

	/**
	 * Sets the flag associated to the queue: stopOnFailed.
	 * @param Boolean $stopOnFailed
	 */
	public function setStopOnFailed($stopOnFailed)
	{
		$this->stopOnFailed = $stopOnFailed;
	}

	/**
	 * Processes the queue (takes the first n files from the queue and processes them).
	 * @param number $limit
	 */
	public function process($limit=5)
	{

		$files = $this->getFirstFiles($limit);

		// LOOPS THROUGH THE FIRST FILES IN THE QUEUE /////////////////////////////////////////////
		foreach($files as $file) {

			// process a file and break loop if an error is found
			try {
				$this->processFile($file);
			} catch (\App\MessagesException $e) {
				// stop process if an error is encountered
				if ($this->stopOnFailed) {
					break;
				}
			}

		}

	}

	/**
	 * Gets the first n files from the queue.
	 * @param Int $limit
	 */
	public function getFirstFiles($limit)
	{
		$files = (Array)\Engine\Dir::getContents($this->location_queue, 'file');
		sort($files);

		if (!$limit) {
			return $files;
		}

		return array_slice($files, 0, $limit);
	}

	/**
	 * Processes one file from the queue (it doesn't have to be the first one).
	 * @param String $file
	 */
	public function processFile($file)
	{

		// check if the convert and the output class names are set
		if (!$this->convertClassName or !$this->outputClassName) {
			throw new \Exception('The export data queue was not correctly configured - convertClassName and outputClassName must be both set.');
		}

		// create the output object
		$outputObj = new $this->outputClassName($file);

		// check if te file exists
		if (!\Engine\File::check($this->location_queue.$file)) {
			$outputObj->setOk(false);
			$outputObj->getMessagesCollector()->addError('The file is not a valid XML.');
		} else {

			// parse the file and check if it is a valid XML
			$xml = @simplexml_load_file($this->location_queue.$file);

			// check if this is a valid XML
			if (!$xml) {
				$outputObj->setOk(false);
				$outputObj->getMessagesCollector()->addError('The file is not a valid XML.');
			}

		}

		// THE ACTUAL PROCESSING OF THE FILE //////////////////////////////////////////////////////
		if ($outputObj->getOk()!==false) {

			// EXECUTE THE FILE CONVERSION --------------------------------------------------------
			$convertObj = new $this->convertClassName($xml);
			$convertObj->convert($outputObj);
		}

		// IF FILE SHOULD BE SKIPPED --------------------------------------------------------------
		if ($outputObj->getSkipFile()) {

			$this->moveFile($file, 'queue', 'confirmed');
			$this->addLog('skipped', 'confirmed', $xml, $outputObj);

		}

		// FILE SHOULD BE PROCESSED ---------------------------------------------------------------
		else {

			// SAVE OPERATION (ON SUCCESS) --------------------------------------------------------
			if ($outputObj->getOk()) {
				$outputObj->save();
			}

			// MOVE OPERATION (ON SUCCESS) --------------------------------------------------------
			if ($outputObj->getOk()) {
				// if the moving fails, an \App\MessagesException is thrown - we capture it and log it so the scipt does not stop
				try {
					$this->moveFile($file, 'queue', 'confirmed');
					$this->addLog('processSuccess', 'confirmed', $xml, $outputObj);
				} catch(\App\MessagesException $e) {
					\Log::getInstance()->critical('The exported XML file `%s` could not be confirmed probably due to access rights, but the file was successfully exported to third party applications.');
					$this->addLog('processSuccess;confirmFailed', 'queue', $xml, $outputObj);
				}
			}

			// IF ANY PROBLEM FOUND, STOP THE PROCESS ---------------------------------------------
			if (!$outputObj->getOk()) {
				if (!$this->stopOnFailed) {
					$this->moveFile($file, 'queue', 'failed');
					$this->addLog('processFailed;fileMoved', 'failed', $xml, $outputObj);
				} else {
					$this->addLog('processFailed;fileKept', 'queue', $xml, $outputObj);
				}

				throw new \App\MessagesException($outputObj->getMessagesCollector());
			}


		}

	}


	/**
	 * Moves a file from one of the dirs to another. The method validates that the file is correctly
	 * moved from one place to another.
	 * @param String $fileName - the file name relative to its location; may include YYYYMM dir but it won't include the main dir location.
	 * @param String $sourceDir - can be: 'queue', 'confirmed', 'failed', 'deleted'
	 * @param String $destinationDir - can be: 'queue', 'confirmed', 'failed', 'deleted'
	 */
	protected function moveFile($fileName, $sourceDir, $destinationDir)
	{

		// the allowed values for the source and dest dir names; association with the actual locations
		$dirActualLocations = [
			'queue' =>		$this->location_queue,
			'confirmed' =>	$this->location_confirmed,
			'failed' =>		$this->location_failed,
			'deleted' =>	$this->location_deleted,
		];

		// determine the real locations
		$realSourceDir = $dirActualLocations[$sourceDir];
		$realDestinationDir = $dirActualLocations[$destinationDir];

		// check source and dest dirs
		if (!$realSourceDir) {
			throw new \Exception('Invalid source dir for the moving of an XML file in export data queue manager: you set `'.$sourceDir.'` but the allowed values are: '.implode(', ', array_keys($dirActualLocations)));
		}
		if (!$realDestinationDir) {
			throw new \Exception('Invalid destionation dir for the moving of an XML file in export data queue manager: you set `'.$destinationDir.'` but the allowed values are: '.implode(', ', array_keys($dirActualLocations)));
		}

		// check wrong combinations of source and destination
		if ($sourceDir=='deleted' or ($sourceDir=='failed' and $destinationDir=='confirmed')) {
			throw new \Exception(sprintf('Invalid combinations of source/destionation dirs: source=%s; destination=%s.', $sourceDir, $destinationDir));
		}

		// check if the supplier fileName is not ok
		if (!$fileName) {
			throw new \App\MessagesException(\App\MessagesCollector::create()->addError(sprintf('The XML exportData file name was not provided for moving from `%s` to `%s`.', $sourceDir, $destinationDir)));
		}

		// check if the file exists
		if (!\Engine\File::check($realSourceDir.$fileName)) {
			throw new \App\MessagesException(\App\MessagesCollector::create()->addError('The file '.$realSourceDir.$fileName.' does not exist or is not readable/writable.'));
		}


		if ($destinationDir=='confirmed') {
			$realDestinationDir .= TODAY_YEAR.TODAY_MONTH.'/';
		}

		// make sure that the dirs exist and they are writable
		if (!\Engine\Dir::check($realDestinationDir)) {
			if (!\Engine\Dir::create($realDestinationDir)) {
				throw new \App\MessagesException(\App\MessagesCollector::create()->addError('The dir `'.$realDestinationDir.'` is not writable or could not be created due to file access.'));
			}
		}

		// move the file
		if (!\Engine\File::move($realSourceDir.$fileName, $realDestinationDir)) {
			throw new \App\MessagesException(\App\MessagesCollector::create()->addError(sprintf('The file `%s` could not be moved from `%s` to `%s` probably due to file access.', $fileName, $sourceDir, $destinationDir)));
		}

	}

	/**
	 * This function is used for manually moving files from a folder to another.
	 * You can move only files from `queue` and `failed` to `deleted` and files from `failed` to `queue`
	 *
	 * @param string $file
	 * @param string $sourceDir
	 * @param string $destinationDir
	 * @throws \App\MessagesException
	 */
	public function moveFileManually($file, $sourceDir, $destinationDir)
	{
		// determine the location of file
		$location = '';
		if ($sourceDir == 'queue') {
			$location = $this->location_queue;
		}
		elseif ($sourceDir == 'failed') {
			$location = $this->location_failed;
		}

		// parsing the file
		$xml = @simplexml_load_file($location.$file);

		$entDet = $entityType = $idEntity = $operation = null;

		// get entity type and id from XML, only if the parsed file is of SimpleXMLElement type
		if ($xml instanceof  \SimpleXMLElement) {
			$entDet = 		$this->getEntityDetails($xml);
			$entityType = 	$entDet['type'];
			$idEntity = 	$entDet['id'];
			$operation = 	$entDet['operation'];
		}

		// move the file from `queue` or `failed` to `deleted`
		if ( ($sourceDir=='queue' or $sourceDir=='failed') and $destinationDir=='deleted') {
			$this->moveFile($file, $sourceDir, $destinationDir);
			if ($entDet !== null) {
				\App\ExportData\Log::getInstance()->insertLog('manuallyMoved', $file, 'deleted', $entityType, $idEntity, $operation);
			}
			else {
				$msg = \App\MessagesCollector::create()->addInfo("Could not log deleted file because it's not a valid xml.");
				\App\ExportData\Log::getInstance()->insertLog('manuallyMoved', $file, 'deleted', "N/A", "N/A", "N/A", $msg);
			}

		}
		// move the file from `failed` to `queue`
		elseif ( ($sourceDir=='failed' or $sourceDir=='confirmed') and $destinationDir=='queue') {
			$this->moveFile($file, $sourceDir, $destinationDir);
			if ($entDet !== null) {
				\App\ExportData\Log::getInstance()->insertLog('manuallyMoved', $file, 'queue', $entityType, $idEntity, $operation);
			}
			else {
				$msg = \App\MessagesCollector::create()->addInfo("Could not log queued file because it's not a valid xml.");
				\App\ExportData\Log::getInstance()->insertLog('manuallyMoved', $file, 'queue', "N/A", "N/A", "N/A", $msg);
			}

		}
		// throw exception if source or destination directory aren't accepted
		else {
			throw new \App\MessagesException(\App\MessagesCollector::create()->addError(sprintf('The file `%s` could not be moved from `%s` to `%s`.', $file, $sourceDir, $destinationDir)));
		}
	}


	public function addLog($messageCode, $dir, \SimpleXMLElement $xml, \App\ExportData\OutputAbstract $outputObj)
	{

		$fileName = $outputObj->getOriginalFileName();


		// get entity type and id from XML
		$entDet = $this->getEntityDetails($xml);
		$entityType = $entDet['type'];
		$idEntity = $entDet['id'];
		$operation = $entDet['operation'];

		\App\ExportData\Log::getInstance()->insertLog($messageCode, $fileName, $dir, $entityType, $idEntity, $operation, $outputObj->getMessagesCollector(), $outputObj->getContextFiles());

		// store logs locally also
		$format = "Message code: `{$messageCode}`. File: {$fileName}; Dir: {$dir}. Entity: {$entityType} #{$idEntity} ({$operation}). Message: %s";
		$this->messagesCollector->appendMessages($outputObj->getMessagesCollector(), $format);



	}

	/**
	 * Extracts entity details from the XML.
	 * @param \SimpleXMLElement $xml
	 */
	private function getEntityDetails(\SimpleXMLElement $xml)
	{

		$xmlRootTag = $xml->getName();

		$type = '?';
		$id = (Int)$xml['id']; // by default most entities have its id stored here

		if ($xmlRootTag=='currencyExchangeRates') {
			$operation = 'update';
		} else {
			$operation = (String)$xml['operation'];
		}

		$entities = [
			'corporation' =>			'Corporate',
			'supplier' =>				'Supplier',
			'currencyExchangeRates' =>	'CurrencyExchangeRates',
			'invoice' =>				'Invoice',
			'cashing' =>				'InvoiceCashing',
			'supplierInvoice' =>		'SupplierInvoice',
			'payment' =>				'SupplierInvoicePayment',
			'invoiceBalance' =>			'Balance',
			'cashbookDoc' =>			'CashBookDocument',
		];
		if ($entities[$xmlRootTag]) {
			$type = $entities[$xmlRootTag];
		}

		return ['type'=>$type, 'id'=>$id, 'operation'=>$operation];

	}

	/**
	 * Returns the messages collected for this processing.
	 * @return \App\MessagesCollector
	 */
	public function getMessagesCollector()
	{
		return $this->messagesCollector;
	}

}

