<?php

namespace App\Cron\Job;

class BalancesRefresh extends \App\Cron\Job\Utils\GeneralAbstract
{
	/**
	 * Cronjob category: {system|operational|financial|reporting}
	 * @var string
	 */
	protected $category = 'system';

	/**
	 * Options for auto-balance.
	 * @see \App\Cron\Job\Utils\GeneralAbstract::getSettingsDialog()
	 */
	public function getSettingsDialog(array $fields)
	{
		$form = parent::getSettingsDialog($fields);

		$lang = \App\Loader_Lang::getInstance()->getSection("Admin/CronJobs/Management/BalancesRefresh");

		$formContainer = \Engine\Form\Container\Fieldset::create($lang['fieldset']);
		$form->addContainer($formContainer);

		// fix corporate debit balances
		$fixCorpDebitBalsFld = new \Engine\Form\Input\Checkbox($lang['option']['fixCorporateDebitBalances'], 'settings[fixCorporateDebitBalances]', $fields['settings']['fixCorporateDebitBalances']);
		$fixCorpDebitBalsFld->setTooltip($lang['option']['fixBalancesTooltip']);
		$formContainer->addElement($fixCorpDebitBalsFld);

		// fix customer debit balances
		$fixCustDebitBalsFld = new \Engine\Form\Input\Checkbox($lang['option']['fixCustomerDebitBalances'], 'settings[fixCustomerDebitBalances]', $fields['settings']['fixCustomerDebitBalances']);
		$fixCustDebitBalsFld->setTooltip($lang['option']['fixBalancesTooltip']);
		$formContainer->addElement($fixCustDebitBalsFld);

		// fix supplier debit balances
		$fixSuppDebitBalsFld = new \Engine\Form\Input\Checkbox($lang['option']['fixSupplierDebitBalances'], 'settings[fixSupplierDebitBalances]', $fields['settings']['fixSupplierDebitBalances']);
		$fixSuppDebitBalsFld->setTooltip($lang['option']['fixBalancesTooltip']);
		$formContainer->addElement($fixSuppDebitBalsFld);

		// fix corporate credit balances
		$fixCorpCreditBalsFld = new \Engine\Form\Input\Checkbox($lang['option']['fixCorporateCreditBalances'], 'settings[fixCorporateCreditBalances]', $fields['settings']['fixCorporateCreditBalances']);
		$fixCorpCreditBalsFld->setTooltip($lang['option']['fixBalancesTooltip']);
		$formContainer->addElement($fixCorpCreditBalsFld);

		// fix customer credit balances
		$fixCustCreditBalsFld = new \Engine\Form\Input\Checkbox($lang['option']['fixCustomerCreditBalances'], 'settings[fixCustomerCreditBalances]', $fields['settings']['fixCustomerCreditBalances']);
		$fixCustCreditBalsFld->setTooltip($lang['option']['fixBalancesTooltip']);
		$formContainer->addElement($fixCustCreditBalsFld);

		// fix supplier credit balances
		$fixSuppCreditBalsFld = new \Engine\Form\Input\Checkbox($lang['option']['fixSupplierCreditBalances'], 'settings[fixSupplierCreditBalances]', $fields['settings']['fixSupplierCreditBalances']);
		$fixSuppCreditBalsFld->setTooltip($lang['option']['fixBalancesTooltip']);
		$formContainer->addElement($fixSuppCreditBalsFld);

		return $form;
	}

	/**
	 * {@inheritDoc}
	 * @see \App\Cron\Job\Utils\GeneralAbstract::validateSettings()
	 */
	public function validateSettings(\App\Entity\Messages $msgs, array $fields)
	{
		if ($fields['scheduleOptions']['type']!='scheduled' ) {
			$msgs->addWarningText("The process should be set as `once per day or less`.");
		}

	}

