<?php
namespace App\Amadeus\StructureParser\Types;

class Price extends \App\Amadeus\StructureParser\Types\CompressedSerializableObject
{
	public $total = 			null;
	public $totalVAT = 			null;
	public $totalCurrency = 	null;

	/**
	 * Internal components
	 *
	 * @var \App\Amadeus\StructureParser\Types\PriceComponents
	 */
	public $components = null;

	public $supplier = 			null;	// total suplier price
	public $supplierVAT = 		null;
	public $supplierCurrency = 	null;

	/**
	 * Service taxes
	 *
	 * @var \App\Amadeus\StructureParser\Types\PriceTax
	 */
	public $tax = 				null;

	public $commission = 		null;		// always a value, even if the type was specified as percent
	public $commissionVAT = 	null;
	public $commissionCurrency=	null;
	public $commissionType = 	null;		// value, percent

	public $serviceFee = 		null;
	public $serviceFeeVAT = 	null;
	public $serviceFeeCurrency =null;

	public $penalty = 		 	null;
	public $penaltyVAT = 	 	null;
	public $penaltyCurrency = 	null;

	public $discount = 		 	null;
	public $discountVAT = 		null;
	public $discountCurrency =  null;

	public $bankersRate = 		null;
	public $bankersDate = 		null;
	public $bankersRate_currencyInitial = 	null;
	public $bankersRate_currencyFinal = 	null;
	public $formOfPayment = 	null;

	/**
	 * Price that wasn't distribuited, filled in for distribuited refunds
	 * @var \App\Utils\BaseParser\Structure\StructureParser\Types\Price
	 */
	public $remainingPriceAfterRefundDistribution = 			null;

	/**
	 * Fare calculation code
	 * We don't really know how to interpret this field. It shows the way the prices are calculated, per segment taking into account taxes (XT tax) and various local limits per segment
	 * @see \App\Utils\BaseParser\Utils\FareCalculationExtractor for details
	 * Ex: RUH GF X/BAH GF DXB110.65NUC110.65END ROE3.75027
	 * Ex: BUD EK DXB M/IT EK BUD M/IT END ROE281.384966 XT 5800AE2700F6400TP800ZR94400YQ
	 *
	 * @var string
	 */
	public $fareCalculation = null;

	/**
	 * Fare calculation entry. The command enterd by the agent in order to display the tariff
	 * Ex: FXP
	 *
	 * NOTE: this might get removed in the future, it seems it's not used
	 * @var string
	 */
	public $fareCalculationEntry = null;

	/**
	 * Fare calculation mode indicator
	 *
	 * NOTE: this might get removed in the future, it seems it's not used
	 * @var string
	 */
	public $fareCalculationModeIndicator = null;

	/**
	 * Fare calculation pricing code
	 * Ex: I
	 *
	 * NOTE: this might get removed in the future, it seems it's not used
	 * @var string
	 */
	public $fareCalculationPricingCode = null;

	/**
	 * Endorsements
	 * 	@see https://servicehub.amadeus.com/c/portal/view-solution/875231/en_US/working-with-the-endorsement-fe-element-cryptic-and-graphic-
	 * 	- The endorsements element (FE) is used to identify any restrictions, airline comments, or rules that may apply to a ticket.
	 * 	- The endorsement element can be:
	 * 		- Automatically created by the fare server
	 * 		- Manually entered by the agent
	 * 	- It consists of free-flow text
	 * Ex: SAR350.00 NONREF
	 * Ex: NON-END/FLEX
	 * @var array
	 */
	public $endorsements = [];

	public $commissionable = 	true;		// this means that the commission is included in the supplier price

	/**
	 * Internal price, before running them though plugins
	 *
	 * @var array
	 */
	public $originalPriceArr = [];


	public function __construct()
	{
		$this->tax = new \App\Amadeus\StructureParser\Types\PriceTax();
		$this->formOfPayment = new \App\Amadeus\StructureParser\Types\FormOfPayment();
		$this->components = new \App\Amadeus\StructureParser\Types\PriceComponents();
	}

	/**
	 * Check if any of the VAT components are set
	 * @return boolean
	 */
	public function hasVAT()
	{
		return ($this->totalVAT || $this->supplierVAT || $this->tax->getVAT() || $this->commissionVAT || $this->serviceFeeVAT || $this->discountVAT || $this->penaltyVAT);
	}

