<?php
namespace App\Entity;

/**
 * Class used for GDS services
 * It assumes the base table contains data & general fields in the main table (the services are in a single table, not 2 separated tables)
 * @author Lucian Sirbu
 *
 */
abstract class GDSServiceAbstract extends \App\Entity\GeneralAbstract
{
	/**
	 * Indicate if the operations available for the current entity must be done inside a transaction or not.
	 * @var Boolean
	 */
	protected $saveInsideTransaction = true;

	/**
	 * Indicate how many seconds the external cache is valid. 0 for unlimited, -1 for no cache.
	 * @var Int
	 */
	const cacheTtl=604800;	// 60*60*24*7

	/**
	 * The sistem code from the importedOrders
	 * @var string
	 */
	const systemCode = null;

	/**
	 * Get the file entity
	 * @param mixed $idFile
	 * @return \App\Entity\GeneralAbstract
	 */
	abstract protected function getFile($idFile);

	/**
	 * Get the storage object for this GDS, same as the function from the transaction obj
	 * @return \App\Utils\BaseParser\Structure\Storage\StorageHandlerAbstract
	 */
	abstract public function getStorageObjInstance();

	/**
	 * Delete is not possible for these entities
	 * @param Int $idService
	 * @throws \Exception
	 */
	static public function delete($idService)
	{
		throw new \Exception('Not developed.');
	}

