<?php

// TODO: atributul idTina (de la serv) - ar trebui sa o facem mai tarziu; sa nu mearga operatie de 'insert'

namespace App\ImportedOrder;

/**
 * The class contains info about a requested service attached on the imported order.
 * @author Gabriel
 */
class Service extends EntityAbstract
{

	/**
	 * The id of the service - the main key to be used when referring to services.
	 * @var String
	 */
	public $idSystem;

	/**
	 * The service operation - can be 'insert', 'update', 'delete', 'cancel', 'refund'.
	 * @var String
	 */
	public $operation;

	/**
	 * The TINA service id - filled if the current imported service should be processed onto the
	 * one mentioned in this field. This is ususally filled in when the reservation in the imported
	 * service is initiated from TINA, and the external system stores it until exports the imported
	 * service.
	 * @var Int
	 */
	public $idServiceTinaInitial;

	/**
	 * An auxiliary reference (foreign key) to other tables in the databse.
	 * @var String
	 */
	public $auxReference;

	/**
	 * The original status in the external system.
	 * @var String
	 */
	public $systemStatus;

	/**
	 * Tries to force the current operation (non strict applying) (eg. 'insert' can be applied even if it exists but not cancelled).
	 * @var Boolean
	 */
	public $forceOperation;

	/**
	 * This indicates the linked parent service (if any).
	 * @var \App\ImportedOrder\Service
	 */
	public ?\App\ImportedOrder\Service $linkedToService = null;

	/**
	 * Indicates if the current service is a normal service, a package or a subservice of a package.
	 * @var String - [null]/subservice/package
	 */
	public $packageItem;

	/**
	 * This indicates the package service (parent reference), when the current service is a subservice.
	 * @var \App\ImportedOrder\Service
	 */
	public ?\App\ImportedOrder\Service $packageParentService = null;

	/**
	 * Indicates the way the offer of package is determined (if this is a package).
	 * @var String - [noValue]/independent/reference
	 */
	public $packageOfferMode;

	/**
	 * The service booker user.
	 * @var \App\ImportedOrder\User
	 */
	public ?\App\ImportedOrder\User $bookUser = null;

	/**
	 * The service book office.
	 * @var \App\ImportedOrder\Office
	 */
	public ?\App\ImportedOrder\Office $bookOffice = null;

	/**
	 * The service issue user.
	 * @var \App\ImportedOrder\User
	 */
	public ?\App\ImportedOrder\User $issueUser = null;

	/**
	 * The service issue office.
	 * @var \App\ImportedOrder\Office
	 */
	public ?\App\ImportedOrder\Office $issueOffice = null;

	/**
	 * The service responsible user.
	 * @var \App\ImportedOrder\User
	 */
	public ?\App\ImportedOrder\User $respUser = null;

	/**
	 * The service responsible office.
	 * @var \App\ImportedOrder\Office
	 */
	public ?\App\ImportedOrder\Office $respOffice = null;

	/**
	 * The list of passengers associated to this service
	 * @var \App\ImportedOrder\Passenger[]
	 */
	public ?array $passengers = null;

	/**
	 * The service type
	 * @var \App\ImportedOrder\ServiceType
	 */
	public ?\App\ImportedOrder\ServiceType $serviceType = null;

	/**
	 * The list of categories attached to the current service (associative array of code=>value).
	 * @var ?array
	 */
	public ?array $categories = null;

	/**
	 * The list of product types attached to the current service (associative array of code=>value).
	 * @var ?array
	 */
	public ?array $productTypes = null;

	/**
	 * The list of service source attributes attached to the current service (associative array of code=>value).
	 * @var ?array
	 */
	public ?array $serviceSources = null;

	/**
	 * The list of supplier products attached to the current service (associative array of code=>value).
	 * @var \App\ImportedOrder\SupplierProduct[]
	 */
	public ?array $supplierProducts = null;

	/**
	 * The multipliers attached to the current service - can be a numeric value or an associative array
	 * of code=>array(count,name).
	 * @var Mixed
	 */
	public float|array|null $multipliers = null;

	/**
	 * The service description.
	 * @var String
	 */
	public $description;

	/**
	 * The service preferences.
	 * @var String
	 */
	public $preferences;

	/**
	 * The service comments.
	 * @var String
	 */
	public $comments;

	/**
	 * The service remarks - they are later parsed when they are imported and processed.
	 * @var String
	 */
	public $remarks;