	/**
	 * Check if the price has any discount specified
	 * @return boolean
	 */
	public function hasDiscount()
	{
		return ($this->discount || $this->discountVAT);
	}


	/**
	 * Add the values & currencies from another object
	 * @param \App\Amadeus\StructureParser\Types\Price $price
	 */
	public function add(\App\Amadeus\StructureParser\Types\Price $price)
	{
		if($price->total){
			if($this->totalCurrency && $this->totalCurrency!=$price->totalCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The totalCurrency has different values. Please investigate");
			}
			$this->total += 		$price->total;
			$this->totalCurrency = 	$price->totalCurrency;
		}

		if($price->supplier){
			if($this->supplierCurrency && $this->supplierCurrency!=$price->supplierCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The supplierCurrency has different values. Please investigate");
			}
			$this->supplier += 			$price->supplier;
			$this->supplierCurrency = 	$price->supplierCurrency;
		}

		if($price->tax->getCurrency()){
			if($this->tax->getCurrency() && $this->tax->getCurrency()!=$price->tax->getCurrency()){
				throw new \App\Amadeus\StructureParser\StructureException("The taxCurrency has different values. Please investigate");
			}
			$this->tax->setAmount($this->tax->getAmount() + $price->tax->getAmount());
			$this->tax->setCurrency($price->tax->getCurrency());

			foreach($price->tax->components as $comp){
				$this->tax->components[] = $comp;
			}
		}

		if($price->serviceFee){
			if($this->serviceFeeCurrency && $this->serviceFeeCurrency!=$price->serviceFeeCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The serviceFeeCurrency has different values. Please investigate");
			}
			$this->serviceFee += 			$price->serviceFee;
			$this->serviceFeeCurrency = 	$price->serviceFeeCurrency;
		}

		if($price->penalty){
			if($this->penaltyCurrency && $this->penaltyCurrency!=$price->penaltyCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The penalty has different values. Please investigate");
			}
			$this->penalty += 				$price->penalty;
			$this->penaltyCurrency = 		$price->penaltyCurrency;
		}

		if($price->discount){
			if($this->discountCurrency && $this->discountCurrency!=$price->discountCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The discountCurrency has different values. Please investigate");
			}
			$this->discount += 			$price->discount;
			$this->discountCurrency = 	$price->discountCurrency;
		}

		if($price->commission){
			if($this->commissionCurrency && $this->commissionCurrency!=$price->commissionCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The commission has different values. Please investigate");
			}
			$this->commission += 			$price->commission;
			$this->commissionCurrency = 	$price->commissionCurrency;
		}

		return $this;
	}

	/**
	 * Substract the values & currencies from another object
	 * @param \App\Amadeus\StructureParser\Types\Price $price
	 */
	public function sub(\App\Amadeus\StructureParser\Types\Price $price)
	{
		if($price->total){
			if($this->totalCurrency && $this->totalCurrency!=$price->totalCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The totalCurrency has different values. Please investigate");
			}
			$this->total -= 		$price->total;
			$this->totalCurrency = 	$price->totalCurrency;
		}

		if($price->supplier){
			if($this->supplierCurrency && $this->supplierCurrency!=$price->supplierCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The supplierCurrency has different values. Please investigate");
			}
			$this->supplier -= 			$price->supplier;
			$this->supplierCurrency = 	$price->supplierCurrency;
		}

		if($price->tax->getCurrency()){
			if($this->tax->getCurrency() && $this->tax->getCurrency()!=$price->tax->getCurrency()){
				throw new \App\Amadeus\StructureParser\StructureException("The taxCurrency has different values. Please investigate");
			}
			$this->tax->setAmount($this->tax->getAmount() - $price->tax->getAmount());
			$this->tax->setCurrency($price->tax->getCurrency());

			foreach($price->tax->components as $comp){
				$comp->amount = -$comp->amount;
				$this->tax->components[] = $comp;
			}
		}

		if($price->serviceFee){
			if($this->serviceFeeCurrency && $this->serviceFeeCurrency!=$price->serviceFeeCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The serviceFeeCurrency has different values. Please investigate");
			}
			$this->serviceFee -= 			$price->serviceFee;
			$this->serviceFeeCurrency = 	$price->serviceFeeCurrency;
		}

		if($price->penalty){
			if($this->penaltyCurrency && $this->penaltyCurrency!=$price->penaltyCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The penalty has different values. Please investigate");
			}
			$this->penalty -= 			$price->penalty;
			$this->penaltyCurrency = 		$price->penaltyCurrency;
		}

		if($price->discount){
			if($this->discountCurrency && $this->discountCurrency!=$price->discountCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The discountCurrency has different values. Please investigate");
			}
			$this->discount -= 			$price->discount;
			$this->discountCurrency = 	$price->discountCurrency;
		}

		if($price->commission){
			if($this->commissionCurrency && $this->commissionCurrency!=$price->commissionCurrency){
				throw new \App\Amadeus\StructureParser\StructureException("The commission has different values. Please investigate");
			}
			$this->commission -= 			$price->commission;
			$this->commissionCurrency = 	$price->commissionCurrency;
		}

		return $this;
	}

