<?php
namespace App\Sabre\Storage;

/**
 * Class to handle internal DB storage
 * @author Lucian Sirbu
 */
class StorageHandler extends \App\Utils\BaseParser\Structure\Storage\StorageHandlerAbstract
{
	use \App\Utils\BaseParser\SingletonTrait;

	public function __construct()
	{
		global $DBTSabreServices;
		$this->table = $DBTSabreServices;

		parent::__construct();
	}

	/**
	 * {@inheritDoc}
	 * @see \App\Utils\BaseParser\Structure\Storage\StorageHandlerAbstract::getSelectedEntity()
	 */
	public function getSelectedEntity($id)
	{
		return \Entity\SabreService::select($id);
	}

	/**
	 * Insert the parsed file into the DB
	 */
	protected function insertParsedFile()
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		$parsedFile = \Entity\SabreFile::select($this->processor->getFileParser()->getFileId());
		if ($parsedFile->recordExists()) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
			throw new \App\Sabre\Storage\Exception("File is already parsed! (original file was '{$parsedFile->filePath}' on {$parsedFile->addTime})");
		}

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
			'getFileId' => $this->processor->getFileParser()->getFileId(),
			'filePath' => $this->processor->getFile(),
		]);

		// let the entity throw exceptions
		$parsedFile = 			\Entity\SabreFile::insert();
		$parsedFile->idFile = 	$this->processor->getFileParser()->getFileId();

		$parsedFile->filePath = $this->processor->getFile();

		$parsedFile->source = 	$this->getOption('source');
		$parsedFile->IP = 		$this->getOption('IP');

		$parsedFile->save();
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
	}

	public function transactionAlreadyExists(\App\Utils\BaseParser\Structure\StructureParser\Structures\Transaction $transaction)
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		$ret = false;
		$minDocDate = \Engine\DateTime::createFromIsoDate($transaction->documents->docDate)->modify("-2 years")->formatDateIso();
		$maxDocDate = \Engine\DateTime::createFromIsoDate($transaction->documents->docDate)->modify("+2 year")->formatDateIso();

		switch ($transaction->getType()) {
			case 'ticketing':
				if ($transaction->documents->docNumber) {
					$existingTransactions = $this->selectPreviousServiceArrFromDB([
						'docNumber' => $transaction->documents->docNumber,
						'docDate>' => $minDocDate,
						'docDate<' => $maxDocDate,
						'idFile!' => $this->processor->getFileParser()->getFileId(),
						'transactionStatus' => ['new', 'refund', 'refunded', 'reissued', 'voided', ],
						'serviceCode' => 'ticketing',
					]);

					$ret = (bool)count($existingTransactions);
				}
				else {
					$ret = false;
				}
			break;

			case 'EMD':
				$existingTransactions = $this->selectPreviousServiceArrFromDB([
					'docNumber' => $transaction->documents->docNumber,
					'docDate>' => $minDocDate,
					'docDate<' => $maxDocDate,
					'idFile!' => $this->processor->getFileParser()->getFileId(),
					'transactionStatus' => ['new', 'reissued', 'refunded', 'voided', ],
					'serviceCode' => 'EMD',
				]);

				$ret = (bool)count($existingTransactions);
			break;

			case 'MCO':
				$existingTransactions = $this->selectPreviousServiceArrFromDB([
					'docNumber' => $transaction->documents->docNumber,
					'docDate>' => $minDocDate,
					'docDate<' => $maxDocDate,
					'idFile!' => $this->processor->getFileParser()->getFileId(),
					'transactionStatus' => ['new', 'reissued', 'refunded', 'voided', ],
					'serviceCode' => 'MCO',
				]);

				$ret = (bool)count($existingTransactions);
			break;

			case 'reissue':
				$existingTransactions = $this->selectPreviousServiceArrFromDB([
					'docNumber' => $transaction->documents->docNumber,
					'docDate>' => $minDocDate,
					'docDate<' => $maxDocDate,
					'idFile!' => $this->processor->getFileParser()->getFileId(),
					'transactionStatus' => ['refund', 'refunded', 'reissue', 'reissued', 'voided', ],
					'serviceCode' => 'ticketing',
				]);

				$ret = (bool)count($existingTransactions);
			break;

			case 'refund':
				$existingTransactions = $this->selectPreviousServiceArrFromDB([
					'docNumber' => $transaction->documents->docNumber,
					'docDate>' => $minDocDate,
					'docDate<' => $maxDocDate,
					'idFile!' => $this->processor->getFileParser()->getFileId(),
					'transactionStatus' => ['refund', 'refunded', ],
					'serviceCode' => 'ticketing',
				]);

				$ret = false;
				if ($existingTransactions) {
					$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();
					if ($CFG['sabre']['CompatibilityWithImportedOrders']['allowRefundUpdate']) {
						$sameDateAndPriceTransactions = array_filter($existingTransactions, function ($t) use ($transaction) {
							$tr = \Entity\SabreService::select($t['id']);
							return $tr->transactionData->documents->docDate == $transaction->documents->docDate
								&& $tr->transactionData->price->total == $transaction->price->total;
						});

						if ($sameDateAndPriceTransactions) {
							$ret = true;	// true duplicate: same date & price, skip it
						}
						else {
							$transaction->nonStructuredDataSet('isRefundUpdate', true);	// corrected refund: let it through
						}
					}
				}
			break;

			case 'hotel':
			case 'car':
			case 'train':
				if ($transaction->documents->docNumber) {
					$existingTransactions = $this->selectPreviousServiceArrFromDB([
						'docNumber' => $transaction->documents->docNumber,
						'docDate>' => $minDocDate,
						'docDate<' => $maxDocDate,
						'idFile!' => $this->processor->getFileParser()->getFileId(),
						'transactionStatus' => ['new', 'refund', 'refunded', 'reissued', 'voided', ],
						'transactionCode^' =>  "{$transaction->documents->RECLOC}-",
						'serviceCode' => $transaction->getType(),
					]);

					$ret = (bool)count($existingTransactions);
				}
				else {
					$ret = false;
				}
			break;

			case 'airTaxi':
			case 'documentIssue':
				if ($transaction->documents->docNumber) {
					$existingTransactions = $this->selectPreviousServiceArrFromDB([
						'docNumber' => $transaction->documents->docNumber,
						'docDate>' => $minDocDate,
						'docDate<' => $maxDocDate,
						'idFile!' => $this->processor->getFileParser()->getFileId(),
						'transactionStatus' => ['new', 'refund', 'refunded', 'reissued', 'voided', ],
						'transactionCode^' =>  "{$transaction->documents->RECLOC}-",
						'serviceCode' => $transaction->getType(),
					]);

					$ret = (bool)count($existingTransactions);
				}
				else {
					$existingTransactions = $this->selectPreviousServiceArrFromDB([
						'docDate>' => $minDocDate,
						'docDate<' => $maxDocDate,
						'idFile!' => $this->processor->getFileParser()->getFileId(),
						'transactionStatus' => ['new', 'refund', 'refunded', 'reissued', 'voided', ],
						'transactionCode' =>  $transaction->documents->transactionCode,
						'serviceCode' => $transaction->getType(),
					]);

					$ret = (bool)count($existingTransactions);
				}
			break;
		}

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logOutputWithNull([
			'ret' => $ret,
		]);

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
		return $ret;
	}

	/**
	 * Loop throgh all transactions in this file and insert/update the DB as needed
	 * @return multitype:
	 */
	protected function insertParsedFileTransactions()
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		// loop through all transactions and store them in the DB flipping various fields as needed
		$transactions = $this->processor->getTransactions();

		if ($transactions) {
			foreach ($transactions as $transaction) {
				\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__.":foreach", func_get_args());

				/* @var $transaction \App\Sabre\StructureParser\Structures\Transaction */

				\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logInput([
					'$transaction->getType()' => $transaction->getType(),
					'$transaction->documents->docNumber' => $transaction->documents->docNumber,
				]);

				// check that this transaction doesn't already exists in the DB
				if ($this->transactionAlreadyExists($transaction)) {
					$transaction->nonStructuredDataSet('storage:skipped', true);
					\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
					continue;	// Sabre repeats ticketing & refund transactions in subsequent files. We should ignore them after the first insert
				}

				$oldTransactions = [];
				switch ($transaction->getType()) {
					case 'void' :
					case 'refund' :
						$oldTransactions = $this->convertSelectionArrToEntityArr($this->selectPreviousServiceArrFromDB([
							'docNumber' => $transaction->documents->docNumber,
							'idFile!' => $this->processor->getFileParser()->getFileId(),
						]));
					break;

					case 'ticketing':
						if ($transaction->documents->docNumber) {
							$oldTransactions = $this->convertSelectionArrToEntityArr($this->selectPreviousServiceArrFromDB([
								'docNumber' => $transaction->documents->docNumber,
								'idFile!' => $this->processor->getFileParser()->getFileId(),
							]));
						}
						else {
							$oldTransactions = $this->convertSelectionArrToEntityArr($this->selectPreviousServiceArrFromDB([
								'transactionCode' => $transaction->documents->transactionCode,
								'idFile!' => $this->processor->getFileParser()->getFileId(),
							]));
						}
					break;

					case 'reissue':
						$oldTransactions = $this->convertSelectionArrToEntityArr($this->selectPreviousServiceArrFromDB([
							'docNumber' => $transaction->relations->filter('reissue')->first()->docNumber,
							'idFile!' => $this->processor->getFileParser()->getFileId(),
						]));
					break;
				}

				$this->insertAndUpdateTransaction($transaction, $oldTransactions);

				\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
			} // end: foreach $transactions
		}
		else {
			// no transactions defined, dont know what to do next
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
			throw new \App\Sabre\Storage\Exception("Received a file without transactions. Dont know how to handle this case.");
		}

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
		return $this->dbTransactions;
	}

	/**
	 * Insert the transactions in the DB
	 * @param \App\Sabre\StructureParser\Structures\Transaction $transaction
	 * @return array:\Entity\SabreService
	 */
	protected function insertTransaction(\App\Utils\BaseParser\Structure\StructureParser\Structures\Transaction $transaction, $oldTransactions = [])
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());
		/****************************************************************************
		 * item doesn't exist in the DB, its new, insert it now
		 ****************************************************************************/
		$insTransactions = [];

		if ($transaction->getIndex('reissue')) {
			\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
			throw new \App\Sabre\Storage\Exception("Received a reissue operation, but don't know how to handle it");
		}

		$insTransaction = \Entity\SabreService::insert();

		// for some transactions get the serviceType from the previous transaction
		switch ($transaction->getType()) {
			case 'ticketing':
				$insTransaction->transactionStatus = 	'new';
			break;

			case 'reissue':
				if (!$oldTransactions) {
					\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
					throw new \App\Sabre\Storage\Exception(sprintf("We received a reissue operation, but we cannot find the original ticket to apply the reissue to. (RECLOC: %s, createDate: %s)",
						$transaction->documents->RECLOC,
						$transaction->documents->createDate
					));
				}

				$insTransaction->transactionStatus = 	'reissue';
			break;

			case 'void':
				if (!$oldTransactions) {
					\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
					throw new \App\Sabre\Storage\Exception(sprintf("We received a void operation, but we cannot find the original ticket to apply the void to. (RECLOC: %s, createDate: %s)",
						$transaction->documents->RECLOC,
						$transaction->documents->createDate
					));
				}

				$insTransaction->serviceCode = $oldTransactions[0]->serviceCode;
				$insTransaction->transactionStatus = 	'void';
			break;

			case 'refund':
				if (!$oldTransactions) {
					\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
					throw new \App\Sabre\Storage\Exception(sprintf("We received a refund operation, but we cannot find the original ticket to apply the refund to. (RECLOC: %s, createDate: %s)",
						$transaction->documents->RECLOC,
						$transaction->documents->createDate
					));
				}

				$insTransaction->transactionStatus = 	'refund';
			break;

			default:
				$insTransaction->serviceCode = 			$transaction->getType();
				$insTransaction->transactionStatus = 	'new';
			break;
		}

		$insTransaction->idFile = 				$this->processor->getFileParser()->getFileId();
		$insTransaction->docNumber = 			$transaction->documents->docNumber;
		$insTransaction->docDate = 				$transaction->documents->docDate;
		$insTransaction->transactionCode = 		$transaction->documents->transactionCode;

		$transactionReferences = new \App\Sabre\Storage\Types\TransactionReferences();
		foreach ($oldTransactions as $tr) {
			if ($transaction->getType()=='reissue') {
				$reference = new \App\Sabre\Storage\Types\TransactionReference('reissueOf', (string)$tr->getId());
			}
			elseif ($transaction->getType()=='ticketing') {
				$reference = new \App\Sabre\Storage\Types\TransactionReference('child', (string)$tr->getId());
			}
			else {
				$reference = new \App\Sabre\Storage\Types\TransactionReference('parent', (string)$tr->getId());
			}
			$transactionReferences->append($reference);
		}

		if ($transaction->relations) {
			foreach ($transaction->relations->relations as $rel) {
				if ($rel->relationType!='reissue') {
					$filters = [];
					$filters['docNumber'] = $rel->docNumber;

					if ($rel->docDate) {
						$filters['docDate'] = $rel->docDate;
					}
					else {
						$filters['docDate>'] = \Engine\DateTime::createFromIsoDate($transaction->documents->createDate)->modify("-6 months")->formatDateIso();
						// $filters['docDate<'] = \Engine\DateTime::createFromIsoDate($transaction->documents->docDate)->modify("+1 day")->formatDateIso();
					}

					$relationEntity = $this->selectServiceArrFromDB($filters);
					if ($relationEntity) {
						$reference = new \App\Sabre\Storage\Types\TransactionReference($rel->relationType, (string)$relationEntity[0]['id']);
						$transactionReferences->append($reference);
					}
					else {
						throw new \App\Sabre\Storage\Exception(sprintf("Cannot find referenced relation %s/%s", $rel->docNumber, $rel->relationType));
					}
				}
			}
		}

		$insTransaction->transactionReferences = $transactionReferences;

		// user / offices
		if ($transaction->agent and $transaction->agent->bookingAgent) {
			$insTransaction->idSabreBookUser = 		$transaction->agent->bookingAgent->idSystemUser;
			$insTransaction->idSabreBookOffice = 	$transaction->agent->bookingAgent->idSystemOffice;
		}
		if ($transaction->agent and $transaction->agent->issuingAgent) {
			$insTransaction->idSabreIssueUser = 	$transaction->agent->issuingAgent->idSystemUser;
			$insTransaction->idSabreIssueOffice = 	$transaction->agent->issuingAgent->idSystemOffice;
		}

		$insTransaction->transactionData = 		$transaction;
		$insTransaction->extraData = 			$transaction->nonStructuredDataGet('isRefundUpdate') ? ['isRefundUpdate' => true] : [];

		// perform the final save
		$insTransaction->save();

		$insTransactions[] = $insTransaction;

		if ($transaction->documents->conjunctionDocNumbers && count($transaction->documents->conjunctionDocNumbers)>1) {
			foreach ($transaction->documents->conjunctionDocNumbers as $docNumber) {
				if ($docNumber!=$transaction->documents->docNumber) {
					// "clone" into a new instance
					$newTransaction = \Entity\SabreService::insert();
					$fields = $insTransaction->getFields();
					$newTransaction->setFields($fields);
					$newTransaction->docNumber = 	$docNumber;
					$newTransaction->attributes = 	['isConjunction', ];
					$newTransaction->save();
					$insTransactions[] = $newTransaction;
				}
			}
		}

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
		return $insTransactions;
	}

	/**
	 * Update the transactions list
	 */
	protected function updateTransaction(\App\Utils\BaseParser\Structure\StructureParser\Structures\Transaction $transaction, $oldTransactions, $insTransactions)
	{
		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallStart($this, __METHOD__, func_get_args());

		$updOldTransactions = [];
		foreach ($oldTransactions as $oldTransaction) {
			// update the old transaction status
			$updOldTransaction = \Entity\SabreService::update($oldTransaction->getId());

			switch ($transaction->getType()) {
				case 'ticketing':
					// if this is an IMR, then avoid invalidating the previous transaction if it was an issue
					if ($transaction->documents->docNumber && !$oldTransaction->docNumber){
						$updOldTransaction->transactionStatus = 'old';
					}
				break;

				case 'reissue':
					$updOldTransaction->transactionStatus = 'reissued';
				break;

				case 'void':
					$updOldTransaction->transactionStatus = 'voided';
				break;

				case 'refund':
					$updOldTransaction->transactionStatus = 'refunded';
				break;
			}

			$transactionReferences = $oldTransaction->transactionReferences;

			foreach ($insTransactions as $insTransaction) {
				if ($transaction->getType()=='ticketing') {
					$transactionReferences->append(new \App\Sabre\Storage\Types\TransactionReference('parent', (string)$insTransaction->getId()));
				}
				else {
					$transactionReferences->append(new \App\Sabre\Storage\Types\TransactionReference('child', (string)$insTransaction->getId()));
				}
			}

			$updOldTransaction->transactionReferences = $transactionReferences;

			$updOldTransaction->save();

			$updOldTransactions[] = $updOldTransaction;
		}

		\Engine\Debug\ParserCallTrace\ParserCallTrace::getInstance()->logCallEnd();
		return $updOldTransactions;
	}


	/**
	 * Set the internal processor
	 */
	public function setProcessor($processor)
	{
		$this->processor = $processor;
	}
}