	/**
	 * Returns the corresponding field of the selected entity
	 *
	 * @param String $var
	 * @return Mixed
	 */
	protected function getAttribute($var)
	{
		// CHECK EVERY POSSIBLE VALUES THAT $var MAY HAVE -----------------------------------------
		switch ($var) {
			// serialized fields
			case 'transactionReferences':
			case 'transactionData':
			case 'extraData': {
				if ($this->fields[$var]) {
					if (is_string($this->fields[$var])) {
						$serializer = new \Engine\Serializer\Custom();
						if ($serializer->isValidFormat($this->fields[$var])) {
							$this->fields[$var] = @$serializer->unserialize($this->fields[$var]);
						}
						else {
							$serializer = new \Engine\Serializer\Json();
							$this->fields[$var] = $serializer->unserialize($this->fields[$var]);

							// TODO: [GDSSerializationUpgrade] remove this, added at 2025-07-10 12:11
							// echo sprintf(
							// 	'<div style="border-left: 2px solid hsl(15, 70%%, 50%%); white-space: pre; font-family: monospace; margin-bottom: 0.75em;">' .
							// 	'<div style="font-weight: bold;">%s</div>' .
							// 	'<div>%s</div>' .
							// 	'</div>',
							// 	htmlspecialchars($var),
							// 	htmlspecialchars(var_export($this->fields[$var], true))
							// );
						}
					}

					return $this->fields[$var];
				}
				return [];
			}

			case 'attributes':
				return array_filter(explode(",", $this->fields['attributes']));

			case 'addTime':
				return $this->fields[$var];

				// return the parent file
			case 'file':
				return $this->getFile($this->getAttribute('idFile'));

			case 'parsedFileTransaction':
				$parsedData = 	$this->getAttribute('file')->parsedFileProcessor;

				$transactions = $parsedData->getTransactionsByDocNumber($this->getAttribute('docNumber'));
				return reset($transactions);

			// return the first service that got inserted for this whole PNR
			case 'PNRRootService':
				if (!array_key_exists($var, $this->fields)) {
					$this->fields[$var] = $this->getAttribute_PNRRootService_using_references();
				}

				return $this->fields[$var];

			case 'importedServiceIds':
				$listObj = new \App\EntityList\ImportedServices();
				$listObj->setFilter('importedService.auxReference', '=', $this->getId());
				$listObj->setFilter('importedService.idSystem', '=', \Entity\ExtResSystem::getSystemIdByCode(static::systemCode));
				$listObj->setOrderBy('id', 'DESC');
				return array_unique($listObj->exportRecordsByColumn('id'));

			case 'importedServiceId':
				$list = $this->getAttribute('importedServiceIds');
				return reset($list);

			case 'PNRFiles':
				$ret = [];
				if ($this->getAttribute('RECLOC')) {
					$PNRServices = $this->getStorageObjInstance()->selectServiceArrFromDB([
						'RECLOC'=>	$this->getAttribute('RECLOC'),
					]);

					foreach ($PNRServices as $ref) {
						$ret[$ref['idFile']] = $this->getFile($ref['idFile']);
					}
				}

				return array_values($ret);

			// get the parent services that this issue has reissued
			// Ex: assuming $this is a reissue transaction, it will return the referenced service
			case 'reissuedServices':
				if (!array_key_exists($var, $this->fields)) {
					if ($this->getAttribute('transactionStatus') == 'reissue') {
						$this->fields[$var] = [];

						$references = $this->getAttribute('transactionReferences');
						foreach ($references->getAll() as $ref) {
							if (($ref->id??null) && in_array($ref->relation, ['reissueOf', ])) {
								$this->fields[$var][] = static::select($ref->id);
							}
						}
					}
					else {
						$this->fields[$var] = [];
					}
				}

				return $this->fields[$var];

			case 'reissuedService':
				return ($this->reissuedServices[0]?:static::select(-1));

			// get the parent service that this refund has refunded
			// Ex: assuming $this is a refund transaction, it will return the referenced service
			case 'refundedService':
				if (!array_key_exists($var, $this->fields)) {
					if ($this->getAttribute('transactionStatus')=='refund') {
						$this->fields[$var] = null;

						$reference = $this->getAttribute('transactionReferences')->filter('parent')->last();
						if ($reference->id??null) {
							$this->fields[$var] = static::select($reference->id);
						}
					}
					else {
						$this->fields[$var] = static::select(-1);
					}
				}

				return $this->fields[$var];
			break;

			case 'nonConjunctions':
				$ret = [];

				$fileServices = $this->getStorageObjInstance()->selectServiceArrFromDB([
					'idFile'=>	$this->getAttribute('idFile'),
				]);

				foreach ($fileServices as $serv) {
					$ent = static::select($serv['id']);
					if (!in_array('isConjunction', $ent->attributes) && $ent->transactionCode==$this->getAttribute('transactionCode')) {
						$ret[] = $ent;
					}
				}

				return $ret;
			break;

			case 'nonConjunction':
				return reset($this->getAttribute('nonConjunctions'));
			break;

				// the rest of the fields (most of them are just copied)
			default: {
				return parent::getAttribute($var);
			}
		}
	}

	/**
	 * Sets the corresponding field of the inserting/updating entity
	 *
	 * @param String $var
	 * @return Mixed
	 */
	protected function setAttribute($var, $value)
	{
		// CHECK EACH POSSIBLE FIELD VALUE --------------------------------------------------------
		switch ($var) {
			// simple fields
			case 'transactionReferences':
			case 'transactionData':
			case 'extraData': {
				if ($value) {
					$this->fields[$var] = $value;
				}
				else {
					$this->fields[$var] = '';
				}
				break;
			}

			case 'serviceCode':
			case 'transactionStatus':
			case 'RECLOC':
			case 'idFile':
			case 'docNumber':
			case 'docDate':
			case 'attributes':
			{
				$this->fields[$var] = $value;
				break;
			}

			// the field is not valid
			default: {
				throw new \Exception("The field '$var' is not valid for the `".__CLASS__."` entity.");
			}
		}
	}

