<?php
namespace App\Utils\BaseParser\Utils;

/**
 * Helper class that will rebuild the refund as seen in GDS by looking up the data in IO
 * @author Lucian Sirbu
 */
class RebuildImportedOrderRefund
{
	use \App\Utils\BaseParser\SingletonTrait;

	public function gatherServiceWithReissues(\Entity\ImportedService $importedService)
	{
		/*
		 * The order in TINA has to contain the TICK & REIS operations
		 * When a RFND arrives, both TICK & REIS are marked as refunded, and some prices are distribuited
		 * In order to re-compose the original refund, we need to go over all refunded services in the order, and try to make connections between them, in order to be able to rebuild the TICK & REIS and RFND operations
		 * Ex:
		 *  TICK 1
		 *  TICK 2
		 *  TICK 3
		 *  REIS 4 (->2) (base TICK:2)
		 *  REIS 5 (->3) (base TICK:3)
		 *  REIS 6 (->4) (base TICK:2)
		 *  REIS 7 (->6) (base TICK:2)
		 *  RFND   (->1) (base TICK:1)
		 *  RFND   (->7) (base TICK:2)
		 *  RFND   (->5) (base TICK:3)
		 *
		 * If we group these, in this case we have 3 TICK's:
		 * 	TICK 1 = TICK + RFND
		 * 	TICK 2 = TICK + REIS 4 + REIS 6 + REIS 7 + RFND
		 * 	TICK 3 = TICK + REIS + RFND
		 *
		 * Please note:
		 * 	- RFND operations are directly attached to the service in IO, in order to re-compose the original RFND, we need to collect the whole reservation chain
		 * 	- REIS operations can be in different orders,
		 * 		NOTE: the current code doesn't handle this case, as I haven't fount any testcases
		 * In order to rebuild the whole chain we need to lookup in the order the current service->idSystem in all the other services, looking at the field idReissuedService
		 * */

		// in the benchmark, caching this will result in a 2x speedup of the whole function call, but we need to be careful of what & when we cache data. The count(files) below is used to separate cached services that are updated in the same test (it happened in uTests, but it should be reproducible in real-life)
		$rebuiltOrderServicesListCacheKey = sprintf("%s:%s:%s", __METHOD__, $importedService->importedOrder->getId(), count($importedService->importedOrder->files));
		$rebuiltOrderServicesList = \Engine\Cache\General::getInstance('localMemoryLinear')->get($rebuiltOrderServicesListCacheKey);
		if (!$rebuiltOrderServicesList) {
			// In order to buld the list, we need to first extract all services in a separate array extended with prev/next attributes, in order to simulate a linked-list
			$rebuiltOrderServicesList = array_map(function($srv) use ($importedService) {
				$IOService = \Entity\ImportedService::getDetailsByServiceExtSys($srv->idSystem, $importedService->idSystem, false);

				return [
					'prev' => 			null,
					'next' => 			null,

					'refunded' => 		null,

					'idService' =>     $IOService['id'],

					'idSystem' => 		$srv->idSystem,
					'idReissuedService'=>$srv->idReissuedService,
					'srv' => 			$srv,
				];
			}, array_filter($importedService->importedOrder->object->services, function($srv){
				// filter-out some services
				$accept = true;

				// voided services
				if ($accept && $srv->operation=='cancel') {
					$accept = false;
				}

				// deleted IMR's
				if ($accept && $srv->operation=='delete') {
					$accept = false;
				}

				// IMR's
				if ($accept && !$srv->chosenOffer) {
					$accept = false;
				}

				// active IMR's
				if ($accept && !$srv->chosenOffer->ticketNumbers) {
					$accept = false;
				}

				return $accept;
			}));

			// TODO: this will probably fail for reissues that point to another IO order alltogether, please see the above note
			// TODO: optimization: avoid using so many array_map/filter calls, they can be slow

			// build a list and fill in all the prev/next pseudo-attributes in the whole order
			foreach ($rebuiltOrderServicesList as $k => $srv) {
				////////////////////////////////////////////////////////////////////////////////
				// pick elements that point to this service
				$relatedServices = array_map(function($s) {
					return $s['idSystem'];
				}, array_filter($rebuiltOrderServicesList, function($s) use ($srv) {
					return $srv['idSystem'] === $s['idReissuedService'];
				}));

				if (count($relatedServices)>1) {
					/**
					 * this should not happen, but I guess the best we can do is to pick the last service
					 * It did happen on Zahid's server, ticket 5339836711, file: 20180410 171836 01 01.FluX.AIRJH25708.AIR
					 * 	@see uTests/tests/App/Bsp/Settlement/Listing/teste legaturi parinti/2020-11-xx TD-6830 post rewrite RFND handler, SA,Zahid . RSH3IL/10.RebuildImportedOrderRefund.class.php
					 */
					$relatedServices = [ end($relatedServices), ];
					// throw new \Exception(sprintf("Found more than 1 reissues pointing to this service. This should not happen. Service: %s, services pointing to it: %s. Encountered while processing service: %s", $srv['idSystem'], implode(", ", $relatedServices), $importedService->idServiceExtSys));
				}

				$srv['next'] = reset($relatedServices);	// pick the first element. There should be only one anyway at this point

				////////////////////////////////////////////////////////////////////////////////
				// pick elements that this service points to
				$srv['prev'] = $srv['idReissuedService'];	// this is conveniently provided by IO

				$srv['refunded'] = ($srv['srv']->refundDetails != null);

				// put the data back in the original array
				$rebuiltOrderServicesList[$k] = $srv;
			}

			\Engine\Cache\General::getInstance('localMemoryLinear')->set($rebuiltOrderServicesListCacheKey, $rebuiltOrderServicesList);
		}

		// the list of services related to the requested one. This should inclide the original TICK and all the subsequent REIS operations
		$rebuiltRelatedServicesList = [];

		// search forward
		$searchedServiceId = $importedService->idServiceExtSys;
		$newService = [];
		do {
			$newService = array_filter($rebuiltOrderServicesList, function($srv) use ($searchedServiceId) {
				// if the price distribution didn't work for the RFND operation in the GDS, then we should ignore the 'refunded' flag
				return $srv['idSystem']==$searchedServiceId;
			});
			$searchedServiceId = null;
			if ($newService) {
				$newService = reset($newService);
				$searchedServiceId = $newService['next'];	// go to the next service
				$rebuiltRelatedServicesList[$newService['idSystem']]= $newService;
			}
		} while($newService);

		// search backward
		$searchedServiceId = $importedService->idServiceExtSys;
		$newService = [];
		do {
			$newService = array_filter($rebuiltOrderServicesList, function($srv) use ($searchedServiceId) {
				// if the price distribution didn't work for the RFND operation in the GDS, then we should ignore the 'refunded' flag
				return $srv['idSystem']==$searchedServiceId;
			});
			$searchedServiceId = null;
			if ($newService) {
				$newService = reset($newService);
				$searchedServiceId = $newService['prev'];	// go to the previous service
				$rebuiltRelatedServicesList[$newService['idSystem']]= $newService;
			}
		} while($newService);

		if (!$rebuiltRelatedServicesList) {
			return [
				$importedService->idSystem => [
					'prev' => 			null,
					'next' => 			null,

					'refunded' => 		null,

					'idService' =>     $importedService->getId(),

					'idSystem' => 		$importedService->idSystem,
					'idReissuedService'=>null,
					'srv' => 			$importedService->object,
				],
			];
		}

		return $rebuiltRelatedServicesList;
	}