	/**
	 * Reformat internal values, making sure we dont hit the -0 bug, and that all the values are in the required interval
	 * @return \App\Amadeus\StructureParser\Types\Price
	 */
	public function clampValues()
	{
		if ($this->supplier!==null) {
			$this->supplier = 	round($this->supplier, 6)+0;	// handle the -0 bug
		}
		if ($this->commission!==null) {
			$this->commission = round($this->commission, 6)+0;	// handle the -0 bug
		}
		if ($this->discount!==null) {
			$this->discount = 	round($this->discount, 6)+0;	// handle the -0 bug
		}
		if ($this->serviceFee!==null) {
			$this->serviceFee = round($this->serviceFee, 6)+0;	// handle the -0 bug
		}
		if ($this->penalty!==null) {
			$this->penalty = 	round($this->penalty, 6)+0;		// handle the -0 bug
		}
		if ($this->total!==null) {
			$this->total = 		round($this->total, 6)+0;		// handle the -0 bug
		}

		return $this;
	}

	/**
	 * Convert the input price taking into account the defined bankersRate
	 * @param float $initialAmount
	 * @param string $initialCurrency
	 * @param string $finalCurrency
	 * @throws \App\Amadeus\StructureParser\StructureException
	 * @return float|null
	 */
	public function convert($initialAmount, $initialCurrency, $finalCurrency, ?\App\Amadeus\Storage\Operation $transaction = null)
	{
		$return = null;
		$log = [];

		if ($initialAmount==0) {					// when amount is 0, we can ignore the currency, its conversion will always be 0
			$return = 0;
		}
		elseif ($initialCurrency==$finalCurrency) {	// when currencies are identical, we can ignore the conversion alltogether
			$return = (float)$initialAmount;
		}

		if ($return === null && $this->bankersRate) {
			$log[]= sprintf("BankersRate is defined as 1 %s=%s %s", $this->bankersRate_currencyInitial, $this->bankersRate, $this->bankersRate_currencyFinal);

			$rate = null;
			if ($initialCurrency==$this->bankersRate_currencyInitial) {
				$rate = $this->bankersRate;
			}
			elseif ($initialCurrency==$this->bankersRate_currencyFinal) {
				$rate = 1/$this->bankersRate;
			}
			else{
				$log[]= sprintf("Cannot convert %s %s to %s using bankersRate", $initialAmount, $initialCurrency, $finalCurrency);
			}

			if ($rate!==null) {
				$return = $rate * $initialAmount;
			}
		}

		// use the internal TINA conversion routines
		if ($return === null) {
			$log[]= sprintf("Using TINA's currency convertor to convert %s %s to %s at %s, because no valid conversion was defined in the AIR file", $initialAmount, $initialCurrency, $finalCurrency, $this->bankersDate);

			if (\App\Config\Currencies::getInstance()->check($initialCurrency) && \App\Config\Currencies::getInstance()->check($finalCurrency)) {
				// this will throw an exception if $initialCurrency or $finalCurrency is invalid
				$return = \App\CurrencyExchange\Rates::getInstance($this->bankersDate)->convert($initialAmount, $initialCurrency, $finalCurrency, null);
				if ($return==0) {
					$log[]= sprintf("Cannot convert %s %s to %s. Please check if there is a exchange rate defined for this currency & date", $initialAmount, $initialCurrency, $finalCurrency);
					$return = null;
				}
			}
			else {
				$log[]= sprintf("Cannot convert %s %s to %s. Invalid or undefined currency", $initialAmount, $initialCurrency, $finalCurrency);
				$return = null;
			}
		}

		if ($transaction) {
			foreach ($log as $l) {
				$transaction->addLog(sprintf("[%s] %s", __METHOD__, $l));
			}
		}

		// throw an exception when we cannot calculate the total using any of the above methods
		if ($return === null) {
			// \Log::getInstance()->critical(__METHOD__. ": Cannot convert {$initialAmount} {$initialCurrency} to {$finalCurrency}. BankersRate is defined as 1 {$this->bankersRate_currencyInitial}={$this->bankersRate} {$this->bankersRate_currencyFinal}. I also tried using TINA's internal convertor", [
			// 	'stacktrace' => new \Engine\Logger\Context\Auto(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)),
			// 	'this' => 		new \Engine\Logger\Context\Auto(var_export($this, true)),
			// ]);

			throw new \App\Utils\BaseParser\Structure\StructureParser\Types\Exception\Exception("Cannot convert {$initialAmount} {$initialCurrency} to {$finalCurrency}.\n" . implode(".\n", $log));
		}

		return \App\Amadeus\StructureParser\Helpers::round($return, 2);
	}

