<?php

namespace Entity;


class AmadeusService 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 = 'amadeus';

	/**
	 * The table name containing service data
	 * @var string
	 */
	protected $dataTableName;

	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\SelectAbstract::setTableName()
	 */
	protected function setTableName(): void
	{
		global $DBTAmadeusServices, $DBTAmadeusServicesData;
		$this->tableName = $DBTAmadeusServices;
		$this->dataTableName = $DBTAmadeusServicesData;
	}

	/**
	 * Static method for delete a service.
	 * @return \Entity\AmadeusService
	 */
	static public function delete($idService)
	{
		throw new \Exception('Not developed.');
	}

	/**
	 * Check if 2 transactions refer to the same actual service (Ex: hotel issue #1 and hotel update #2 should return true)
	 * @param \Entity\AmadeusService $entity1
	 * @param \Entity\AmadeusService $entity2
	 * @return boolean
	 */
	static public function isSameService(\Entity\AmadeusService $entity1, \Entity\AmadeusService $entity2)
	{
		/**
		 * this can be implemented in multiple ways
		 *  - we could check transactionCode (current code, probably easiest)
		 *  - we could look through the references and track operations as they unravel, and mark their connections
		 *
		 */
		return (
				($entity1->serviceCode == $entity2->serviceCode)
				&& ($entity1->transactionCode == $entity2->transactionCode)
				&& ($entity1->idFile != $entity2->idFile)
			) || (
				$entity1->getId() == $entity2->getId()
			);
	}

	/**
	 * Load raw data(blobs and text fields) from the extra data-table.
	 * It populates the internal fields
	 * @param mixed $id
	 * @throws \DBException
	 */
	protected function loadExtraDataTable($id)
	{
		$db = \DB::getInstance();
		$data = $db->GetRow("SELECT * FROM `{$this->dataTableName}` WHERE `id`=?", [$id]);
		if ($db->ErrorNo()) {
			throw new \DBException("Cannot load data from {$this->dataTableName} for {$id}");
		}

		foreach ($data as $k=>$v) {
			if (!array_key_exists($k, $this->fields)) {
				$this->fields[$k] = $v;
			}
		}
	}

	/**
	 * 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 'transactionParseLog': case 'transactionProcessLog': case 'transactionDocData': case 'transactionSupplierData':
				case 'transactionPriceData': case 'transactionPassengerData': case 'transactionSegmentData': case 'transactionExtraData': case 'transactionRemarksData':
				case 'transactionBookingAgentData': case 'transactionIssueAgentData': case 'extraData': {
					if (!array_key_exists($var, $this->fields)) {
						$this->loadExtraDataTable($this->id);
					}
	
					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
								// TODO: 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 null;
				}
	

			case 'dateStart':
			case 'dateEnd':
			case 'addTime':
				return ($this->fields[$var]=='0000-00-00 00:00:00'?'':$this->fields[$var]);

			case 'departureCountry':
				return \Entity\Country::select($this->fields['departureCountryCode']);
			case 'destinationCountry':
				return \Entity\Country::select($this->fields['destinationCountryCode']);

			case 'departureCity':
				return \Entity\City::select($this->fields['departureCityId']);
			case 'destinationCity':
				return \Entity\City::select($this->fields['destinationCityId']);

			case 'departureAirport':
				return \Entity\Station::select($this->fields['departureAirportCode']);
			case 'destinationAirport':
				return \Entity\Station::select($this->fields['destinationAirportCode']);

			// return the parent file
			case 'file': {
				return \Entity\AmadeusFile::select($this->getAttribute('idFile'));
			}

			// return the transaction from the parent file
			case 'parsedFileTransaction': {
				if (!array_key_exists($var, $this->fields)) {
					$processor = 	$this->getAttribute('file')->parsedFileProcessor;
					$transactions = $processor->getTransactions();
					$this->fields[$var] = $processor->getTransactionByTransactionCode($transactions, $this->getAttribute('transactionCode'));
				}
				return $this->fields[$var];
			}

			// extract the record locator
			case 'RECLOC':
				return substr($this->getAttribute('transactionCode'), 0, 6);
			break;

			case 'TINABookUser':
				return User::select($this->fields['idTINABookUser']);

			case 'TINAIssueUser':
				return User::select($this->fields['idTINAIssueUser']);

			case 'TINAServiceIds':
				if ($this->recordExists()) {
					$listObj = new \App\EntityList\ImportedServices();
					$listObj->setColumn('idServiceTina');
					$listObj->setFilter('importedService.auxReference', '=', $this->getId());
					$listObj->setFilter('importedService.processStatus', '=', 'processed');
					$listObj->setFilter('importedService.idSystem', '=', \Entity\ExtResSystem::getSystemIdByCode(static::systemCode));
					$listObj->setOrderBy('id', 'DESC');
					return array_unique($listObj->exportRecordsByColumn('idServiceTina'));
				}
				else {
					return [];
				}

			case 'TINAServiceId':
				$list = $this->getAttribute('TINAServiceIds');
				return reset($list);

			case 'importedServiceIds':
				if ($this->recordExists()) {
					$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'));
				}
				else {
					return [];
				}

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

			// 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;

						$references = $this->getAttribute('transactionReferences');
						foreach($references as $ref) {
							if ($ref['relationType']=='refund') {
								$this->fields[$var] = static::select($ref['id']);
								break;
							}
						}
					}
					else {
						$this->fields[$var] = static::select(-1);
					}
				}

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

			// get the parent services that this refund 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] = null;

						$references = $this->getAttribute('transactionReferences');
						foreach($references as $ref) {
							if ($ref['relationType']=='reissueOf') {
								$this->fields[$var][] = static::select($ref['id']);
							}
						}
					}
					else {
						$this->fields[$var] = [];
					}
				}

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

			case 'voidedService':
				if (!array_key_exists($var, $this->fields)) {
					if ($this->getAttribute('transactionStatus')=='void') {
						$this->fields[$var] = null;

						$references = $this->getAttribute('transactionReferences');
						foreach($references as $ref) {
							if ($ref['relationType']=='void') {
								$this->fields[$var] = static::select($ref['id']);
								break;
							}
						}
					}
					else {
						$this->fields[$var] = [];
					}
				}

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

			case 'references':
				if (!array_key_exists($var, $this->fields)) {
					$references = $this->getAttribute('transactionReferences');
					$this->fields[$var] = [];
					if ($references) {
						foreach($references as $ref) {
							$ref['service'] = static::select($ref['id']);
							$this->fields[$var][] = $ref;
						}
					}
				}

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

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

			// 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();
				}

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

			case 'PNRFiles':
				$ret = [];

				if ($this->getAttribute('RECLOC')) {
					$PNRServices = \App\Amadeus\Storage\StorageHandler::selectByTransactionCodeComponents([
						'RECLOC'=>	$this->getAttribute('RECLOC'),
					]);

					foreach ($PNRServices as $ref) {
						$ret[$ref['idFile']] = \Entity\AmadeusFile::select($ref['idFile']);
					}
				}

				return array_values($ret);
			break;

			case 'conjunctions':
				$ret = [];

				$fileServices = \App\Amadeus\Storage\StorageHandler::selectByTransactionCodeComponents([
					'idFile'=>	$this->getAttribute('idFile'),
				]);

				foreach ($fileServices as $serv) {
					if ($serv['autoIgnored']=='conjunction' and $serv['transactionCode']==$this->getAttribute('transactionCode')) {
						$ret[] = \Entity\AmadeusService::select($serv['id']);
					}
				}

				return $ret;
			break;

			case 'nonConjunctions':
				$ret = [];

				$fileServices = \App\Amadeus\Storage\StorageHandler::selectByTransactionCodeComponents([
					'idFile'=>	$this->getAttribute('idFile'),
				]);

				foreach ($fileServices as $serv) {
					if ($serv['autoIgnored']!='conjunction' and $serv['transactionCode']==$this->getAttribute('transactionCode')) {
						$ret[] = \Entity\AmadeusService::select($serv['id']);
					}
				}

				return $ret;
			break;

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

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

	/**
	 * Getter for PNRRootService
	 * @return \Entity\AmadeusService
	 */
	protected function getAttribute_PNRRootService()
	{
		if (!$this->getAttribute('RECLOC')) {
			return $this;
		}

		/**
		 * In Amadeus the RECLOC pool gets cleared every 4 months.
		 * This means that we only need to look 4 months behind to look for related services, not in the whole table.
		 * This does NOT mean that the ticket expires with the RECLOC!!!
		 *
		 * There is an exception though: for refunds, there is no time limit (according to Paul/ParavionRO), so we may get a refund for EMD's older than these 4 months
		 *
		 *	@see TD-1066: problems with a ticket issued on a PNR already used in 2013 (may 2015)
		 * 	@see TD-4129: error when trying to parse a refund AIR file #11967         (dec 2017)
		 *  @see TD-4129: seems to be an exception for EMD's, because here, the 2 EMD files are issued 6 months and 10 days apart
		 *  @see TD-5183: again, there seems that we misunderstood the limit. It seems that it applies 4 months after the service endDate, not docDate (ian 2019)
		 *
		 * Also used in StorageHandler->lookupOldTransactions(), with less details
		 */

		if ($this->getAttribute('transactionStatus')=='void') {
			// voids should come in the same day as the original issue, so 1 month look-behind is more than generous
			$PNRServices = \App\Amadeus\Storage\StorageHandler::selectByTransactionCodeComponents([
				'RECLOC' =>					$this->getAttribute('RECLOC'),
				'docDate:min' => 			\Engine\DateTime::createFromIsoDate($this->getAttribute('docDate'))->modify("-1 months")->formatDateIso(),
			]);
		}
		elseif ($this->getAttribute('transactionStatus')=='refund') {
			// refunds seem to be special snowflakes, as they act dirrectly on the ticket, wihout really needing an active PNR for it
			$PNRServices = \App\Amadeus\Storage\StorageHandler::selectByTransactionCodeComponents([
				'RECLOC' =>					$this->getAttribute('RECLOC'),
				'docDate:min' => 			\Engine\DateTime::createFromIsoDate($this->getAttribute('docDate'))->modify("-12 months")->formatDateIso(),
				'docNumber' =>				$this->getAttribute('docNumber'),
			]);
		}

		// !!!!!!!!!!!! we now compare serviceCode !!!!!!!!!!!!
		elseif ($this->getAttribute('serviceCode')=='EMD') {
			// EMD's should be handled differently because we can issue them a long time after or before the original reservation date
			$PNRServices = \App\Amadeus\Storage\StorageHandler::selectByTransactionCodeComponents([
				'RECLOC' =>					$this->getAttribute('RECLOC'),
				'docDate:min' => 			\Engine\DateTime::createFromIsoDate($this->getAttribute('docDate'))->modify("-12 months")->formatDateIso(),
			]);
		}
		else {
			// issue operations
			$PNRServices = \App\Amadeus\Storage\StorageHandler::selectByTransactionCodeComponents([
				'RECLOC' =>					$this->getAttribute('RECLOC'),
				'RECLOC_poolDate:min' => 	\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) {
				// please note that docDate is Y-m-d, while dateEnd is Y-m-d H:i:s
				$docDate = \Engine\DateTime::createFromIsoDate($this->docDate)->modify("-1 month")->formatDateTimeIso();
				foreach ($PNRServices as $k=>$srvArr) {
					if ($srvArr['dateEnd'] && $srvArr['dateEnd']<$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);
			if ($p->transactionReferences) {
				foreach($p->transactionReferences as $ref) {
					if ($ref['id']) {
						$referenceIds[] = $ref['id'];
					}
				}
			}
		}

		$referenceIds = array_unique($referenceIds);

		$parent = static::select(min($referenceIds));
		if ($parent->getId()!=$this->getId()) {
			if ($parent->PNRRootService) {
				$parent = $parent->PNRRootService;
			}
		}

		// handle SPLIT's
		if ($parent->transactionReferences) {
			foreach ($parent->transactionReferences as $ref) {
				if ($ref['relationType'] == 'reissueOf') {
					$parent = $parent->PNRRootService;
					break;
				}
			}
		}

		return $parent;
	}

	/**
	 * 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 'transactionParseLog': case 'transactionProcessLog': case 'transactionDocData': case 'transactionSupplierData':
			case 'transactionPriceData': case 'transactionPassengerData': case 'transactionSegmentData': case 'transactionExtraData': case 'transactionRemarksData':
			case 'transactionBookingAgentData': case 'transactionIssueAgentData': case 'extraData': {
				if($value){
					$this->fields[$var] = $value;
				}
				else {
					$this->fields[$var] = '';
				}
				break;
			}

			case 'serviceCode': case 'transactionStatus': case 'transactionCode': case 'idFile':
			case 'docNumber':	case 'docDate':
			case 'autoIgnored':
			case 'departureCountryCode': case 'departureCityId': case 'departureAirportCode':
			case 'destinationCountryCode': case 'destinationCityId': case 'destinationAirportCode':
			case 'dateStart': case 'dateEnd': case 'passengerFirstName': case 'passengerLastName':
			case 'priceTotal': case 'priceTotalCurrency': case 'priceSupplier': case 'priceSupplierCurrency': case 'priceTax': case 'priceTaxCurrency':
			case 'priceCommission': case 'priceCommissionCurrency': case 'priceServiceFee': case 'priceServiceFeeCurrency':
			case 'priceCommissionable': case 'formOfPayment':
			case 'idTINABookUser': case 'idTINAIssueUser': case 'idTINABookOffice': case 'idTINAIssueOffice':
			case 'idAmadeusBookUser': case 'idAmadeusIssueUser': case 'idAmadeusBookOffice': case 'idAmadeusIssueOffice':
			{
				$this->fields[$var] = $value;
				break;
			}

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

	protected function save_filterSaveData(Array $saveData)
	{
		$serializedFields = ['transactionReferences', 'transactionParseLog', 'transactionProcessLog', 'transactionDocData', 'transactionSupplierData',
			'transactionPriceData', 'transactionPassengerData', 'transactionSegmentData', 'transactionExtraData', 'transactionRemarksData',
			'transactionBookingAgentData', 'transactionIssueAgentData', '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);
			}
		}

		return $saveData;
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\GeneralAbstract::save_finalSaveData()
	 */
	protected function save_finalSaveData(Array $saveData)
	{
		if ($this->operation == 'insert') {
			$saveData['addTime'] =  DATETIME;
			$saveData['idAddUser']= CURRENT_ID_USER;
		}

		if (in_array($this->operation, ['update']) ) {
			$saveData['lastUpdateTime'] = 	DATETIME;
			$saveData['idUpdateUser'] = 	CURRENT_ID_USER;
			$saveData['sql:updatesCount']=	'updatesCount+1';
		}

		return $saveData;
	}

	/**
	 * Split the input array into 'raw' data and 'processed' data
	 * @param array $fields
	 * @return array
	 */
	protected function splitExtraDataTableFields(Array $fields)
	{
		$dataKeys = ['transactionReferences',
			'transactionParseLog',
			'transactionProcessLog',
			'transactionDocData',
			'transactionSupplierData',
			'transactionPriceData',
			'transactionPassengerData',
			'transactionSegmentData',
			'transactionExtraData',
			'transactionRemarksData',
			'transactionBookingAgentData',
			'transactionIssueAgentData',
			'extraData',
		];

		$dataFields = [];
		foreach ($fields as $k=>$v) {
			if(in_array($k, $dataKeys)) {
				$dataFields[$k] = $v;
				unset($fields[$k]);
			}
		}

		return [$fields, $dataFields];
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\GeneralAbstract::save_executeInsert()
	 */
	protected function save_executeInsert(Array $fields)
	{
		// split fields before sending them to the DB
		list($fields, $dataFields) = $this->splitExtraDataTableFields($fields);

		// call the base parent
		parent::save_executeInsert($fields);

		// do the insert into the data-table
		$dataFields['id'] = $this->id;
		$this->save_executeSql_insert($this->dataTableName, $dataFields);
	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Entity\GeneralAbstract::save_executeUpdate()
	 */
	protected function save_executeUpdate(Array $fields)
	{
		// split fields before sending them to the DB
		list($fields, $dataFields) = $this->splitExtraDataTableFields($fields);

		// call the base parent
		parent::save_executeUpdate($fields);

		// do the insert into the data-table
		$dataFields['id'] = $this->id;
		$this->save_executeSql_update($this->dataTableName, $dataFields, 'id', $this->id);

	}

}
