<?php

namespace Modules\RemoteData\EntityManagementWs\Model\Entity\Service;

class Process extends \Modules\RemoteData\EntityManagementWs\Model\Entity\GeneralAbstract
{

	protected $forcedRequestAttributes = ['importedServicesList', 'idOrder', 'orderDetails', 'flags'];

	/**
	 * Only basic validations are added here; more validationa are applied on process, but they need
	 * to selectForUpdate and we want to optimize this by validating them inside the DB transaction.
	 * {@inheritDoc}
	 * @see \Modules\RemoteData\EntityManagementWs\Model\Entity\GeneralAbstract::processRequest()
	 */
	protected function checkRequestData() :void
	{
		if (!$this->request->importedServicesList) {
			throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException('No imported services provided.', 'NoImportedServices');
		}
		// check order id vs details
		if (!$this->request->idOrder and !$this->request->orderDetails) {
			throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException('You must specify either the `idOrder` or the `orderDetails` fields.', 'NoOrderIdOrDetails');
		}

		if ($this->request->idOrder and $this->request->orderDetails) {
			throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException('You cannot specify both the `idOrder` or the `orderDetails` fields.', 'BothOrderIdAndDetails');
		}

	}

	/**
	 * {@inheritDoc}
	 * @see \Modules\RemoteData\EntityManagementWs\Model\Entity\GeneralAbstract::checkRequestData()
	 */
	protected function processRequest() :void
	{

		try {

			// creates month tables, outside the transaction, in case the service insert is successful
			\Entity\Order::createMonthlyTables();

			\DB::startTransaction();

			$this->processRequest_check();

			$this->processRequest_do();

			\DB::commitTransaction();

		} catch (\App\Entity\Exception $e) {
			\DB::rollbackTransaction();
			throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException($e->getMessagesCollector()->getMessageBulk(), 'TinaEntityError');
		} catch (\Exception $e) {
			\DB::rollbackTransaction();
			throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException($e->getMessage(), 'TinaError');
		}

	}

	/**
	 * Checks that the input is ok.
	 * @throws \Modules\RemoteData\EntityManagementWs\Model\RuntimeException
	 */
	private function processRequest_check()
	{

		// CHECK IMPORTED SERVICES ////////////////////////////////////////////////////////////////

		// check services one by one
		foreach($this->request->importedServicesList as $impServiceKey=>$impServiceDet) {

			// check current imported service based on TINA id
			if ($impServiceDet->idImportedService??null) {
				$impServEnt = \Entity\ImportedService::selectForUpdate($impServiceDet->idImportedService);
				if (!$impServEnt->recordExists()) {
					throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException(sprintf('Invalid imported service id `%s` (key `%s`).', $impServiceDet->idImportedService, $impServiceKey), 'ImportedServiceIdInvalid');
				}
				if ($impServEnt->processStatus!='new') {
					throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException(sprintf('The imported service id `%s` (key `%s`) does not have the correct status - it must be `new` in order to be processed.', $impServiceDet->idImportedService, $impServiceKey), 'ImportedServiceStatusInvalid');
				}
			}
			// check imported service based on external system id
			else {

				// determine the external system
				if ($impServiceDet->idSystem??null) {
					$extSystemEnt = \Entity\ExtResSystem::selectForUpdate($impServiceDet->idSystem);
					if (!$extSystemEnt->recordExists() or $extSystemEnt->deleted) {
						throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException(sprintf('The imported service with external id `%s` (key `%s`) does not have a valid system reference.', $impServiceDet->idServiceExtSys, $impServiceKey), 'ImportedServiceSystemIdInvalid');
					}
					$idSystem = $impServiceDet->idSystem;
				} else {
					if (!$impServiceDet->systemCode) {
						throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException(sprintf('The imported service with external id `%s` (key `%s`) does not have a valid system code.', $impServiceDet->idServiceExtSys, $impServiceKey), 'ImportedServiceSystemCodeMissing');
					} else {
						$idSystem = \Entity\ExtResSystem::getSystemIdByCode($impServiceDet->systemCode);
						if (!$idSystem) {
							throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException(sprintf('The imported service with external id `%s` (key `%s`) does not have a matched system code.', $impServiceDet->idServiceExtSys, $impServiceKey), 'ImportedServiceSystemCodeInvalid');
						}

					}
				}

				// match imported service by id in ext system
				$importedServiceTinaDet = \Entity\ImportedService::getDetailsByServiceExtSys($impServiceDet->idServiceExtSys, $idSystem, true);
				if (!$importedServiceTinaDet) {
					throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException(sprintf('The imported service with external id `%s` (key `%s`) cannot be matched by external system service id.', $impServiceDet->idServiceExtSys, $impServiceKey), 'ImportedServiceInExtSysInvalid');
				}
				$impServiceDet->idImportedService = $importedServiceTinaDet['id'];

			}

		}


		// CHECK ORDER DETAILS ////////////////////////////////////////////////////////////////////

		if ($this->request->idOrder) {
			$orderEnt = \Entity\Order::selectForUpdate($this->request->idOrder);
			if (!$orderEnt->recordExists()) {
				throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException('Invalid order id provided.', 'InvalidOrderId');
			}
		}

	}