	/**
	 * Get a list of all defined components
	 * @return Array
	 */
	public function getDefinedComponentsList()
	{
		// return array_intersect($this->getDefinedAmountsList(), $this->getDefinedCurrenciesList());
		return array_unique(array_merge($this->getDefinedAmountsList(), $this->getDefinedCurrenciesList()));
	}

	/**
	 * Get a list of components with defined amounts
	 * @return Array
	 */
	public function getDefinedAmountsList()
	{
		$list = [];
		$list[] = ($this->supplier?'supplier': null);
		$list[] = ($this->tax->getAmount()?'tax': null);
		$list[] = ($this->commission?'commission': null);
		$list[] = ($this->serviceFee?'serviceFee': null);
		$list[] = ($this->penalty?'penalty': null);
		$list[] = ($this->discount?'discount': null);

		return array_values(array_filter($list));
	}

	/**
	 * Get a list of components with defined currencies
	 * @return Array
	 */
	public function getDefinedCurrenciesList()
	{
		$list = [];
		$list[] = ($this->supplierCurrency?'supplier': null);
		$list[] = ($this->tax->getCurrency()?'tax': null);
		$list[] = ($this->commissionCurrency?'commission': null);
		$list[] = ($this->serviceFeeCurrency?'serviceFee': null);
		$list[] = ($this->penaltyCurrency?'penalty': null);
		$list[] = ($this->discountCurrency?'discount': null);

		return array_values(array_filter($list));
	}

	/**
	 * Check the price integrity
	 * @throws \App\Amadeus\StructureParser\StructureException
	 */
	public function checkIntegrity(\App\Amadeus\Storage\Operation $transaction, $performedChecks = [])
	{
		if ($this->total!==null) {
			$this->clampValues();
			$currency = $this->totalCurrency;

			$OK = true;
			if($currency){
				if($this->supplierCurrency && 	$this->supplierCurrency!=$currency) 	$OK = false;
				if($this->tax->getCurrency() && $this->tax->getCurrency()!=$currency) 	$OK = false;
				if($this->commissionCurrency && $this->commissionCurrency!=$currency) 	$OK = false;
				if($this->penaltyCurrency && 	$this->penaltyCurrency!=$currency) 		$OK = false;

				if($OK){
					// expect $this->discount to be negative in here! thats why i'm adding it.
					// expect $this->penalty to be negative in here! thats why i'm adding it.
					$total = $this->supplier + $this->tax->getAmount() + $this->commission + $this->discount + $this->penalty +
						$this->convert($this->serviceFee, $this->serviceFeeCurrency, $this->totalCurrency, $transaction);
					if(round($total, 2)!=round($this->total, 2)){
						$transaction->addLog("Invalid total price (sum={$total}, expected={$this->total}). Please investigate");
					}
				}
			}

			if(!$performedChecks['negativeTotal'] && $this->total<0){
				$transaction->addLog(sprintf("Total price is negative(%s). The defined amounts list: %s. Please investigate", $this->total, implode(", ", $this->getDefinedAmountsList())));
			}
			if($performedChecks['negativeTotal'] && $this->total>0){
				$transaction->addLog(sprintf("Total price is positive(%s). The defined amounts list: %s. Please investigate", $this->total, implode(", ", $this->getDefinedAmountsList())));
			}

			if(!$performedChecks['negativeSupplier'] && $this->supplier<0){
				$transaction->addLog("Supplier price is negative({$this->supplier}). Please investigate");
			}
			if($performedChecks['negativeSupplier'] && $this->supplier>0){
				$transaction->addLog("Supplier price is positive({$this->supplier}). Please investigate");
			}
		}
	}

