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

abstract class ProcessorAbstract
{
	/**
	 * Local object cache, used to avoid duplicating various entities
	 * @var array
	 */
	protected $localObjectCache = [];

	/**
	 * Get the finalized converted order
	 * @throws \Exception
	 * @return \App\ImportedOrder\Order
	 */
	abstract public function getConvertedOrder();


	/**
	 * Generate the TINA service id
	 * @param \App\Entity\GDSServiceAbstract $service
	 * @return string
	 */
	abstract protected function generateTINAServiceIdSystem(\App\Entity\GDSServiceAbstract $service);


	/**
	 * Create unique cache key
	 * @param String $key
	 * @param array $references
	 * @return String
	 */
	protected function createCacheKey($key, Array $references)
	{
		return sprintf("%s-%s", $key, implode("-", $references));
	}

	protected function getCacheSimilarKeys($keys)
	{
		$object = null;
		foreach ($keys as $k) {
			if (array_key_exists($k, $this->localObjectCache)) {
				$object = $this->localObjectCache[$k];
				break;
			}
		}

		return $object;
	}

	protected function setCacheSimilarKeys($keys, $object)
	{
		foreach ($keys as $k) {
			$this->localObjectCache[$k] = $object;
		}
	}

	protected function cloneServiceOnOrder(\App\ImportedOrder\Service $sourceService, \App\ImportedOrder\Order $order)
	{
		$destinationService = clone $sourceService;

/*		$destinationService->operation = 		$sourceService->operation;
		$destinationService->idSystem = 		$sourceService->idSystem;
		$destinationService->systemStatus = 	$sourceService->systemStatus;

		// add passenger to service and order
		$passengers = (array)$sourceService->passengers;

		foreach ($passengers as $passenger) {
			// add passenger to service
			if (!$destinationService->passengerExists($passenger)) {
				$destinationService->addPassenger($passenger);
			}
			// add passenger to order
			if (!$order->passengerExists($passenger)) {
				$order->addPassenger($passenger);
			}
		}

		// set chosen offer
		$chosenOffer = $sourceService->chosenOffer;
		if ($chosenOffer) {
			$destinationService->chosenOffer = $chosenOffer;
			$destinationService->chosenOfferConfirmed = true;
		}

		// get product type
		$sourceProductTypes = (array)$sourceService->productTypes;
		foreach ($sourceProductTypes as $sourceProductType) {
			$destinationService->addProductType($sourceProductType->code, $sourceProductType->name);
		}

		// set departure and destination object
		$sourceDeparturePlace = $sourceService->departurePlace;
		$destinationService->departurePlace = $sourceDeparturePlace;

		$sourceDestinationPlace = $sourceService->destinationPlace;
		$destinationService->destinationPlace = $sourceDestinationPlace;

		$sourceDepartureTime = $sourceService->departureDateTime;
		$destinationService->departureDateTime = $sourceDepartureTime;

		$sourceDestinationTime = $sourceService->destinationDateTime;
		$destinationService->destinationDateTime = $sourceDestinationTime;

		// set service type
		$sourceServiceType = $sourceService->serviceType;
		$destinationService->serviceType = $sourceServiceType;
*/
#		debug($sourceService);

#		debug($destinationService);
#		printf("%s: %s", __FILE__, __LINE__); die();

		$order->addService($destinationService);

		return $destinationService;

	}


	/**
	 * Determine if agencyCommission is true/false depending if the credit card enlisted in the gds is set in the AgencyCreditCards entity
	 * @param array $creditCardList
	 * @return boolean
	 */
	protected function determineIgnoreSupplierOnDocumentsValueForCC(Array $creditCardList)
	{
		foreach ($creditCardList as $CCRowDetails) {
			if ($CCRowDetails->ccNumber) {
				$matcher = new \App\Utils\CreditCard\MatchCardFromGDS($CCRowDetails->ccNumber, ($CCRowDetails->ccExpiryDate?$CCRowDetails->ccExpiryDate:null)); // the parser wrongly assumes the expiry date on 01 of the month, in reality it's the last day of the month
				if ($matcher->matchByAgencyCreditCards()) {
					return false;
				}
			}
		}

		return true;
	}