	/**
	 * The actual process of the imported document.
	 */
	private function processRequest_do() :void
	{

		$servicesList = [];

		// EXISTING ORDER /////////////////////////////////////////////////////////////////////////
		if ($this->request->idOrder) {

			$orderEnt = \Entity\Order::selectForUpdate($this->request->idOrder);
			$orderCustomers = $orderEnt->idCustomersList;
			$orderUndefinedPax = $orderEnt->undefinedPax;

			// GO THROUGH EACH IMPORTED SERVICE AND ADD IT TO THE ORDER ///////////////////////////
			foreach($this->request->importedServicesList as $impServiceKey=>$impServiceDet) {

				$serviceInsertEnt = \Entity\Service::insert();

				$serviceInsertEnt->idReservation = $this->request->idOrder;
				$this->populateServiceEntity($serviceInsertEnt, $impServiceDet);

				// UPDATE ORDER - add new passengers on it --------------------------------
				$orderCustomers = array_merge($orderCustomers, $serviceInsertEnt->getField('idCustomers'));
				$orderUpdateEnt = \Entity\Order::update($this->request->idOrder);
				$orderUpdateEnt->idCustomers = array_unique($orderCustomers);
				$orderUpdateEnt->undefinedPax = $orderUndefinedPax;
				$orderUpdateEnt->save();

				// save the new service
				$serviceInsertEnt->save();

				$servicesList[$impServiceKey] = $serviceInsertEnt;

			}

			$idOrder = $this->request->idOrder;

		}

		// NEW ORDER //////////////////////////////////////////////////////////////////////////////
		else {

			$orderInsertEnt = \Entity\Order::insert();

			$idCustomersList = $idCorporatesList = [];

			// GO THROUGH EACH IMPORTED SERVICE AND ADD IT TO THE ORDER ///////////////////////////
			foreach($this->request->importedServicesList as $impServiceKey=>$impServiceDet) {

				// add service to the list
				$serviceInsertEnt = \Entity\Service::insert();
				$this->populateServiceEntity($serviceInsertEnt, $impServiceDet);
				$servicesList[$impServiceKey] = $serviceInsertEnt;

				// get customers on this service
				$idCustomersList = array_unique(array_merge($idCustomersList, $serviceInsertEnt->getField('idCustomers')));

				// get corporates of the current order
				$importedServiceEnt = \Entity\ImportedService::selectForUpdate($impServiceDet->idImportedService);
				$importedOrderObj = $importedServiceEnt->importedOrder->object;
				if ($importedOrderObj->ownerCorporate) {
					// match corporate
					$idCorporate = $importedOrderObj->ownerCorporate->getMatchedId($importedServiceEnt->idSystem);
					if ($idCorporate and !in_array($idCorporate, $idCorporatesList)) {
						$idCorporatesList[] = $idCorporate;
					}
				}

			}

			// associate the services on the current order
			$orderInsertEnt->servicesList = $servicesList;

			// attach the corporates and the customers from the services
			$orderInsertEnt->idCorporates = $idCorporatesList;
			$orderInsertEnt->idCustomers = $idCustomersList;

			// populate the order and associate the services
			$this->populateOrderEntity($orderInsertEnt, $servicesList);

			$orderInsertEnt->save();

			// get order id
			$idOrder = $orderInsertEnt->getId();

		}

		// ADD AND CONFIRM ALL OFFERS /////////////////////////////////////////////////////////////
		$this->populateOffersEntity($servicesList);

		// COMPOSE THE RESPONSE -------------------------------------------------------------------
		$this->populateResponse_process($idOrder, $servicesList);

	}