	/**
	 * The departure place
	 * @var \App\ImportedOrder\Place
	 */
	public ?\App\ImportedOrder\Place $departurePlace = null;

	/**
	 * The destination place
	 * @var \App\ImportedOrder\Place
	 */
	public ?\App\ImportedOrder\Place $destinationPlace = null;

	/**
	 * The departure date and time.
	 * @var \App\ImportedOrder\DateTime
	 */
	public ?\App\ImportedOrder\DateTime $departureDateTime = null;

	/**
	 * The destination date and time.
	 * @var \App\ImportedOrder\DateTime
	 */
	public ?\App\ImportedOrder\DateTime $destinationDateTime = null;

	/**
	 * The agency cost center.
	 * @var String
	 */
	public $agencyCostCenter;

	/**
	 * Estimated invoice date
	 * @var String
	 */
	public $estInvoiceDate;

	/**
	 * Estimated cash date.
	 * @var String
	 */
	public $estCashDate;

	/**
	 * The chosen offer for this service.
	 * @var \App\ImportedOrder\Offer
	 */
	public ?\App\ImportedOrder\Offer $chosenOffer = null;

	/**
	 * Indicates if the offer is confirmed (service status is OF vs OK).
	 * @var Boolean
	 */
	public $chosenOfferConfirmed;

	/**
	 * The details of the current refund operation
	 * @var \App\ImportedOrder\RefundDetails
	 */
	public ?\App\ImportedOrder\RefundDetails $refundDetails = null;

	/**
	 * The id of the original service that this service reissues
	 * The id is expected to point to an idServiceExtSys in the importedOrders_services table
	 * @var String
	 */
	public $idReissuedService;

	/**
	 * Details about the service cancelling.
	 * @var \App\ImportedOrder\CancelDetails
	 */
	public ?\App\ImportedOrder\CancelDetails $cancelDetails = null;

	/**
	 * A list of extra fields stored as an associative array.
	 * @var ?array
	 */
	public ?array $extraInfo = null;


	/**
	 * Returns a list of internal class attributes that can be copied when merging orders.
	 * @return Array
	 */
	protected function getMergingFields() :array
	{
		return [
			'idSystem', 'operation', 'systemStatus', 'forceOperation', 'auxReference', 'idServiceTinaInitial', 'linkedToService',
			'packageItem', 'packageParentService', 'packageOfferMode',
			'bookUser', 'bookOffice', 'issueUser', 'issueOffice', 'respUser', 'respOffice',
			'serviceType', 'categories', 'productTypes', 'serviceSources', 'multipliers',
			'description', 'preferences', 'comments', 'remarks', 'agencyCostCenter', 'estInvoiceDate', 'estCashDate',
			'departurePlace', 'destinationPlace', 'departureDateTime', 'destinationDateTime',
			'chosenOfferConfirmed', 'refundDetails', 'cancelDetails', 'extraInfo',
		]; // we do NOT include 'chosenOffer' as it would overwrite all its fields; I manually merge the offer fields
	}


	/**
	 * Adds a new passenger on the current service.
	 * @param Passenger $passenger
	 * @throws \App\MessagesException
	 */
	public function addPassenger(Passenger $passenger)
	{
		if ($this->passengerExists($passenger)) {
			throw new \Exception(sprintf('There is a passenger which was added twice on a service.'));
		}
		if (!$this->passengers) {
			$this->passengers = [];
		}
		$this->passengers[] = $passenger;
	}