	/**
	 * Get a deep clone of this object
	 */
	public function getClone()
	{
		$clone = unserialize(serialize($this));

		return $clone;
	}

	/**
	 * Calculate the total amount
	 * @return number|NULL
	 */
	public function calculateTotal()
	{
		// compute the total
		try {
			$total = $this->supplier + ($this->tax?$this->tax->getAmount():0) + $this->commission + $this->discount + $this->penalty +
				$this->convert($this->serviceFee, $this->serviceFeeCurrency, $this->totalCurrency, null);
		}
		catch (\App\Amadeus\StructureParser\Types\PriceException $ex) {
			$total = null;
		}

		return $total;
	}

	public function calculateTotalVAT()
	{
		// compute the total
		try {
			$totalVAT = $this->supplierVAT + ($this->tax?$this->tax->getVAT():0) + $this->commissionVAT + $this->discountVAT + $this->penaltyVAT +
				$this->convert($this->serviceFeeVAT, $this->serviceFeeCurrency, $this->totalCurrency, null);
		}
		catch (\App\Amadeus\StructureParser\Types\PriceException $ex) {
			$totalVAT = null;
		}

		return $totalVAT;
	}

	/**
	 * Insert a new component
	 * @param \App\Amadeus\StructureParser\Types\PriceComponent $tax
	 */
	public function componentAdd(\App\Amadeus\StructureParser\Types\PriceComponent $priceComponent)
	{
		$acceptedMembers = [
			'supplier', 'supplierVAT', 'supplierCurrency',
			'commission', 'commissionVAT', 'commissionCurrency',
			'serviceFee', 'serviceFeeVAT', 'serviceFeeCurrency',
			'penalty', 'penaltyVAT', 'penaltyCurrency',
			'discount', 'discountVAT', 'discountCurrency',
			'total', 'totalVAT', 'totalCurrency',
		];

		// check if the component is valid
		$member = $priceComponent->category;
		if (!in_array($member, $acceptedMembers)) {
			throw new \App\Amadeus\StructureParser\StructureException("Invalid price component category: {$member}");
		}
		$this->{$member}+= $priceComponent->amount;

		// load the VAT
		$member = "{$priceComponent->category}VAT";
		$this->{$member}+= $priceComponent->VAT;

		// load the currency
		$member = "{$priceComponent->category}Currency";
		if ($this->{$member} && $this->{$member}!=$priceComponent->currency) {
			throw new \App\Amadeus\StructureParser\StructureException("Invalid price component currency: {$member}, existing currency: {$this->{$member}}, new currency: {$priceComponent->currency}");
		}
		$this->{$member} = $priceComponent->currency;

		// store this component internally
		$this->components->append($priceComponent);
		return $this;
	}

	/**
	 * Set the value to one component, remove all others
	 * @param \App\Amadeus\StructureParser\Types\PriceComponent $tax
	 */
	public function componentSet(\App\Amadeus\StructureParser\Types\PriceComponent $priceComponent)
	{
		$acceptedMembers = [
			'supplier', 'supplierVAT', 'supplierCurrency',
			'commission', 'commissionVAT', 'commissionCurrency',
			'serviceFee', 'serviceFeeVAT', 'serviceFeeCurrency',
			'penalty', 'penaltyVAT', 'penaltyCurrency',
			'discount', 'discountVAT', 'discountCurrency',
			'total', 'totalVAT', 'totalCurrency',
		];

		// check if the component is valid
		$member = $priceComponent->category;
		if (!in_array($member, $acceptedMembers)) {
			throw new \App\Amadeus\StructureParser\StructureException("Invalid price component category: {$member}");
		}
		$this->{$member} = $priceComponent->amount;

		// load the VAT
		$member = "{$priceComponent->category}VAT";
		$this->{$member} = $priceComponent->VAT;

		// load the currency
		$member = "{$priceComponent->category}Currency";
		$this->{$member} = $priceComponent->currency;

		// store this component internally, but first remove all siblings
		$this->components = $this->components->getComponentsFiltered(function($comp) use ($priceComponent) {
			return $comp->category!=$priceComponent->category;
		});
		$this->components->append($priceComponent);

		return $this;

	}

}