	/**
	 * Populates all service fields.
	 * @param \Entity\Service $serviceInsertEnt
	 * @param \stdClass $importedServiceDet
	 * @throws \Modules\RemoteData\EntityManagementWs\Model\RuntimeException
	 */
	private function populateServiceEntity(\Entity\Service $serviceInsertEnt, \stdClass $importedServiceDet)
	{

		// imported service details
		$idImportedService = $importedServiceDet->idImportedService;
		$importedServiceEnt = \Entity\ImportedService::select($idImportedService);
		$importedServiceObj = $importedServiceEnt->object;
		$importedOfferObj = $importedServiceObj->chosenOffer;
		$idSystem = $importedServiceEnt->idSystem;

		// ADD MAIN SERVICE DETAILS ///////////////////////////////////////////////////////////////
		$populateServiceObj = new \App\ImportedOrder\PopulateEntity\Service($importedServiceObj, $serviceInsertEnt, $idSystem);
		$populateServiceObj->populate();

		// ASSOCIATE PASSENGERS ///////////////////////////////////////////////////////////////////
		$serviceInsertEnt->idCustomers = []; // reset the customers list
		$idCustomersList = [];
		$undefinedPax = [];
		$psgsTicketsList = [];
		foreach ((array)$importedServiceObj->passengers as $importedPassengerObj) {
			$idCustomer = null;
			if (!$importedPassengerObj->isUndefined()) {
				$custMatchObj = $importedPassengerObj->getPopulatedMatchObject($idSystem);
				$custMatchObj->setUseCacheInSqls(false);
				$custMatchObj->search();

				// check for multiple matches
				if ($custMatchObj->getResultsCount()>1 and $this->request->flags and $this->request->flags->stopOnCustomerMultipleMatch) {
					throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException(sprintf('An imported service customer was matched more than once; idCustomers: %s.', implode(', ', array_keys($custMatchObj->getAll()))), 'CustomerMultipleMatches');
				}
				if ($custMatchObj->getResultsCount()==1) {
					$idCustomer = $custMatchObj->getFirstMatchId();
				} else {
					$custInsert = \Entity\Customer::insert();
					$custMatchObj = new \App\ImportedOrder\PopulateEntity\Customer($importedPassengerObj, $custInsert, $idSystem);
					$custMatchObj->populate();
					$custInsert->save();
					$idCustomer = $custInsert->getId();

				}
			} else {
				$undefPsgType = $importedPassengerObj->type->getMatchedId($idSystem);
				if (!$undefPsgType) {
					throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException('Found an undefined passenger without a defined/matched type.', 'UndefinedCustomerWithNoType');
				}
				$undefinedPax[$undefPsgType]++;
			}

			// collect the current customer
			if ($idCustomer) {
				$idCustomersList[] = $idCustomer;
			}

			// collect the psg ticket number
			if ($idCustomer and $importedOfferObj) {
				$currPsgTktNo = $importedOfferObj->getPassengerTicket($importedPassengerObj);
				$psgsTicketsList[$idCustomer] = $currPsgTktNo;
			}
		}

		$serviceInsertEnt->idCustomers = array_unique($idCustomersList);
		$serviceInsertEnt->undefinedPax = $undefinedPax;

		// associate imported service id
		$serviceInsertEnt->idImportedService = $idImportedService;

		// determine passengers tickets list&date
		$serviceCode = $serviceInsertEnt->getField('serviceCode');
		if ($serviceCode) {
			if (\App\Config\Services_OfferFields::getInstance()->manageTicketDetails($serviceCode)) {
				// associate tickets details
				$serviceInsertEnt->passengersTicketsList = $psgsTicketsList;
				if ($importedOfferObj and $importedOfferObj->ticketDate) {
					$serviceInsertEnt->passengersTicketsDate = $importedOfferObj->ticketDate;
				} else {
					$serviceInsertEnt->passengersTicketsDate = TODAY_ISO;
				}
			}

		}

		// GET USER/OFFICE DETAILS FROM ORDER RESPONSIBLE (IF NOT MATCHED) ------------------------
		if (!$serviceInsertEnt->getField('idUser')) {
			$serviceInsertEnt->idUser = $importedServiceDet->idUser;
			$serviceInsertEnt->idOffice = \Entity\User::select($importedServiceDet->idUser)->idOffice;
		}

		// MANAGE SUPPLIER PRODUCT ////////////////////////////////////////////////////////////////
		$importedSupplierProduct = current((array)$importedServiceObj->supplierProducts);
		if (!$serviceInsertEnt->getField('idSupplierProduct') and $importedSupplierProduct) {

			// get current service config to check the product name attribute type
			$servAttrCfg = \App\Config\Services_GeneralAttributes::getInstance()->getServiceProductName( $serviceInsertEnt->getField('serviceCode') );

			// if supplier product is set to be automatically added and the service code supports supplier product
			if ($servAttrCfg['type']=='autocomplete' or $servAttrCfg['type']=='both') {
				$suppProdMatchObj = $importedSupplierProduct->getPopulatedMatchObject($idSystem);
				$suppProdMatchObj->search();
				if ($suppProdMatchObj->getResultsCount()==1) {
					$serviceInsertEnt->idSupplierProduct = $suppProdMatchObj->getFirstMatchId();
				}
			}

		}

	}


