<?php

namespace Modules\RemoteData\EntityManagementWs;

/**
 * The module handles REST-JSON webservice calls for entities management.
 * @author Gabi
 */
class Controller extends \App\Mvc\Controller\RestJsonServerAbstract
{

	/**
	 * This defines a list of operations that are requested and they should be renamed internally; this is
	 * mainly done because "match" has become a keyword in PHP 8.0.
	 * @var array
	 */
	private $operationAlias = ['match'=>'matching'];


	/**
	 * Returns the response object associated to the current webservice (when the request is valid).
	 * {@inheritDoc}
	 * @see \App\Mvc\Controller\RestJsonServerAbstract::createResponseObject()
	 */
	protected function createResponseObject(array $R, ?\stdClass $request=null)
	{
		return new \Modules\RemoteData\EntityManagementWs\Model\Response();
	}

	/**
	 * {@inheritDoc}
	 * @see \App\Mvc\Controller\RestJsonServerAbstract::executeOperation()
	 */
	protected function executeOperation(array $R, \stdClass $request, \App\Utils\Webservice\Rest\ResponseAbstract $response)
	{

		// INITS //////////////////////////////////////////////////////////////////////////////////

		$R = cleanHtmlRequestVars($R);

		$this->cleanRequestRec($request);

		$operation = $R['operation'];
		if (!$operation) {
			$response->setErrorMessage('No operation set.', 'operationNotSet');
			return ;
		}

		$entity = $R['entity'];
		if ($operation!='authenticate' and !$entity) {
			$response->setErrorMessage('No entity set.', 'entityNotSet');
			return ;
		}

		$hitCount = $R['hitCount']??null;


		// EXECUTE THE OPERATION AS CORE FUNCTIONALITY ////////////////////////////////////////////
		$this->checkAccess($entity, $operation, $R['sessionToken'], $hitCount, $R['secretToken']);


		// EXECUTE AUTHENTICATION HERE ////////////////////////////////////////////////////////////
		if ($operation=='authenticate') {
			try {

				$newSessionToken = \Modules\RemoteData\EntityManagementWs\Model\SessionToken::getInstance()->auth($R['authToken']);

			} catch (\Modules\RemoteData\EntityManagementWs\Model\SessionTokenException $e) {

				\Engine\Os::sleep(\Engine\RandomGenerator::randomFloat(1, 2));
				$response->applyRuntimeException($e);
				return ;
			}

			// associate the session token for a successful authentication
			$response->sessionToken = $newSessionToken;
			return ;
		}


		// EXECUTE ALL OTHER OPERATIONS ///////////////////////////////////////////////////////////


		if ($this->operationAlias[$operation]) {
			$className = ucfirst($this->operationAlias[$operation]);
		} else {
			$className = ucfirst($operation);
		}

		// check if the class managing this entity-operation exists
		$execClassName = sprintf("%s\\Model\\Entity\\%s\\%s", __NAMESPACE__, ucfirst($entity), $className);

		if (!class_exists($execClassName)) {
			\Engine\Os::sleep(\Engine\RandomGenerator::randomFloat(1, 2));
			$response->setErrorMessage('Invalid operation/entity.', 'EntityInvalidOperation');
			return ;
		}

		// execute the entity-operation
		if (class_exists($execClassName)) {
			try {

				// grant full access level
				\App\UserAccess\CurrentUserAccessFast::getInstance()->becomeSuperUser();

				$executeOperationObj = new $execClassName($R, $request, $response);
				$executeOperationObj->execute();

			}
			catch (\Modules\RemoteData\EntityManagementWs\Model\RuntimeException $e) {
				$response->applyRuntimeException($e);
			}
			catch (\Exception $e) { // catches all types of exceptions also
				$response->applyRuntimeException($e);
			} finally {
				\App\UserAccess\CurrentUserAccessFast::getInstance()->becomeNormalUser();
			}
		}

		// unclean response
		$this->uncleanResponseRec($response);

	}