	/**
	 * {@inheritDoc}
	 * @see \App\Entity\GeneralAbstract::save_filterSaveData()
	 */
	protected function save_filterSaveData(Array $saveData)
	{
		$serializedFields = ['transactionReferences', 'transactionData', 'extraData',];

		$serializer = new \Engine\Serializer\Json();

		$serializerOld = new \Engine\Serializer\Custom();
		$serializerOld->setCompression(true);

		foreach ($serializedFields as $field) {
			if ($saveData[$field]) {
				$originalData = $saveData[$field];
				$saveData[$field] = $serializer->serialize($originalData);

				// TODO: [GDSSerializationUpgrade] remove this, added at 2025-07-10 12:11
				if (!compareMixedTypes($serializerOld->unserialize($serializerOld->serialize($originalData)), $serializer->unserialize($saveData[$field]), $diff, '')) {
					\Log::getInstance()->critical(sprintf("[%s] Object comparison failed! %s on %s", __METHOD__, $diff['reason'], ($diff['path']?:'root')), [
						'function' => 		__FUNCTION__,
						'field' => 			$field,
						'id' => 			$this->getId(),
						'diff' => 			new \Engine\Logger\Context\ArrayAssoc($diff),

						// if I log these, I'll get exceptions when trying to serialize the messagesCollector objects, so I disabled it
						// 'object' => 		new \Engine\Logger\Context\Auto($originalData),
						// 'serialized' => 	new \Engine\Logger\Context\Auto($serializer->unserialize($saveData[$field])),
					]);
					throw new \RuntimeException(sprintf("Object comparison failed!"));
				}

				// TODO: remove this, added at 2025-09-16 16:17
				// $saveData[$field] = $serializerOld->serialize($originalData);
			}
		}

		if ($saveData['attributes']) {
			$saveData['attributes'] = implode(",", $saveData['attributes']);
		}

		return $saveData;
	}

	/**
	 * Getter for PNRRootService
	 * @return self
	 */
	public function getAttribute_PNRRootService_using_RECLOC()
	{
		if (!$this->getAttribute('RECLOC')) {
			return $this;
		}

		/**
		 * In Amadeus the RECLOC pool gets cleared every 4 months, not sure about this specific GDS, but I'm assuming the same
		 * This means that we only need to look 4-6 months behind to look for related services, not in the whole table
		 */
		$PNRServices = $this->getStorageObjInstance()->selectServiceArrFromDB([
			'RECLOC' => 	$this->getAttribute('RECLOC'),
			'docDate>' => 	\Engine\DateTime::createFromIsoDate($this->getAttribute('docDate'))->modify("-6 months")->formatDateIso(),
		]);

		// filter out services whose service was "executed", so it could be considered in an "old" PNR
		if ($this->docDate) {
			$docDate = \Engine\DateTime::createFromIsoDate($this->docDate)->modify("-1 month")->formatDateIso();
			foreach ($PNRServices as $k=>$srvArr) {
				$srv = self::select($srvArr['id']);
				if ($srv->transactionData->itinerary && $srv->transactionData->itinerary->getDestination() && $srv->transactionData->itinerary->getDestination()->date
					&& $srv->transactionData->itinerary->getDestination()->date<$docDate) {
					unset($PNRServices[$k]);
				}
			}
			$PNRServices = array_values($PNRServices);
		}

		$referenceIds = [];
		$referenceIds[] = $this->getId();
		foreach ($PNRServices as $ref) {
			$referenceIds[] = $ref['id'];
		}

		$referenceIds = array_unique($referenceIds);

		foreach ($referenceIds as $refId) {
			$p = static::select($refId);
			$refs = $p->transactionReferences;
			/* @var $refs \App\Utils\BaseParser\Structure\Storage\Types\TransactionReferences */
			if ($refs) {
				foreach ($refs->getAll() as $ref) {
					/* @var $ref \App\Utils\BaseParser\Structure\Storage\Types\TransactionReference */
					if ($ref->id??null) {
						$referenceIds[] = $ref->id;
					}
				}
			}
		}

		$referenceIds = array_unique($referenceIds);

		$parent = static::select(min($referenceIds));

		return $parent;
	}