	/**
	 * (non-PHPdoc)
	 * @see \App\Cron\Job\Utils\GeneralAbstract::executeJob()
	 */
	protected function executeJob()
	{

		// CORPORATE DEBIT BALANCES
		$this->executeJob_processBalances('corporate', 'debit', 'fixCorporateDebitBalances');

		// CUSTOMER DEBIT BALANCES
		$this->executeJob_processBalances('customer', 'debit', 'fixCustomerDebitBalances');

		// SUPPLIER DEBIT BALANCES
		$this->executeJob_processBalances('supplier', 'debit', 'fixSupplierDebitBalances');

		// CORPORATE CREDIT BALANCES
		$this->executeJob_processBalances('corporate', 'credit', 'fixCorporateCreditBalances');

 		// CUSTOMER CREDIT BALANCES
 		$this->executeJob_processBalances('customer', 'credit', 'fixCustomerCreditBalances');

 		// SUPPLIER CREDIT BALANCES
 		$this->executeJob_processBalances('supplier', 'credit', 'fixSupplierCreditBalances');

	}


	/**
	 * Displays and fixes (by user choice) balances for a certain entity and certain balance type.
	 * This is the very core of the current cron and is executed in various combinations.
	 * @param string $entityType - corporate/customer/supplier
	 * @param string $balanceType - credit/debit
	 * @param string $fixBalancesOptionCode - the cron settings var that indicates if the balances will be fixed or not
	 */
	private function executeJob_processBalances(string $entityType, string $balanceType, string $fixBalancesOptionCode) :void
	{

		// indicates if the balances will be fixed; also we selectForUpdate in these cases and we are inside a transaction
		$shouldFixBalances = (bool)$this->settings[$fixBalancesOptionCode];

		// log some messages
		$this->logger->info("Started checking the {$entityType} {$balanceType} balances.");
		if ($shouldFixBalances) {
			$this->logger->info("The process is configured to update all {$entityType} offset {$balanceType} balances.");
		}

		// start main transaction if we have to fix this type of balance
		if ($shouldFixBalances) {
			\DB::startTransaction();
		}

		// GETS ALL BALANCES AND COMPARES THEM ////////////////////////////////////////////////////
		$balanceNs = ($balanceType=='debit') ? 'DebitBalance' : 'ImpDocsBalance';
		$className = "\\App\\PartnerBalance\\FixStoredValues\\{$balanceNs}\\" . ucfirst($entityType);
		$entityBalancesRefresh = new $className();
		$calculatedBalances = $entityBalancesRefresh->getAllRealBalances($shouldFixBalances);
		$storedBalances = $entityBalancesRefresh->getAllStoredBalances($shouldFixBalances);
		$diffs = $entityBalancesRefresh->getAllBalancesDiffs($calculatedBalances, $storedBalances);

		// DISPLAY THE ENTITY BALANCES DIFFS //////////////////////////////////////////////////////
		$problemsCount = $this->logFoundEntityBalanceDiffs($diffs, $shouldFixBalances, $entityType, $balanceType);

		// if no problems found, we stop the current process
		if (!$problemsCount) {
			// stop current transaction as there will be no action
			if ($shouldFixBalances) {
				\DB::rollbackTransaction();
			}
		}

		// FIX THE BALANCES ///////////////////////////////////////////////////////////////////////
		$errorFound = false;
		if ($shouldFixBalances and $diffs and $problemsCount) {

			$this->logger->info("Start updating the offset {$entityType} {$balanceType} balances.");

			// GO THROUGH EACH ENTITY -------------------------------------------------------------
			foreach ($diffs as $idEntity=>$diffDetails) {

				// skip clients with small differences (that are within tolerance)
				if (!$diffDetails['shouldFix']) {
					continue;
				}

				$entityDisplayName = $this->getEntityDisplayName($entityType, $idEntity, true);
				$this->logger->info( "Start updating the {$balanceType} balances for {$entityType} `{$entityDisplayName}` (#{$idEntity})." );

				// FIX THE CURRENT ENTITY BALANCE -------------------------------------------------
				try {
					$entityBalancesRefresh->overwriteRealBalances($idEntity);

					// if this point reached - the entity was successfully updated
					$this->logger->success( "Successfully updated the {$balanceType} balances for {$entityType} `{$entityDisplayName}` (#{$idEntity})." );

				} catch (\App\PartnerBalance\FixStoredValues\Exception $e) {
					$this->logger->error( $e->getMessage() );
					$errorFound = true;
					break;
				}

			}

			// finish this huge transaction
			if ($errorFound) {
				$this->logger->critical( 'All changes are rollback-ed, even the ones that are confirmed above (if any).' );
				\DB::rollbackTransaction();
			} else {
				\DB::commitTransaction();
			}

		}

		// toggle the checkbox option for fixing back off
		if (!$errorFound and $this->settings[$fixBalancesOptionCode]) {
			$this->settings[$fixBalancesOptionCode] = false;
			$this->setSettings($this->settings);
			$this->logger->info("The option for updating {$entityType} {$balanceType} balances was unchecked automatically.");
		}

	}