	/**
	 * Populates the order fields.
	 * @param \Entity\Order $orderInsertEnt
	 * @param array $servicesList
	 */
	private function populateOrderEntity(\Entity\Order $orderInsertEnt, array $servicesList)
	{

		$orderDetailsRq = $this->request->orderDetails;
		$orderInsertEnt->reservationCode		= $orderDetailsRq->code??'';
		$orderInsertEnt->flag					= $orderDetailsRq->flag??'';
		$orderInsertEnt->resType				= $orderDetailsRq->type??'';
		$orderInsertEnt->servicesType			= $orderDetailsRq->servicesType??'';
		$orderInsertEnt->idMiceProject			= $orderDetailsRq->idMiceProject??0;
		$orderInsertEnt->description			= $orderDetailsRq->description??'';
		$orderInsertEnt->comments				= $orderDetailsRq->comments??'';

		if ($orderDetailsRq?->idUser=='fromFirstService') {
			$firstService = reset($servicesList);
			$idRespUser = $firstService->getField('idUser');
		} else {
			$idRespUser = $orderDetailsRq->idUser;
		}
		$orderInsertEnt->idUser				= $idRespUser;
		$orderInsertEnt->idOffice			= \Entity\User::select($idRespUser)->idOffice;
		$orderInsertEnt->idDepartment		= \Entity\User::select($idRespUser)->idDepartment;


		// set corporate owner (if any) and force it on the order (if not present already)
		if ($orderDetailsRq->idCorporateOwner??null) {
			if (!in_array($orderDetailsRq->idCorporateOwner, $orderInsertEnt->getField('idCorporates'))) {
				$orderInsertEnt->idCorporates	= array_merge($orderInsertEnt->getField('idCorporates'), [$orderDetailsRq->idCorporateOwner]);
			}
			$orderInsertEnt->idCorporateOwner	= $orderDetailsRq->idCorporateOwner;
		}


		// decide on the owner customer
		if ($this->request->orderDetails->idCustomerOwner??null) {
			// if the customer owner is manually set but not present in the list of customers - add it
			$currentCustomersList = $orderInsertEnt->getField('idCustomers');
			if (!in_array($this->request->orderDetails->idCustomerOwner, $currentCustomersList)) {
				$currentCustomersList[] = $this->request->orderDetails->idCustomerOwner;
				$orderInsertEnt->idCustomers = $currentCustomersList;
			}
			$orderInsertEnt->idCustomerOwner = $this->request->orderDetails->idCustomerOwner;
		} else {
			$orderInsertEnt->idCustomerOwner = reset($orderInsertEnt->getField('idCustomers'));
		}

	}