	/**
	 * Check if a service already exists in importedOrders
	 */
	protected function isServiceAlreadyImportedInImportedServices(\App\Entity\GDSServiceAbstract $entity)
	{
		$IOService = \Entity\ImportedService::getDetailsByServiceExtSys(
			$this->generateTINAServiceIdSystem($entity),
			\Entity\ExtResSystem::getSystemIdByCode($entity::systemCode),
			false,
			'-2 years'
		);
		if ($IOService && $IOService['systemStatus']!='deleted') {
			return true;
		}
		return false;
	}

	/**
	 * Generate TINA fareUsed equavalent for fareUsedTaxCodes
	 *
	 * @param array $refundTaxComponents
	 * @param array $issuetaxComponents
	 * @return array
	 */
	protected function generateTinaSericeRefundDetailsFareUsedTaxes(array $refundTaxComponents, array $issuetaxComponents)
	{
		// Step 1: In order to fill in fareUsedTaxCode, we need to first group taxes by code, both in the issue and in the refund service

		// Step 1.1: grouped taxes in the initial issue transaction
		$groupedTaxesInEntRefunded = [];
		foreach ($issuetaxComponents as $comp) {
			if (!$groupedTaxesInEntRefunded[$comp->code]) {
				$groupedTaxesInEntRefunded[$comp->code] = [
					'amount' => 	$comp->amount,
					'VAT' => 		$comp->VAT,
					'currency' => 	$comp->currency,
					'code' => 		$comp->code,
				];
			}
			else {
				if ($comp->currency != $groupedTaxesInEntRefunded[$comp->code]['currency']) {
					throw new \App\Utils\BaseParser\Structure\CompatibilityWithImportedOrders\InvalidStructure(sprintf("Trying to group tax code %s but it has a different currencies (%s vs %s)", $comp->code, $comp->currency, $groupedTaxesInEntRefunded[$comp->code]['currency']));
				}

				$groupedTaxesInEntRefunded[$comp->code]['amount']+= $comp->amount;
				$groupedTaxesInEntRefunded[$comp->code]['VAT']+= 	$comp->VAT;
			}
		}

		// Step 1.2: grouped taxes in the initial issue transaction
		$groupedTaxesInEnt = [];
		foreach ($refundTaxComponents as $comp) {
			if (!$groupedTaxesInEnt[$comp->code]) {
				$groupedTaxesInEnt[$comp->code] = [
					'amount' => 	$comp->amount,
					'VAT' => 		$comp->VAT,
					'currency' => 	$comp->currency,
					'code' => 		$comp->code,
				];
			}
			else {
				if ($comp->currency != $groupedTaxesInEnt[$comp->code]['currency']) {
					throw new \App\Utils\BaseParser\Structure\CompatibilityWithImportedOrders\InvalidStructure(sprintf("Trying to group tax code %s but it has a different currencies (%s vs %s)", $comp->code, $comp->currency, $groupedTaxesInEnt[$comp->code]['currency']));
				}

				$groupedTaxesInEnt[$comp->code]['amount']+= $comp->amount;
				$groupedTaxesInEnt[$comp->code]['VAT']+= 	$comp->VAT;
			}
		}


		// Step 2: now calculate the difference between both arrays
		$fareUsedGroupedTaxes = [];
		foreach ($groupedTaxesInEntRefunded as $compArr) {
			$fareUsedGroupedTaxes[$compArr['code']] = [];
			$fareUsedGroupedTaxes[$compArr['code']]['currency'] = 	$compArr['currency'];
			$fareUsedGroupedTaxes[$compArr['code']]['code'] = 		$compArr['code'];
			$fareUsedGroupedTaxes[$compArr['code']]['amount'] = 	(
				$compArr['amount']!==null || $groupedTaxesInEnt[$compArr['code']]['amount']!==null
				?($compArr['amount'] + $groupedTaxesInEnt[$compArr['code']]['amount'])
				:null
			);
			$fareUsedGroupedTaxes[$compArr['code']]['VAT'] = 		(
				 ($compArr['VAT']!==null || $groupedTaxesInEnt[$compArr['code']]['VAT']!==null)
				?($compArr['VAT'] + $groupedTaxesInEnt[$compArr['code']]['VAT'])
				:null
			);
		}

		// Step 2.1: add back remaining taxes in $ent
		foreach ($groupedTaxesInEnt as $compArr) {
			if (!$fareUsedGroupedTaxes[$compArr['code']]) {
				$fareUsedGroupedTaxes[$compArr['code']] = [];
				$fareUsedGroupedTaxes[$compArr['code']]['currency'] = 	$compArr['currency'];
				$fareUsedGroupedTaxes[$compArr['code']]['code'] = 		$compArr['code'];
				$fareUsedGroupedTaxes[$compArr['code']]['amount'] = 	$compArr['amount'];
				$fareUsedGroupedTaxes[$compArr['code']]['VAT'] = 		$compArr['VAT'];
			}
		}

		return $fareUsedGroupedTaxes;
	}

