<?php

namespace App\PartnerBalance\History;

abstract class GeneralAbstract
{

	/**
	 * The entity id.
	 * @var Int
	 */
	protected $idEntity;

	/**
	 * The currency of the balance history.
	 * @var String
	 */
	protected $currency;

	/**
	 * The db table name.
	 * @var String
	 */
	protected $tableName;

	/**
	 * The db table column name for the entity id.
	 * @var String
	 */
	protected $tableColumnIdEntity;

	/**
	 * Stores the date of the last getBalance() request.
	 * @var String
	 */
	protected $dateOfLastBalanceReq;

	/**
	 * Indicates a number of days (depending on the current day) - this period will not be saved in the database
	 * as it is too new and chances are that the client balances will change very often.
	 * @var Int
	 */
	protected $skipHistorySavingPeriod = 7;


	/**
	 * The singleton instances.
	 * @var array
	 */
	static protected $instance = [];


	/**
	 * Hide the class constructor for multiton instantiation.
	 * @param Int $idEntity
	 * @param String $idEntity
	 */
	final protected function __construct($idEntity, $currency)
	{
		if (!is_numeric($idEntity) or $idEntity<=0) {
			throw new \Exception('The entity id is not set/correct.');
		}
		// check currency
		if (!$currency or !\App\Config\Currencies::getInstance()->check($currency)) {
			throw new \Exception('Invalid currency `'.$currency.'` for `'.__METHOD__.'`.');
		}

		// set and check table details
		$this->setTableDetails();
		if (!$this->tableName) {
			throw new \Exception('The table name was not set for `'.get_called_class().'`.');
		}
		if (!$this->tableColumnIdEntity) {
			throw new \Exception('The table column name for the entity was not set for `'.get_called_class().'`.');
		}

		// set the entity id and the currency
		$this->idEntity = $idEntity;
		$this->currency = $currency;

	}

	/**
	 * Sets the internal attributes for db table access (tableName and tableColumnIdEntity)
	 */
	abstract protected function setTableDetails();


	/**
	 * Multiton instantiation for balance history stuff.
	 * @param String $idEntity
	 * @param String $currency
	 * @return \App\PartnerBalance\History\GeneralAbstract
	 */
	static public function getInstance($idEntity, $currency)
	{
		$currClass = get_called_class();
		if (!static::$instance[$currClass][$idEntity.'_'.$currency]) {
			static::$instance[$currClass][$idEntity.'_'.$currency] = new static($idEntity, $currency);
		}
		return static::$instance[$currClass][$idEntity.'_'.$currency];
	}

	/**
	 * Returns the date of the last getBalance() request.
	 * @return string - an ISO date or null if the
	 */
	public function getDateOfLastBalanceReq()
	{
		return $this->dateOfLastBalanceReq;
	}