	/**
	 * Build the total prices components, by adding together all the price components from a list of services
	 * @return Array
	 */
	public function rebuildChosenOffer($rebuiltRelatedServicesList, $handleOnlyRefunds)
	{
		$chosenOffer = [
			'servicePriceTotal' => 	null,
			'totalCurrency' => 		null,
			'commissionTotal' => 	null,
			'commissionCurrency' => null,
			'taxTotal' => 			null,
			'taxCurrency' => 		null,
		];

		// chosenOffer->servicePriceTotal
		$chosenOffer['servicePriceTotal'] = array_sum(array_map(function($srv) use ($handleOnlyRefunds) {
			if ($handleOnlyRefunds && !$srv['refunded']) {
				return 0.00;
			}
			return ($srv['srv']->chosenOffer->commissionable?$srv['srv']->chosenOffer->servicePriceTotal:$srv['srv']->chosenOffer->servicePriceTotal+$srv['srv']->chosenOffer->commissionTotal);
		}, $rebuiltRelatedServicesList));

		$chosenOffer['totalCurrency'] = reset(array_unique(array_map(function($srv) use ($handleOnlyRefunds) {
			return $srv['srv']->chosenOffer->totalCurrency;
		}, $rebuiltRelatedServicesList)));

		$cfgCommissionHandling = (string)\App\Bsp\Config::getInstance()->get("commissionHandling");

		// chosenOffer->commissionTotal
		$chosenOffer['commissionTotal'] = array_sum(array_map(function($srv) use ($handleOnlyRefunds, $cfgCommissionHandling) {
			if ($handleOnlyRefunds && !$srv['refunded']) {
				return 0.00;
			}
			$offer = $srv['srv']->chosenOffer;
			if ($offer->commissionable) {
				return 0.00;
			}

			if ($cfgCommissionHandling=='useInitialCommission') {
				$offerExtraInfo = (array)$offer->extraInfo;
				$originalCommission = ($offerExtraInfo['originalPriceArr']['commission']??$offer->commissionTotal);
				return $originalCommission;
			}
			else {
				return $offer->commissionTotal;
			}
		}, $rebuiltRelatedServicesList));

		$chosenOffer['commissionTotal_skipped'] = array_sum(array_map(function($srv) use ($handleOnlyRefunds, $cfgCommissionHandling) {
			if ($handleOnlyRefunds && !$srv['refunded']) {
				return 0.00;
			}
			// same as above, but the commissionable check negated, in order to obtain the differences only
			$offer = $srv['srv']->chosenOffer;
			if (!$offer->commissionable) {
				return 0.00;
			}

			if ($cfgCommissionHandling=='useInitialCommission') {
				$offerExtraInfo = (array)$offer->extraInfo;
				$originalCommission = ($offerExtraInfo['originalPriceArr']['commission']??$offer->commissionTotal);
				return $originalCommission;
			}
			else {
				return $offer->commissionTotal;
			}
		}, $rebuiltRelatedServicesList));

		$chosenOffer['commissionCurrency'] = reset(array_unique(array_map(function($srv) use ($handleOnlyRefunds) {
			return $srv['srv']->chosenOffer->commissionCurrency;
		}, $rebuiltRelatedServicesList)));

		// chosenOffer->taxTotal
		$chosenOffer['taxTotal'] = array_sum(array_map(function($srv) use ($handleOnlyRefunds) {
			if ($handleOnlyRefunds && !$srv['refunded']) {
				return 0.00;
			}
			return $srv['srv']->chosenOffer->taxTotal;
		}, $rebuiltRelatedServicesList));

		$chosenOffer['taxCurrency'] = reset(array_unique(array_map(function($srv) use ($handleOnlyRefunds) {
			return $srv['srv']->chosenOffer->taxCurrency;
		}, $rebuiltRelatedServicesList)));

		return $chosenOffer;
	}