	/**
	 * Inserts+chooses the offers on the services on this order.
	 */
	private function populateOffersEntity(array $servicesInsertList)
	{

		// LOOP THROUGH ALL SERVICES TO MANAGE OFFERS /////////////////////////////////////////////
		foreach($servicesInsertList as $serviceInsertEnt) {

			$idImportedService = $serviceInsertEnt->getField('idImportedService');
			$idService = $serviceInsertEnt->getId();

			$importedServiceEnt = \Entity\ImportedService::selectForUpdate($idImportedService);
			$serviceEnt = \Entity\Service::select($idService);

			$importedServiceObj = $importedServiceEnt->object;
			$importedOfferObj = $importedServiceObj->chosenOffer;

			// INSERT AND CONFIRM THE OFFER ///////////////////////////////////////////////////////
			if ($importedOfferObj) {

				// INSERT THE OFFER ---------------------------------------------------------------
				$offerInsertEnt = \Entity\Offer::insert();
				$offerItemInsertEnt = \Entity\OfferItem::insert();
				$offerInsertEnt->offerItems = [$offerItemInsertEnt];
				$this->populateOffersEntity_insertOffer($serviceEnt, $offerInsertEnt, $offerItemInsertEnt, $importedOfferObj);

				$offerInsertEnt->save();

				// SELECT (AND CONFIRM) CHOSEN OFFER ----------------------------------------------
				$updateServiceEnt = \Entity\Service::update($idService);
				$updateServiceEnt->chosenOfferId = $offerInsertEnt->getId();
				$updateServiceEnt->confirmedOffer = $importedServiceObj->chosenOfferConfirmed;
				$updateServiceEnt->save();

			}

		}

	}


	/**
	 * Populates the offers on the current order.
	 */
	private function populateOffersEntity_insertOffer(\Entity\Service $serviceEnt, \Entity\Offer $offerEnt, \Entity\OfferItem $offerItemEnt, \App\ImportedOrder\Offer $importedOfferObj)
	{

		// check input
		if ($serviceEnt->getOperation()!='select' or $offerEnt->getOperation()!='insert' or $offerItemEnt->getOperation()!='insert') {
			throw new \Exception('Wrong call of '.__METHOD__);
		}

		$offerEnt->idService = $serviceEnt->getId();

		$populateOfferObj = new \App\ImportedOrder\PopulateEntity\Offer($importedOfferObj, $offerEnt, $serviceEnt->idExternalSystem);
		$populateOfferObj->setOfferItem($offerItemEnt);
		$populateOfferObj->setIdCorporateForCardMatch($serviceEnt->order->idCorporateOwner);
		$populateOfferObj->populate();

		// if some supplier product found on the imported offer, but no match found, try to auto-insert it
		$idSystem = $serviceEnt->idExternalSystem;
		$importedSupplierProduct = $importedOfferObj->supplierProduct;
		if (!$offerEnt->getField('idSupplierProduct') and $importedSupplierProduct) {

			// get current service config to check the product name attribute type
			$servAttrCfg = \App\Config\Services_GeneralAttributes::getInstance()->getServiceProductName( $offerEnt->getField('serviceCode') );

			// if supplier product is set to be automatically added
			if ($servAttrCfg['type']=='autocomplete' or $servAttrCfg['type']=='both') {
				$suppProdMatchObj = $importedSupplierProduct->getPopulatedMatchObject($idSystem);
				$suppProdMatchObj->search();
				if ($suppProdMatchObj->getResultsCount()==1) {
					$offerEnt->idSupplierProduct = $suppProdMatchObj->getFirstMatchId();
				}
			}

		}

		$this->populateOffersEntity_insertOffer_applyOfferProfile($serviceEnt, $offerEnt, $offerItemEnt);

	}