	/**
	 * Returns the balance for a certain date (the balance is considered at the end of the day).
	 * @param String $date - ISO date
	 */
	public function getBalance($date, $storeBalancesHistory=true)
	{

		// resets the $this->dateOfLastBalanceReq (upon every getBalance() call).
		$this->dateOfLastBalanceReq = null;

		// CHECK INPUT ////////////////////////////////////////////////////////////////////////////

		// check date
		if (!validDate($date)) {
			throw new \Exception('The date is not valid for `'.__METHOD__.'`.');
		}
		if ($date>TODAY_ISO) {
			throw new \Exception('The date cannot be in the future for `'.__METHOD__.'`.');
		}
		if ($date>=TODAY_ISO) {
			throw new \Exception('The date is not valid for `'.__METHOD__.'` - you should not use the current day but get the client/supplier current balace.');
		}

		// GET PREV/NEXT BALANCES FOR AND CHECK THEM //////////////////////////////////////////////

		// gets prev balance (outputs array)
		$prevBalance = $this->getPrevBalance($date);

		// if the previous balance is the same as the requested date, this is the balance we need (it optimizes the searches)
		if ($prevBalance and $prevBalance['date']==$date) {
			$this->dateOfLastBalanceReq = $date;
			return $prevBalance['balance'];
		}

		// gets nect balance (outputs array)
		$nextBalance = $this->getNextBalance($date);


		// RETURNS THE BALANCE FOR THE REQUESTED DATE /////////////////////////////////////////////

		// IF NEXT BALANCE IS PRESENT - THIS MEANS THAT THE VALUES ARE ALREADY CALCULATED AND CACHED
		if ($nextBalance) {
			// return this balance
			if ($prevBalance) {
				$this->dateOfLastBalanceReq = $prevBalance['date'];
				return $prevBalance['balance'];
			} else {
				$this->dateOfLastBalanceReq = null;
				return 0;
			}
		}
		// NO NEXT BALANCE PRESENT - CALCULATE IT FROM THE PREVIOUS DATE TO CURRENT DATE ----------
		else {

			// start from the balance and go until the req date
			if ($prevBalance) {
				$initialBalance = $prevBalance['balance'];
				// starts from the following day AFTER initialDate
				$initialDateObj = \Engine\DateTime::createFromIsoDate($prevBalance['date']);
				$initialDateObj->add(new \DateInterval('P1D'));
				$initialDate = $initialDateObj->formatDateIso();
			}
			// start from 0 and go until the req date
			else {
				$initialBalance = 0;
				$initialDate = null;
			}

			// the transactions from the chosen interval
			$dailyTransList = $this->getTransactions($initialDate, $date, $storeBalancesHistory);

			// if no transactions were found in the selected interval - return the previous balance
			if (!$dailyTransList) {
				$this->dateOfLastBalanceReq = $prevBalance?$prevBalance['date']:null;
				return $initialBalance;
			}

			return $this->getFinalBalance($initialBalance, $dailyTransList, $storeBalancesHistory);

		}

	}


	/**
	 * Returns the last cached balance for the current entity/currency. Also sets the dateOfLastBalanceReq attribute.
	 * @return Float
	 */
	public function getLastStoredBalance()
	{
		// resets the $this->dateOfLastBalanceReq (upon every getBalance() call).
		$this->dateOfLastBalanceReq = null;

		// gets prev balance starting from today (outputs array)
		$prevBalance = $this->getPrevBalance(TODAY_ISO);

		// if some previous balance was found - return the data
		if ($prevBalance) {
			$this->dateOfLastBalanceReq = $prevBalance['date'];
			return $prevBalance['balance'];
		} else {
			return 0;
		}

	}


	/**
	 * Gets the previous balance for the current entity and currency. It includes the selected date.
	 * @param String $date - ISO date
	 * @throws \DBException - if the db SQL fails
	 * @return Array - an associative array of keys: date, balance
	 */
	protected function getPrevBalance($date)
	{

		$db = \DB::getInstance();

		// get the previous balance (including the selected date)
		$getPrevBalSQL = "SELECT date, balance FROM `{$this->tableName}` WHERE `{$this->tableColumnIdEntity}`=? AND `currency`=? AND `date`<=? ORDER BY `date` DESC LIMIT 1";
		$getPrevBalEx = $db->Execute($getPrevBalSQL, [$this->idEntity, $this->currency, $date]);
		if (!$getPrevBalEx) {
			throw new \DBException('Could not get the previously cached entity balance in order to determine the day balance.');
		}
		$row = $getPrevBalEx->FetchRow();
		if ($row and isset($row['balance'])) {
			$row['balance'] = (Float)$row['balance'];
		}
		return $row;

	}


	/**
	 * Gets the next balance for the current entity and currency. It doesn't include the selected date.
	 * @param String $date - ISO date
	 * @throws \DBException - if the db SQL fails
	 * @return Array - an associative array of keys: date, balance
	 */
	protected function getNextBalance($date)
	{

		$db = \DB::getInstance();

		// get the next balance (excluding the selected date)
		$getNextBalSQL = "SELECT date, balance FROM `{$this->tableName}` WHERE `{$this->tableColumnIdEntity}`=? AND `currency`=? AND `date`>? ORDER BY `date` ASC LIMIT 1";
		$getNextBalEx = $db->Execute($getNextBalSQL, [$this->idEntity, $this->currency, $date]);
		if (!$getNextBalEx) {
			throw new \DBException('Could not get the next cached entity balance in order to determine the day balance.');
		}
		return $getNextBalEx->FetchRow();

	}