	public function rebuildOriginalRefund(\Entity\ImportedService $importedService)
	{
		$rebuiltRelatedServicesList = $this->gatherServiceWithReissues($importedService);

		$cfgCommissionHandling = (string)\App\Bsp\Config::getInstance()->get("commissionHandling");

		$rebuiltPrices = [
			'chosenOffer' => [
				'servicePriceTotal' => 	null,
				'totalCurrency' => 		null,
				'commissionTotal' => 	null,
				'commissionCurrency' => null,
				'taxTotal' => 			null,
				'taxCurrency' => 		null,
			],

			'refundFareUsed' => [
				'fareUsedSupplierValue' => 	null,
				'fareUsedCommissionValue' =>null,
				'fareUsedTaxValue' => 		null,
			],

			'refund' => [
				'servicePriceTotal' => 	null,
				'totalCurrency' => 		null,
				'commissionTotal' => 	null,
				'commissionCurrency' => null,
				'taxTotal' => 			null,
				'taxCurrency' => 		null,
			],
		];

		// rebuild the price components
		$rebuiltPrices['chosenOffer'] = $this->rebuildChosenOffer($rebuiltRelatedServicesList, true);

		// refundDetails->fareUsedSupplierValue
		$rebuiltPrices['refundFareUsed']['fareUsedSupplierValue'] = array_sum(array_map(function($srv) {
			return $srv['srv']->refundDetails->fareUsedSupplierValue??null;
		}, $rebuiltRelatedServicesList));

		// refundDetails->fareUsedCommissionValue
		if ($cfgCommissionHandling=='useInitialCommission') {
			$rebuiltPrices['refundFareUsed']['fareUsedCommissionValue'] = array_sum(array_map(function($srv) {
				$refundDetails = $srv['srv']->refundDetails;
				$originalCommission = null;
				if ($refundDetails) {
					$refundExtraInfo = (array)$refundDetails->extraInfo;
					$originalCommission = ($refundExtraInfo['originalPriceArr']['fareUsedCommissionValue']??$refundDetails->fareUsedCommissionValue);
					return $originalCommission;
				}
			}, $rebuiltRelatedServicesList));
		}
		else {
			$rebuiltPrices['refundFareUsed']['fareUsedCommissionValue'] = array_sum(array_map(function($srv) {
				return $srv['srv']->refundDetails->fareUsedCommissionValue??null;
			}, $rebuiltRelatedServicesList));
		}

		// refundDetails->fareUsedTaxValue
		$rebuiltPrices['refundFareUsed']['fareUsedTaxValue'] = array_sum(array_map(function($srv) {
			return $srv['srv']->refundDetails->fareUsedTaxValue??null;
		}, $rebuiltRelatedServicesList));

		// refundDetails->penaltyValue
		$rebuiltPrices['refundFareUsed']['penaltyValue'] = array_sum(array_map(function($srv) {
			return $srv['srv']->refundDetails->penaltyValue??null;
		}, $rebuiltRelatedServicesList));


		// calculate the actual refund data
		$rebuiltPrices['refund']['servicePriceTotal'] =   $rebuiltPrices['chosenOffer']['servicePriceTotal'] - $rebuiltPrices['refundFareUsed']['fareUsedSupplierValue']
														+ $rebuiltPrices['chosenOffer']['commissionTotal'] -   $rebuiltPrices['refundFareUsed']['fareUsedCommissionValue'];
		$rebuiltPrices['refund']['totalCurrency'] = 	  $rebuiltPrices['chosenOffer']['totalCurrency'];

		$rebuiltPrices['refund']['commissionTotal'] = 	  $rebuiltPrices['chosenOffer']['commissionTotal'] + $rebuiltPrices['chosenOffer']['commissionTotal_skipped'] - $rebuiltPrices['refundFareUsed']['fareUsedCommissionValue'];
		$rebuiltPrices['refund']['commissionCurrency'] =  $rebuiltPrices['chosenOffer']['commissionCurrency'];

		$rebuiltPrices['refund']['taxTotal'] = 			  $rebuiltPrices['chosenOffer']['taxTotal'] - $rebuiltPrices['refundFareUsed']['fareUsedTaxValue']
														- $rebuiltPrices['refundFareUsed']['penaltyValue'];
		$rebuiltPrices['refund']['taxCurrency'] =		  $rebuiltPrices['chosenOffer']['taxCurrency'];

		// pepare the rest of the fields to be filled-in
		$lastService = reset(array_filter($rebuiltRelatedServicesList, function($srv) {
			return !$srv['next'];
		}));

		if ($lastService['idSystem']==$importedService->idServiceExtSys) {
			// fill-in the final fields in the return response
			$rebuiltRefund = [
				'idSystem' => 		$lastService['srv']->idSystem,
				'ticketNumber' => 	$lastService['srv']->chosenOffer->confirmationNumber,
				'formOfPayment'	=> 	implode(",", array_unique(array_filter(array_map(function($srv) {
					return $srv['srv']->refundDetails->formOfPayment??null;
				}, $rebuiltRelatedServicesList)))),

				// this affects the call to createKeyFromImportedServiceForRefund() in PopulateBspSettlementRecords cronjob
				'date' => 			$lastService['srv']->chosenOffer->date,
				// 'date' => 			$lastService['srv']->refundDetails->date,

				'rebuiltPrices' => 				$rebuiltPrices,
				'rebuiltRelatedServicesIdSystemList' => array_keys($rebuiltRelatedServicesList),
				// 'rebuiltRelatedServicesList' => $rebuiltRelatedServicesList,	// might be useful for debugging
			];
		}
		else {
			// fill-in the final fields in the return response
			$rebuiltRefund = [
				'idSystem' => 		$lastService['srv']->idSystem,
				'ticketNumber' => 	$lastService['srv']->chosenOffer->confirmationNumber,
				'formOfPayment'	=> 	implode(",", array_unique(array_filter(array_map(function($srv) {
					return $srv['srv']->refundDetails->formOfPayment??null;
				}, $rebuiltRelatedServicesList)))),

				// this affects the call to createKeyFromImportedServiceForRefund() in PopulateBspSettlementRecords cronjob
				'date' => 			$lastService['srv']->chosenOffer->date,
				// 'date' => 			$lastService['srv']->refundDetails->date,

				'rebuiltPrices' => 				[
					'refund' => [
						'servicePriceTotal' => 	0.00,
						'totalCurrency' => 		0.00,
						'commissionTotal' => 	0.00,
						'commissionCurrency' => 0.00,
						'taxTotal' => 			0.00,
						'taxCurrency' => 		0.00,
					],
				],
				'rebuiltRelatedServicesIdSystemList' => array_keys($rebuiltRelatedServicesList),
				// 'rebuiltRelatedServicesList' => $rebuiltRelatedServicesList,	// might be useful for debugging
			];
		}

		return $rebuiltRefund;
	}
}