	/**
	 * Indicates if a passenger object already exists on the current service.
	 * @param Passenger $passenger
	 * @return boolean
	 */
	public function passengerExists(Passenger $passenger) :bool
	{
		if (!$this->passengers) {
			return false;
		}
		foreach($this->passengers as $psgObj) {
			if ($passenger===$psgObj) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns the passenger object matched by the system id.
	 * @param String $idSystem
	 * @return \App\ImportedOrder\Passenger - or null if the passenger does not exist
	 */
	public function getPassengerByIdSystem($idSystem)
	{
	    // search through all passengers - if a psg is found by the system id, it returns the object
	    if ($this->passengers and $idSystem) {
	        foreach($this->passengers as $psgObj) {
	            if ((String)$psgObj->idSystem===(String)$idSystem) {
	                return $psgObj;
	            }
	        }
	    }
	    // if this point reached - no psg was found
	    return null;
	}

	/**
	 * Removes a passenger from the list.
	 * @param Passenger $passenger
	 * @return boolean
	 */
	public function removePassenger(Passenger $passenger) :bool
	{
		if (!$this->passengers) {
			return false;
		}
		foreach($this->passengers as $key=>$psgObj) {
			if ($passenger===$psgObj) {
				unset($this->passengers[$key]);
				return true;
			}
		}
		return false;
	}

	/**
	 * Adds a new category to this service.
	 * @param String $code
	 * @param String $name
	 */
	public function addCategory($code, $name)
	{
		if (!$this->categories) {
			$this->categories = [];
		}
		$this->categories[$code] = $name;
	}

	/**
	 * Returns the list of categories and their corresponding match (if any).
	 * @param String $serviceType - the service type (from the external system)
	 * @param String $categoryCode - the category code (from the external system)
	 * @param Int @idExternalSystem - the external system id (from TINA)
	 */
	public function getMatchedCategory($serviceType, $categoryCode, $idExternalSystem)
	{
		if (!strlen($serviceType) or !strlen($categoryCode)) {
			return null;
		}
		$matchedItem = \App\Config\ImportedOrders_Matches::getInstance()->getServiceCategory($idExternalSystem, $serviceType, $categoryCode);
		if (strlen((string)$matchedItem)) {
			return $matchedItem;
		}
		return null;
	}

	/**
	 * Adds a new product type on this service.
	 * @param String $code
	 * @param String $name
	 */
	public function addProductType($code, $name)
	{
		if (!$this->productTypes) {
			$this->productTypes = [];
		}
		$this->productTypes[$code] = $name;
	}

	/**
	 * Returns the list of productTypes and their corresponding match (if any).
	 */
	public function getMatchedProductType($serviceType, $productTypeCode, $idExternalSystem)
	{
		if (!strlen($serviceType) or !strlen($productTypeCode)) {
			return null;
		}
		$matchedItem = \App\Config\ImportedOrders_Matches::getInstance()->getServiceProductType($idExternalSystem, $serviceType, $productTypeCode);
		if (strlen((string)$matchedItem)) {
			return $matchedItem;
		}
		return null;
	}


	/**
	 * Adds a new service source on this service.
	 * @param String $code
	 * @param String $name
	 */
	public function addServiceSource($code, $name)
	{
		if (!$this->serviceSources) {
			$this->serviceSources = [];
		}
		$this->serviceSources[$code] = $name;
	}


	/**
	 * Returns the list of service source attributes and their corresponding match (if any).
	 */
	public function getMatchedServiceSource($serviceType, $serviceSourceCode, $idExternalSystem)
	{
		if (!strlen($serviceType) or !strlen($serviceSourceCode)) {
			return null;
		}
		$matchedItem = \App\Config\ImportedOrders_Matches::getInstance()->getServiceSource($idExternalSystem, $serviceType, $serviceSourceCode);
		if (strlen((string)$matchedItem)) {
			return $matchedItem;
		}
		return null;
	}



	/**
	 * Adds a new supplier product to this service.
	 * @param SupplierProduct $sp
	 * @throws \Exception
	 */
	public function addSupplierProduct(SupplierProduct $sp)
	{
		if ($this->supplierProductExists($sp)) {
			throw new \Exception('There is a supplier product that was associated twice with a service.');
		}
		if (!$this->supplierProducts) {
			$this->supplierProducts = [];
		}
		$this->supplierProducts[] = $sp;
	}

	/**
	 * Checks if a certain supplier product object is already associated to this service.
	 * @param SupplierProduct $sp
	 * @return boolean
	 */
	public function supplierProductExists(SupplierProduct $sp) :bool
	{
		if (!$this->supplierProducts) {
			return false;
		}
		foreach($this->supplierProducts as $spObj) {
			if ($sp===$spObj) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Removes a supplier product from the list.
	 * @param SupplierProduct $sp
	 * @return boolean - true if the supplier product was found and removed
	 */
	public function removeSupplierProduct(SupplierProduct $sp) :bool
	{
		if (!$this->supplierProducts) {
			return false;
		}
		foreach($this->supplierProducts as $key=>$spObj) {
			if ($sp===$spObj) {
				unset($this->supplierProducts[$key]);
				return true;
			}
		}
		return false;
	}


	/**
	 * Add a multiplier item
	 * @param int $count
	 * @param string $code
	 * @param string $name
	 */
	public function addMultiplierItem(float $count, ?string $code, ?string $name)
	{
		// reset the multipliers as array, just to make sure that we erase previously stored data
		if (!is_array($this->multipliers)) {
			$this->multipliers = [];
		}
		$this->multipliers[(string)$code] = [ 'count'=>$count, 'name'=>(string)$name ];
	}

	/**
	 * Gets the matched service multiplier based on multiplier code
	 * @param string $serviceType
	 * @param string $multiplierCode
	 * @param int $idExternalSystem
	 * @return ?string
	 */
	public function getMatchedMultiplier(string $serviceType, string $multiplierCode, ?int $idExternalSystem) :?string
	{
		if (!strlen($serviceType) or !strlen($multiplierCode)) {
			return null;
		}
		$matchedItem = \App\Config\ImportedOrders_Matches::getInstance()->getServiceMultiplier($idExternalSystem, $serviceType, $multiplierCode);
		if (strlen((string)$matchedItem)) {
			return $matchedItem;
		}
		return null;
	}

	/**
	 * Parses the service remarks and returns an associative array with details.
	 * @return array
	 */
	public function getProcessedRemarks(): array
	{
		return \App\Config\Remarks::process($this->remarks, 'service');
	}


	/**
	 * (non-PHPdoc)
	 * @see \App\ImportedOrder\EntityAbstract::validateEntityFields()
	 */
	protected function validateEntityFields(\App\MessagesCollector $messagesCollector) :void
	{

		// check id system
		if (!$this->idSystem) {
			$messagesCollector->addError('The service id from the external system is not set.');
		} elseif (mb_strlen($this->idSystem)>100) {
			$messagesCollector->addError('The service id from the external system cannot have more than 100 chars.');
		}

		// check operation
		if (!in_array($this->operation, ['insert', 'update', 'cancel', 'refund', 'delete'])) {
			$messagesCollector->addError('The operation is not set or valid - '.$this->operation);
		}

		// check tina service id
		if ($this->idServiceTinaInitial and (!is_numeric($this->idServiceTinaInitial) or $this->idServiceTinaInitial<0)) {
			$messagesCollector->addInfo('The TINA service id `%s` is invalid.'.$this->idServiceTinaInitial);
		}

		// check package item
		if ($this->packageItem and $this->packageItem!='subservice' and $this->packageItem!='package') {
			$messagesCollector->addError('The package item field is invalid - it can only be set as `subservice` or `package`.');
		}

		// check package type
		if ($this->packageOfferMode and $this->packageOfferMode!='independent' and $this->packageOfferMode!='reference') {
			$messagesCollector->addError('The package offer type field is invalid - it can only be set as `independent` or `reference`.');
		}

		// chech the paclkage offer item and the offer type
		if ($this->packageOfferMode and $this->packageItem!='package') {
			$messagesCollector->addError('The package offer type field can only be set for the (parent) package service.');
		}
		// check if the package offer type is set
		if ($this->packageItem=='package' and !$this->packageOfferMode) {
			$messagesCollector->addError('The package offer type is mandatory for the package service.');
		}
		// package parent service ref. can only be set for subservices
		if ($this->packageParentService and $this->packageItem!='subservice') {
			$messagesCollector->addError('The parent package service can only be set for subvservices.');
		}
		// package parent service is not set for a subervice
		if ($this->packageItem=='subservice' and !$this->packageParentService) {
			$messagesCollector->addError('The parent package service was not set for a subservice.');
		}

		// check responsible user and office
		if ($this->respUser) {
			$messagesCollector->appendMessages($this->respUser->getValidatingMessagesCollector(), 'Responsible user: %s');
		}
		if ($this->respOffice) {
			$messagesCollector->appendMessages($this->respOffice->getValidatingMessagesCollector(), 'Responsible office: %s');
		}

		// check passengers
		if ($this->passengers) {
			foreach ($this->passengers as $idx=>$psg) {
				$messagesCollector->appendMessages($psg->getValidatingMessagesCollector(), "Passenger #".($idx+1).": %s");
			}
		}

		// check service type
		if ($this->serviceType) {
			$messagesCollector->appendMessages($this->serviceType->getValidatingMessagesCollector(), "Service type: %s");
		}

		// check departure/destination place
		if ($this->departurePlace) {
			$messagesCollector->appendMessages($this->departurePlace->getValidatingMessagesCollector(), "Departure place: %s");
		}
		if ($this->destinationPlace) {
			$messagesCollector->appendMessages($this->destinationPlace->getValidatingMessagesCollector(), "Destination place: %s");
		}

		// check departure/destination dateTime
		if ($this->departureDateTime) {
			$messagesCollector->appendMessages($this->departureDateTime->getValidatingMessagesCollector(), "Departure date/time: %s");
		}
		if ($this->destinationDateTime) {
			$messagesCollector->appendMessages($this->destinationDateTime->getValidatingMessagesCollector(), "Destination date/time: %s");
		}

		// check multipliers
		if (is_array($this->multipliers) and $this->multipliers) {
			foreach($this->multipliers as $mulCode=>$mulDet) {
				if (!is_numeric($mulDet['count']) or $mulDet['count']<=0) {
					$messagesCollector->addError(sprintf('The multiplier value for code `%s` must be a numeric and positive value.', $mulCode));
				}
			}
		} elseif ($this->multipliers!==null) {
			if (!is_numeric($this->multipliers) or $this->multipliers<0) { // TODO: we should not allow 0 as multiplier but we have a lot of stored imported orders with 0 as multiplier
				$messagesCollector->addError(sprintf('The multiplier value must be a numeric and positive value.'));
			}
		}

		// check tickets for the chosen offer
		$offObj = $this->chosenOffer;
		if ($offObj) {
			$messagesCollector->appendMessages($offObj->getValidatingMessagesCollector(), "Offer: %s");
			if ($offObj->ticketNumbers) {
				foreach ($offObj->ticketNumbers as $tkt) {
					if (!$this->passengerExists($tkt['passenger'])) {
						$messagesCollector->addError('A passenger has a ticket associated on an offer but it is not associated with the service itself.');
					}
				}
			}
		}

		// check 'chosenOfferConfirmed'
		if ($this->chosenOfferConfirmed and !$this->chosenOffer) {
			$messagesCollector->addInfo('The service does not have a chosen offer but the `chosenOfferConfirmed` flag is set to true.');
		}

		// refund details
		if ($this->refundDetails and $this->operation!='refund') {
			$messagesCollector->addInfo('Refund details should be set only for `refund` operations.');
		}
		if (!$this->refundDetails and $this->operation=='refund') {
			$messagesCollector->addWarning('Refund details must be set for `refund` operations.');
		}
		if ($this->refundDetails) {
			$messagesCollector->appendMessages($this->refundDetails->getValidatingMessagesCollector(), "Refund details: %s");
		}

		// check cancel details
		if ($this->cancelDetails and $this->operation!='cancel') {
			$messagesCollector->addInfo('Cancel details should be  set only for `cancel` operation.');
		}
		if (!$this->cancelDetails and $this->operation=='cancel') {
			$messagesCollector->addError('Cancel details must be set for `cancel` operation.');
		}
		if ($this->cancelDetails) {
			$messagesCollector->appendMessages($this->cancelDetails->getValidatingMessagesCollector(), "Cancel details: %s");
		}
	}


	/**
	 * Returns the matched agency cost center.
	 */
	public function getMatchedAgencyCostCenter($idExternalSystem)
	{

		// if no agency cost center - return null
		if (!strlen((string)$this->agencyCostCenter)) {
			return null;
		}

		// gets the agency cc list
		$agCcList = \App\Config\AgencyCostCenters::getInstance()->getListActive();

		// check by the ag. cc code/name
		$agCcUpper = mb_strtoupper($this->agencyCostCenter);
		$matchedCodes = [];
		foreach($agCcList as $code=>$name) {
			if ($agCcUpper==mb_strtoupper($code) or $agCcUpper==mb_strtoupper($name)) {
				$matchedCodes[$code] = true;
			}
		}

		// check matched cost center
		if ($this->agencyCostCenter and $idExternalSystem) {
			$matchedCostCenter = \App\Config\ImportedOrders_Matches::getInstance()->getAgencyCostCenter($idExternalSystem, $this->agencyCostCenter);
			if (strlen((string)$matchedCostCenter)) {
				$matchedCodes[$matchedCostCenter] = true;
			}
		}

		// return the value if we have exactly one record
		if (count($matchedCodes)==1) {
			return key($matchedCodes);
		} else {
			return false;
		}

	}

}