	/**
	 * Processes all daily transactions and returns the final balance (the initial balance is also provided).
	 * @param Float $initialBalance
	 * @param Array $dailyTransList
	 * @return Float
	 */
	protected function getFinalBalance($initialBalance, $dailyTransList, $storeBalancesHistory=true)
	{

		// if balance history is to be saved, a transaction must be started
		if ($storeBalancesHistory and !\DB::isInsideTransaction()) {
			throw new \Exception('If you want to store the entity balances history you must be inside a transaction.');
		}

		$balance = $initialBalance;
		$historyBalances = [];

		$historyMaxDate = $this->getHistoryMaxDate();

		foreach($dailyTransList as $date=>$transList) {
			foreach($transList as $trans) {
				$balance += $trans['value'];
			}
			// filter only the old histories
			if ($date<=$historyMaxDate) {
				$historyBalances[$date] = $balance;
			}
		}

		// stores that last date of our interval
		$this->dateOfLastBalanceReq = $date;

		// saves the history balances
		if ($storeBalancesHistory and $historyBalances) {
			$this->writeHistoryBalances($historyBalances);
		}

		return $balance;

	}

	/**
	 * Returns the history max date (which will be stored as cache in the db). Newer dates are
	 * not stored as they may change very often.
	 * @return String
	 */
	protected function getHistoryMaxDate()
	{
		return \Engine\DateTime::create("- {$this->skipHistorySavingPeriod} days")->formatDateIso();
	}

	/**
	 * Writes the calculated daily balances into the database.
	 * @param Array $historyBalances
	 * @throws \DBException
	 */
	protected function writeHistoryBalances(Array $historyBalances)
	{

		$db = \DB::getInstance();

		// create prepared statement arrays
		$sqlValues = [];
		$sqlColumns = [];
		foreach($historyBalances as $date=>$balance) {
			$sqlColumns[] = "(?, ?, ?, ?)";
			$sqlValues = array_merge($sqlValues, [$this->idEntity, $this->currency, $date, $balance]);
		}

		// compose the final SQL
		$insertBalancesSQL = "INSERT INTO `{$this->tableName}` (`{$this->tableColumnIdEntity}`, `currency`, `date`, `balance`) VALUES " . implode(", ", $sqlColumns);

		// execute the SQL
		$insertBalancesEx = $db->Execute($insertBalancesSQL, $sqlValues);
		// check the SQL
		if (!$insertBalancesEx) {
			throw new \DBException('Could not insert the daily balances for an entity.');
		}

	}

	/**
	 * Returns the balances of the current entity (for the chosen currency) for the interval (initialDate, finalDate].
	 * The initial balance is considered at the end of the initialDate day (it includes transactions from that day).
	 * @param String $initialDate - ISO date
	 * @param String $finalDate - ISO date
	 * @param Boolean $selectForUpdate - indicates if the transactions will be blocked so a transaction is started.
	 * @return Array - associative array of day=>balance
	 */
	abstract protected function getTransactions($initialDate, $finalDate, $selectForUpdate) ;


	/**
	 * Eliminates all stale balances newer than a certain date (this should be done when older invoices/documents are added/changed.
	 * @param String $date - ISO date
	 */
	public function flushBalances($date)
	{

		// check that the date is valid
		if (!validDate($date)) {
			throw new \Exception('The date for flushBalances call is not valid for the class '.get_called_class());
		}

		// if the date is in the history max period - there will be noting to do, so skip this step
		if ($date>$this->getHistoryMaxDate()) {
			return true;
		}

		$db = \DB::getInstance();

		// compose and excute SQL
		$deleteBalancesSQL = "DELETE FROM `{$this->tableName}` WHERE `{$this->tableColumnIdEntity}`=? AND currency=? AND date>=?";
		$deleteBalancesEx = $db->Execute($deleteBalancesSQL, [$this->idEntity, $this->currency, $date]);
		// check sql
		if (!$deleteBalancesEx) {
			throw new \DBException('Some stale entity history balances could not be deleted.');
		}

	}

}