	/**
	 * Logs in the cronjob logs all issues found for a specific entity and balance type.
	 * @param array $diffs
	 * @param bool $shouldFixBalances
	 * @param string $entityType - corporate, customer, supplier
	 * @param string $balanceType - debit/credit
	 * @return int - returns the number of problems found (that needs fixing)
	 */
	private function logFoundEntityBalanceDiffs(array $diffs, bool $shouldFixBalances, string $entityType, string $balanceType) :int
	{

		// if no diffs found; display a message and stop the process
		if (!$diffs) {
			$this->logger->idle("No issues found on any of the {$balanceType} blances for entity `{$entityType}`.");
			return 0;
		}

		// goes thtough each diff for current entity balances and displays some messages
		$problemsCount = 0;
		foreach ($diffs as $idEntity=>$diffDetails) {

			$entityDisplayName = $this->getEntityDisplayName($entityType, $idEntity, $shouldFixBalances);

			if (!$diffDetails['shouldFix']) {
				$msg = "Found a small difference with the {$balanceType} balance of {$entityType} {$entityDisplayName} (#{$idEntity}) but the difference is within tolerance. ";
				$msg .= 'Calculated balance: ' . ( $diffDetails['realBalance'] ? $this->displayBalance($diffDetails['realBalance']) : '-' ) . '. ';
				$msg .= 'Stored balance: ' . ( $diffDetails['storedBalance'] ? $this->displayBalance($diffDetails['storedBalance']) : '-' ) . '. ';
				$this->logger->info($msg);
			} else {
				$msg = "Found an offset with the {$balanceType} balance of {$entityType} {$entityDisplayName} (#{$idEntity}). ";
				$msg .= 'Calculated balance: ' . ( $diffDetails['realBalance'] ? $this->displayBalance($diffDetails['realBalance']) : '-' ) . '. ';
				$msg .= 'Stored balance: ' . ( $diffDetails['storedBalance'] ? $this->displayBalance($diffDetails['storedBalance']) : '-' ) . '. ';
				$this->logger->warning($msg);
				$problemsCount++;
			}

		}

		// display some counts
		if ($problemsCount) {
			$this->logger->warning( sprintf("Found %s problem(s) with offset {$balanceType} balances.", $problemsCount) );
		} else {
			$this->logger->idle( "No relevant isues found with {$balanceType} balances on entity {$entityType}." );
		}

		return $problemsCount;
	}

	/**
	 * Displays a balance (in a human friendly format) even when the balance contains multiple currencies.
	 * @param array $amountsPerCurrency - associative array of currency=>amount
	 * @return string
	 */
	private function displayBalance(array $amountsPerCurrency) :string
	{
		$return = [];
		foreach ($amountsPerCurrency as $curr=>$amount) {
			$return[] = \App\Utils\PriceFormat::format($amount, $curr);
		}
		return implode(' + ', $return);
	}


	/**
	 * Returns the display name of a single entity based on type and id. Also uses forUpdate flag
	 * when selecting the info.
	 * @param string $entityType
	 * @param int $idEntity
	 * @param bool $forUpdate
	 * @return string
	 */
	private function getEntityDisplayName(string $entityType, int $idEntity, bool $forUpdate) :string
	{
		if ($entityType=='corporate') {
			return \Entity\Corporate::select($idEntity, $forUpdate)->alias;
		} elseif ($entityType=='customer') {
			return \Entity\Customer::select($idEntity, $forUpdate)->name;
		} elseif ($entityType=='supplier') {
			return \Entity\Supplier::select($idEntity, $forUpdate)->alias;
		} else {
			throw new \Exception("Invalid entity type `{$entityType}` called in ".__METHOD__);
		}
	}

}
