<?php

namespace Franzz\Spot;

use Franzz\Objects\PhpObject;
use Franzz\Objects\ToolBox;

//TODO Keep only local specificities and move bulk to Franzz\Objects\Controller
class Controller extends PhpObject
{
	const MUTATING_ACTIONS = array(
		'add_post',
		'subscribe',
		'unsubscribe',
		'update_project',
		'upload',
		'add_comment',
		'add_position',
		'admin_set',
		'admin_create',
		'admin_delete',
		'build_geojson'
	);

	private Spot $oSpot;
	private array $asReq;
	private string $sCsrfToken = '';

	public function __construct()
	{
		parent::__construct(__CLASS__);
	}

	private function setReqVal(string $sKey, $oValue, string $sValidation=''): void
	{
		$this->asReq[$sKey] = $this->validateValue($sValidation, $oValue);
	}

	public function handle($sProcessPage, array $argv = array()): string
	{
		//Start buffering so warnings/notices can be collected
		ob_start();

		//Parse variables
		$asReq = ToolBox::getRequest($argv);
		$this->asReq = array();
		$sAction = $asReq['a'] ?? '';
		$this->setReqVal('t', $asReq['t'] ?? '');
		$this->setReqVal('name', $asReq['name'] ?? '');
		$this->setReqVal('content', $asReq['content'] ?? '');
		$this->setReqVal('id_project', $asReq['id_project'] ?? 0, 'positiveInt');
		$this->setReqVal('id', $asReq['id'] ?? 0);
		$this->setReqVal('id_entity', $asReq['id'] ?? 0, 'positiveInt');
		$this->setReqVal('field', $asReq['field'] ?? '');
		$this->setReqVal('value', $asReq['value'] ?? '');
		$this->setReqVal('type', $asReq['type'] ?? '');
		$this->setReqVal('email', $asReq['email'] ?? '');
		$this->setReqVal('latitude', $asReq['latitude'] ?? '');
		$this->setReqVal('longitude', $asReq['longitude'] ?? '');
		$this->setReqVal('timestamp', $asReq['timestamp'] ?? 0, 'positiveInt');
		$this->setReqVal('csrf_token', $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? ''));

		//Create Spot Instance
		$this->oSpot = new Spot($sProcessPage, $this->asReq['t']);
		$this->oSpot->setProjectId($this->asReq['id_project']);

		//Validate CSRF & dispatch
		if(!$this->validateMutationRequest($sAction)) $sResult = Spot::getJsonResult(false, Spot::UNAUTHORIZED);
		elseif($sAction == '') $sResult = $this->oSpot->getAppMainPage($this->getCsrfToken());
		else $sResult = $this->dispatch($sAction);

		//Clean errors
		$sDebug = ob_get_clean();
		if($sDebug != '') $this->oSpot->addUncaughtError($sDebug);

		return $sResult;
	}

	private function validateMutationRequest(string $sAction): bool
	{
		return
			PHP_SAPI === 'cli'
			||
			!in_array($sAction, self::MUTATING_ACTIONS, true)
			||
			($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && $this->checkCsrfToken($this->asReq['csrf_token'])
		;
	}

	private function getCsrfToken(): string
	{
		if($this->sCsrfToken === '') $this->initCsrfToken();
		return $this->sCsrfToken;
	}

	private function setCsrfToken(): void
	{
		if(empty($_SESSION['csrf_token'])) $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
		$this->sCsrfToken = $_SESSION['csrf_token'];
	}

	private function initCsrfToken(): void
	{
		if(PHP_SAPI === 'cli') return;

		$bCloseSession = false;
		if(session_status() !== PHP_SESSION_ACTIVE) {
			$bSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
			session_set_cookie_params(array('httponly' => true, 'secure' => $bSecure, 'samesite' => 'Lax'));
			session_start();
			$bCloseSession = true;
		}

		$this->setCsrfToken();
		if($bCloseSession) session_write_close();
	}

	private function checkCsrfToken(string $sClientToken): bool
	{
		$sServerToken = $this->getCsrfToken();
		return PHP_SAPI === 'cli' || ($sServerToken !== '' && is_string($sClientToken) && hash_equals($sServerToken, $sClientToken));
	}

	private function dispatch(string $sAction): string
	{
		return match($sAction) {
			'markers'			=> $this->oSpot->getMarkers(),
			'last_update'		=> $this->oSpot->getLastUpdate(),
			'geojson'			=> $this->oSpot->getProjectGeoJson(),
			'next_feed'			=> $this->oSpot->getNextFeed($this->asReq['id']),
			'new_feed'			=> $this->oSpot->getNewFeed($this->asReq['id']),
			'add_post'			=> $this->oSpot->addPost($this->asReq['name'], $this->asReq['content']),
			'subscribe'			=> $this->oSpot->subscribe($this->asReq['email'], $this->asReq['name']),
			'unsubscribe'		=> $this->oSpot->unsubscribe(),
			'unsubscribe_email'	=> $this->oSpot->unsubscribeFromEmail($this->asReq['id_entity']),
			'update_project'	=> $this->oSpot->updateProject(),
			default				=> $this->dispatchAdmin($sAction)
		};
	}

	private function dispatchAdmin(string $sAction): string
	{
		if(!$this->oSpot->checkUserClearance(User::CLEARANCE_ADMIN)) {
			return Spot::getJsonResult(false, Spot::NOT_FOUND);
		}

		return match($sAction) {
			'upload'		=> $this->oSpot->upload(),
			'add_comment'	=> $this->oSpot->addComment($this->asReq['id_entity'], $this->asReq['content']),
			'add_position'	=> $this->oSpot->addPosition($this->asReq['latitude'], $this->asReq['longitude'], $this->asReq['timestamp']),
			'admin_get'		=> $this->oSpot->getAdminSettings(),
			'admin_set'		=> $this->oSpot->setAdminSettings($this->asReq['type'], $this->asReq['id_entity'], $this->asReq['field'], $this->asReq['value']),
			'admin_create'	=> $this->oSpot->createAdminSettings($this->asReq['type']),
			'admin_delete'	=> $this->oSpot->deleteAdminSettings($this->asReq['type'], $this->asReq['id_entity']),
			'sql'			=> $this->oSpot->getDbBuildScript(),
			'build_geojson'	=> $this->oSpot->buildGeoJSON($this->asReq['name']),
			default			=> Spot::getJsonResult(false, Spot::NOT_FOUND)
		};
	}

	private static function validateValue(string $sValidation, $oValue=0)
	{
		return match($sValidation) {
			'' => $oValue,
			'positiveInt' => filter_var($oValue, FILTER_VALIDATE_INT, array('options' => array('default' => 0, 'min_range' => 0)))
		};
	}
}