	/**
	 * Fill in the credit card details
	 *
	 * @param \App\ImportedOrder\Offer $offer
	 * @param \App\Utils\BaseParser\Structure\StructureParser\Types\Price $priceData
	 * @param string $onEmptyCCAmounts What to do when CC amounts are empty: 'ignore' or 'loadTotal'
	 * @param string $ccPickingAlgorithm Algorithm for picking CC: 'first' or 'largest'
	 * @param string $offerPaymentAlgorithm Algorithm for offer payment: 'sum' or 'ignore'
	 * @param string $offerPaymentAlgorithmDefault Default behavior when offer payment algorithm cannot be applied
	 * @return array
	 */
	protected function generateOfferCreditCardDetails(\App\ImportedOrder\Offer $offer, \App\Utils\BaseParser\Structure\StructureParser\Types\Price $priceData, string $ccPickingAlgorithm = 'largest', string $offerPaymentAlgorithm = 'sum', string $offerPaymentAlgorithmDefault = 'ignore'): array
	{
		$extraRemarks = [];
		$creditCard = new \App\ImportedOrder\CreditCard();
		$fopCCs = $priceData->formOfPayment->getPaymentByTypes(['cc', 'CC']);
		if ($ccPickingAlgorithm=='largest') {
			$clientCCs = [];
			$agencyCCs = [];
			// first, separate agency & client CC's
			foreach ($fopCCs as $CC) {
				/* @var $CC \App\Utils\BaseParser\Structure\StructureParser\Types\PriceFormOfPaymentComponent */
				if ($CC->ccNumber) {
					$matcher = new \App\Utils\CreditCard\MatchCardFromGDS($CC->ccNumber, ($CC->ccExpiryDate?$CC->ccExpiryDate:null));
					if ($matcher->matchByAgencyCreditCards()) {
						$agencyCCs[]= $CC;
					}
					else {
						$clientCCs[]= $CC;
					}
				}
				else {
					$clientCCs[]= $CC;
				}
			}

			/**
			 * CC picking algorithm (A = agency, C = client):
			 * 	if A  use first CC, amount = first(CC)
			 * 	if C  use first CC, amount = first(CC)
			 * 	if AC use client CC, amount = max(client CC)
			 * 	if AA use max(agency CC), amount = max(agency CC)
			 * 	if CC use max(client CC), amount = sum(CC)
			 */

			if ($clientCCs) {
				$pickedCCs = $clientCCs;
			}
			else {	// if no client CCs, take the max agency CC
				$CCAmounts = [];
				$CCCurrencies = [];
				$CCLargestPay = null;
				/* @var $CCLargestPay \App\Utils\BaseParser\Structure\StructureParser\Types\PriceFormOfPaymentComponent */
				foreach ($agencyCCs as $CC) {
					/* @var $CC \App\Utils\BaseParser\Structure\StructureParser\Types\PriceFormOfPaymentComponent */
					if ($CC->amount > ($CCAmounts?max($CCAmounts):0)) {
						$CCLargestPay = $CC;
					}

					$CCAmounts[]= $CC->amount;
					$CCCurrencies[]= $CC->currency;
				}

				$pickedCCs = [$CCLargestPay, ];	// use only the largest CC, ignore the others, because the algorithm for AA is different
			}

			// filter & sum as needed according to the above algorithm
			$CCAmounts = [];
			$CCCurrencies = [];
			$CCLargestPay = null;
			foreach ($pickedCCs as $CC) {
				/* @var $CC \App\Utils\BaseParser\Structure\StructureParser\Types\PriceFormOfPaymentComponent */
				if (!$CCAmounts || ($CC->amount > max($CCAmounts))) {
					$CCLargestPay = $CC;
				}

				$CCAmounts[]= $CC->amount;
				$CCCurrencies[]= $CC->currency;
			}

			// use $extraRemarks to store remarks that should get appended once the service starts being populated. At this moment it's mostly empty
			if (count($pickedCCs)>1) {
				$extraRemarks[]= sprintf("MultipleCards:%s", implode("/", array_map(function(\App\Sabre\StructureParser\Types\PriceFormOfPaymentComponent $CC) {
					return sprintf("%s%s", $CC->amount, $CC->currency);
				}, $pickedCCs)));
			}

			if ($offerPaymentAlgorithm=='sum') {
				if ($CCAmounts && count(array_unique($CCCurrencies))==1) {
					$offer->creditCardPaymentValue = 	array_sum($CCAmounts);
					$offer->creditCardPaymentCurrency =	$CCCurrencies[0];
				}
			}
			elseif (in_array($offerPaymentAlgorithm, ['first', 'largest'])) {
				if ($CCLargestPay) {
					$offer->creditCardPaymentValue = 	$CCLargestPay->amount;
					$offer->creditCardPaymentCurrency =	$CCLargestPay->currency;
				}
			}
			else {
				throw new \Exception("Unknown offer payment algorithm: " . $offerPaymentAlgorithm);
			}

			if (!$offer->creditCardPaymentValue && !$offer->creditCardPaymentCurrency) {
				if ($offerPaymentAlgorithmDefault=='ignore') {
					// do nothing
				}
				elseif ($offerPaymentAlgorithmDefault=='loadTotal') {
					if ($priceData->total && $priceData->totalCurrency) {
						$offer->creditCardPaymentValue 		= $priceData->total;
						$offer->creditCardPaymentCurrency 	= $priceData->totalCurrency;
					}
				}
			}

			$CC = $CCLargestPay;
		}
		elseif ($ccPickingAlgorithm=='first') {
			$CC = $fopCCs[0];
			if ($offerPaymentAlgorithm=='first') {
				if ($CC->amount && $CC->currency) {
					$offer->creditCardPaymentValue = 	$CC->amount;
					$offer->creditCardPaymentCurrency =	$CC->currency;
				}
			}
			else {
				throw new \Exception("Unknown offer payment algorithm: " . $offerPaymentAlgorithm);
			}

			if ($offerPaymentAlgorithmDefault=='loadTotal' && !$offer->creditCardPaymentValue && !$offer->creditCardPaymentCurrency) {
				if ($priceData->total && $priceData->totalCurrency) {
					$offer->creditCardPaymentValue 		= $priceData->total;
					$offer->creditCardPaymentCurrency 	= $priceData->totalCurrency;
				}
			}
		}
		else {
			throw new \Exception("Unknown CC picking algorithm: " . $ccPickingAlgorithm);
		}

		/* @var $CC \App\Utils\BaseParser\Structure\StructureParser\Types\PriceFormOfPaymentComponent */
		$creditCard->company = 		$CC->ccCompanyIndicator;
		$creditCard->expireDate = 	$CC->ccExpiryDate;
		$creditCard->number = 		$CC->ccNumber;
		$creditCard->approvalCode = $CC->ccApprovalCode;
		$offer->creditCard = $creditCard;

		if ($creditCard->number && $creditCard->expireDate) {
			$creditCard = $this->matchCreditCardAndStoreReference($creditCard, ['agency']);
		}

		return [$offer, $extraRemarks];
	}