	private function populateOffersEntity_insertOffer_applyOfferProfile(\Entity\Service $serviceEnt, \Entity\Offer $offerEnt, \Entity\OfferItem $offerItemEnt)
	{

		// APPLY THE OFFER PROFILE ............................................................
		// populate the fields for the offer profile calculation
		$fields = new \App\OfferProfileRules\Fields();
		$fields->populateFromOrderEntitySelect($serviceEnt->order);
		$fields->populateFromServiceEntitySelect($serviceEnt);
		$fields->populateFromOfferEntityInsert($offerEnt);
		$fields->operation = 'issue';

		// the offer item details (fields)
		$offerItemFields = $offerItemEnt->getFields();

		// get the offer profile depending on collected fields
		$profileCollector = new \App\OfferProfileRules\GetProfile($fields);
		$profileCollector->search();

		// apply the new profile on the existing offers
		$applyProfile = new \App\OfferProfileRules\ApplyProfile();

		$appliedOfferProfileRules = [
			'commission' => null,
			'discount' => null,
			'serviceFee' => null,
		];
		if (!is_numeric($offerItemFields['commissionInitialValue'])) {
			if ($profileCollector->getCommissionDetails()) {
				$applyProfile->setCommissionDetails($profileCollector->getCommissionDetails());
			}
			$appliedOfferProfileRules['commission'] = (Int)$profileCollector->getIdMatchedRule_commission();
		}
		if (!is_numeric($offerItemFields['discountInitialValue'])) {
			if ($profileCollector->getDiscountDetails()) {
				$applyProfile->setDiscountDetails($profileCollector->getDiscountDetails());
			}
			$appliedOfferProfileRules['discount'] = (Int)$profileCollector->getIdMatchedRule_discount();
		}
		if (!is_numeric($offerItemFields['serviceFeeInitialValue'])) {
			if ($profileCollector->getServiceFeeDetails()) {
				$applyProfile->setServiceFeeDetails($profileCollector->getServiceFeeDetails());
			}
			$appliedOfferProfileRules['serviceFee'] = (Int)$profileCollector->getIdMatchedRule_serviceFee();
		}

		$offerEnt->appliedOfferProfileRules = $appliedOfferProfileRules;

		// updates the offer and the offer item with details from offer profile
		$applyProfile->populateOfferFields($offerEnt, $offerItemEnt);

	}

	/**
	 * Composes the response on service process.
	 * @param int $idOrder
	 * @param array $servicesList
	 */
	private function populateResponse_process($idOrder, array $servicesList)
	{

		$this->response->idEntity = null;
		$this->response->idOrder = $idOrder;

		$servicesListResponse = new \stdClass();
		foreach($servicesList as $serviceKey=>$serviceInsertEnt) {
			$servicesListResponse->$serviceKey = $serviceInsertEnt->getId();
		}
		$this->response->servicesList = $servicesListResponse;

	}

}