	/**
	 * Getter for PNRRootService
	 * @return self
	 */
	public function getAttribute_PNRRootService_using_transactionCode()
	{
		if (!$this->getAttribute('RECLOC')) {
			return $this;
		}

		/**
		 * In Amadeus the RECLOC pool gets cleared every 4 months, not sure about this specific GDS, but I'm assuming the same
		 * This means that we only need to look 4-6 months behind to look for related services, not in the whole table
		 */
		$PNRServices = $this->getStorageObjInstance()->selectServiceArrFromDB([
			'transactionCode~' => 	$this->getAttribute('RECLOC').'-%',
			'docDate>' => 	\Engine\DateTime::createFromIsoDate($this->getAttribute('docDate'))->modify("-6 months")->formatDateIso(),
		]);

		// filter out services whose service was "executed", so it could be considered in an "old" PNR
		if ($this->docDate) {
			$docDate = \Engine\DateTime::createFromIsoDate($this->docDate)->modify("-1 month")->formatDateIso();
			foreach ($PNRServices as $k=>$srvArr) {
				$srv = self::select($srvArr['id']);
				if ($srv->transactionData->itinerary && $srv->transactionData->itinerary->getDestination() && $srv->transactionData->itinerary->getDestination()->date
					&& $srv->transactionData->itinerary->getDestination()->date<$docDate) {
					unset($PNRServices[$k]);
				}
			}
			$PNRServices = array_values($PNRServices);
		}


		$referenceIds = [];
		$referenceIds[] = $this->getId();
		foreach ($PNRServices as $ref) {
			$referenceIds[] = $ref['id'];
		}

		$referenceIds = array_unique($referenceIds);

		foreach ($referenceIds as $refId) {
			$p = static::select($refId);
			$refs = $p->transactionReferences;
			/* @var $refs \App\Utils\BaseParser\Structure\Storage\Types\TransactionReferences */
			if ($refs) {
				foreach ($refs->getAll() as $ref) {
					/* @var $ref \App\Utils\BaseParser\Structure\Storage\Types\TransactionReference */
					if ($ref->id??null) {
						$referenceIds[] = $ref->id;
					}
				}
			}
		}

		$referenceIds = array_unique($referenceIds);

		$parent = static::select(min($referenceIds));

		return $parent;
	}

	/**
	 * Getter for PNRRootService
	 * @return \Entity\GalileoService
	 */
	public function getAttribute_PNRRootService_using_references()
	{
		if ($this->operation!='select') {
			throw new \Exception('You cannot call `'.__METHOD__.'` for `'. $this->operation .'` operations.');
		}
		return static::select(min($this->getAllRelatedReferences()));
	}

	protected function getAllRelatedReferences()
	{
		$referenceIds = [];
		$referenceIds[]= $this->getId();

		do {
			$newReferenceAdded = false;

			foreach ($referenceIds as $refId) {
				$p = static::select($refId);
				$refs = $p->transactionReferences;
				/* @var $refs \App\Utils\BaseParser\Structure\Storage\Types\TransactionReferences */
				if ($refs) {
					foreach ($refs->getAll() as $ref) {
						/* @var $ref \App\Utils\BaseParser\Structure\Storage\Types\TransactionReference */
						if (($ref->id??null) && !in_array($ref->id, $referenceIds)) {
							$referenceIds[] = $ref->id;
							$newReferenceAdded = true;
						}
					}
				}

				$fileServices = $this->getStorageObjInstance()->selectServiceArrFromDB([
					'idFile' => $p->getAttribute('idFile'),
				]);

				foreach ($fileServices as $srvArr) {
					if (!in_array($srvArr['id'], $referenceIds)) {
						$referenceIds[]= $srvArr['id'];
						$newReferenceAdded = true;
					}
				}
			}
		} while ($newReferenceAdded);

		return array_unique($referenceIds);
	}
}