	/**
	 * Match the credit card with specified entity types
	 * @param \App\ImportedOrder\CreditCard $creditCard The credit card to match
	 * @param array $matchTypes Array of entity types to match against (e.g., ['agency'])
	 * @return \App\ImportedOrder\CreditCard
	 */
	protected function matchCreditCardAndStoreReference(\App\ImportedOrder\CreditCard $creditCard, array $matchTypes=['agency'])
	{
		if (in_array('agency', $matchTypes)) {
			// match the credit card with the agency credit cards and store the match from this step, as \App\ImportedOrder\CreditCard::getMatchedCardEntityDetails() dosen't use the GDS-specific MatchCardFromGDS call
			$matcher = new \App\Utils\CreditCard\MatchCardFromGDS($creditCard->number, $creditCard->expireDate);
			$matchedAgencyCreditCard = $matcher->matchByAgencyCreditCards();
			if ($matchedAgencyCreditCard) {
				$creditCard->entityType = 'agency';
				$creditCard->entityId = $matchedAgencyCreditCard;
			}
		}
		return $creditCard;
	}

	protected function getIOServiceEntity(string $IOServiceId, \App\Entity\GDSServiceAbstract $entity)
	{
		$IOService = \Entity\ImportedService::getDetailsByServiceExtSys(
			$IOServiceId,
			\Entity\ExtResSystem::getSystemIdByCode($entity::systemCode),
			false,
			'-2 years'
		);
		if ($IOService && $IOService['systemStatus']!='deleted') {
			return \Entity\ImportedService::select($IOService['id']);
		}
		return null;
	}