	/**
	 * Recursively htmlClean-s the request object.
	 * @param \stdClass $request
	 */
	private function cleanRequestRec($request)
	{
		if (is_object($request)) {
			$return = $request;
			foreach($return as $attribute=>$value) {
				$return->$attribute = $this->cleanRequestRec($value);
			}
		} elseif (is_array($request)) {
			$return = [];
			foreach($request as $key=>$val) {
				$return[$key] = $this->cleanRequestRec($val);
			}
		} elseif ($request===null)  {
			$return = null;
		} else {
			$return = trim(htmlClean($request));
		}

		return $return;

	}



	private function checkAccess($entity, $operation, $sessionToken, $hitCount, $secretToken)
	{
		// for authenticate - no need for user access
		if ($operation=='authenticate') {
			return ;
		}


		// CHECK AUTH /////////////////////////////////////////////////////////////////////////////
		$idAuthAccount = null;
		try {

			$sessionTokenValidator = Model\SessionToken::getInstance();
			$sessionTokenValidator->check($sessionToken, $hitCount, $secretToken);

			$idAuthAccount = $sessionTokenValidator->getFoundIdAccount();

		} catch (\Modules\RemoteData\EntityManagementWs\Model\SessionTokenException $e) {
			// random sleep if there are issues with the session token
			\Engine\Os::sleep(\Engine\RandomGenerator::randomFloat(1, 2));
			throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException($e->getMessage(), $e->getMessageCode());
		}

		// check for entity
		$accountEnt = \Entity\EntityMngWsAccount::select($idAuthAccount);
		$accountAccessList = $accountEnt->accessLevel;

		// sleep based on hao many requests were done in the current minute
		$sleepTime = $this->getSleepTimeBasedOnMinuteCount($accountEnt);
		\Engine\Os::sleep( $sleepTime );

		if (!array_key_exists($entity, $accountAccessList)) {
			throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException('You dont have access to this entity or the entity is invalid.', 'EntityInvalidOperation');
		}

		// check for operation
		if (!in_array($operation, $accountAccessList[$entity])) {
			throw new \Modules\RemoteData\EntityManagementWs\Model\RuntimeException('You dont have access to this operation for this entity or the operation is invalid.', 'EntityInvalidOperation');
		}

	}


	/**
	 * Recursively htmlUnclean-s the response object.
	 * @param \stdClass $request
	 */
	private function uncleanResponseRec($response)
	{

		if (is_object($response)) {
			$return = $response;
			foreach($return as $attribute=>$value) {
				$return->$attribute = $this->uncleanResponseRec($value);
			}
		} elseif (is_array($response)) {
			$return = [];
			foreach($response as $key=>$val) {
				$return[$key] = $this->uncleanResponseRec($val);
			}
		} elseif ($response===null)  {
			$return = null;
		} else {
			$return = trim(htmlUnclean($response));
		}

		return $return;

	}

	/**
	 * Slows the execution based on rqCounter_count.
	 * @param \Entity\EntityMngWsAccount $accountSel
	 */
	private function getSleepTimeBasedOnMinuteCount(\Entity\EntityMngWsAccount $accountSel) :float
	{

		$CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();

		// less than min rpm
		if ($accountSel->rqCounter_count<=$CFG['RemoteData']['EntityManagementWs']['minRqPerMinute']) {
			return 0;
		}
		// between min and max - determine the proportional value from 0 to maxSleepTime
		elseif ($accountSel->rqCounter_count<=$CFG['RemoteData']['EntityManagementWs']['maxRqPerMinute']) {
			return ($accountSel->rqCounter_count - $CFG['RemoteData']['EntityManagementWs']['minRqPerMinute']) / ($CFG['RemoteData']['EntityManagementWs']['maxRqPerMinute'] - $CFG['RemoteData']['EntityManagementWs']['minRqPerMinute']) * $CFG['RemoteData']['EntityManagementWs']['maxSleepTime'];
		}
		// greater than max rpm
		else {
			return $CFG['RemoteData']['EntityManagementWs']['maxSleepTime'];
		}

	}

}