	/**
	 * For reissued tickets, without any price exchange, copy the form of payment from the original ticket to ensure consistent payment information in IO
	 * @param \App\Entity\GDSServiceAbstract $ent
	 * @param \App\ImportedOrder\Offer $offer
	 * @param null|\Entity\ImportedService $reissuedIOService
	 * @return \App\ImportedOrder\Offer
	 */
	protected function specialHandlingForFormOfPaymentForReissues(\App\Entity\GDSServiceAbstract $ent, \App\ImportedOrder\Offer $offer, ?\Entity\ImportedService $reissuedIOService=null)
	{
		if ($ent->transactionStatus=='reissue' && $offer->servicePriceTotal==0 && $offer->taxTotal==0 && !$offer->formOfPayment) {
			// Get the original service entity
			$reissuedIOServiceUsed = ($reissuedIOService?:$this->getIOServiceEntity($this->generateTINAServiceIdSystem($ent->reissuedService->nonConjunction), $ent->reissuedService->nonConjunction));
			if ($reissuedIOServiceUsed->object && $reissuedIOServiceUsed->object->chosenOffer && $reissuedIOServiceUsed->object->chosenOffer->formOfPayment) {
				$reissuedOffer = $reissuedIOServiceUsed->object->chosenOffer;
				// Copy the form of payment from the original ticket
				$offer->formOfPayment = $reissuedOffer->formOfPayment;

				// If the original had a credit card, copy that too
				if ($reissuedOffer->creditCard) {
					$offer->creditCard = 				clone $reissuedOffer->creditCard;
				}
			}
		}

		return $offer;
	}

	/**
	 * Load ticket numbers from documents->docNumber & documents->conjunctionDocNumbers
	 * @param \App\Entity\GDSServiceAbstract $ent
	 * @param \App\ImportedOrder\Service $serv
	 * @param \App\ImportedOrder\Offer $offer
	 * @return \App\ImportedOrder\Offer
	 */
	protected function loadTicketNumbersDefault(\App\Entity\GDSServiceAbstract $ent, \App\ImportedOrder\Service $serv, \App\ImportedOrder\Offer $offer)
	{
		$paxs = (array)$serv->passengers;

		if ($paxs) {
			$conjunctions = [];
			if ($ent->transactionData->documents->conjunctionDocNumbers) {
				foreach ($ent->transactionData->documents->conjunctionDocNumbers as $doc) {
					if ($doc!=$ent->docNumber) {
						$conjunctions[] = $doc;
					}
				}
			}

			$offer->addTicket($paxs[0], $ent->transactionData->documents->docNumber, $conjunctions);
		}

		return $offer;
	}

	/**
	 * Load ticket numbers from remarks
	 * @param \App\Entity\GDSServiceAbstract $ent
	 * @param \App\ImportedOrder\Service $serv
	 * @param \App\ImportedOrder\Offer $offer
	 * @return \App\ImportedOrder\Offer
	 */
	protected function loadTicketNumbersFromRemarks(\App\Entity\GDSServiceAbstract $ent, \App\ImportedOrder\Service $serv, \App\ImportedOrder\Offer $offer)
	{
		$processedRemarks = $ent->transactionData->remarks->getProcessedRemarks();

		if ($processedRemarks['TICKET_NUMBER']) {
			$offer->confirmationNumber = $processedRemarks['TICKET_NUMBER'];
			$offer->ticketDate = $ent->transactionData->documents->issueDate;

			$paxs = $serv->passengers;
			if ($paxs) {
				$offer->addTicket($paxs[0], $processedRemarks['TICKET_NUMBER'], []);
			}
		}

		return $offer;
	}

}
