<?php 

class basesession {

	public const SHOW_ARCHIVED_PROJECTS = 'showArchivedProjects';
	public const SHOW_INACTIVE_USERS = 'showInactiveUsers';
	protected static object $session;

	/**
	 * @param $sessionObject
	 * @param bool $isAuthCheck
	 * @return array
	 * @throws BadRequestException
	 * @throws ServerException
	 * @throws NotFoundException
	 */
	public static function init($sessionObject, bool $isAuthCheck=false): array {
		static::$session=$sessionObject;
		$now=time();
		$timeoutSeconds=60*config::get('auth_timeout_mins');
		if($isAuthCheck) {
			$timeoutLimit=(int)(session::get('lastAccess'))+($timeoutSeconds);
			if(session::isLoggedIn()){
				if($now>=$timeoutLimit){
					session::destroy();
					return ['secondsRemaining'=>0];
				}
				return ['secondsRemaining'=>$now-session::get('lastAccess')];
			} else {
				session::destroy();
				return ['secondsRemaining'=>0];
			}
		} else {
			session::$session->set('lastAccess',time());
			session::refreshProjectPermissions();
			return ['secondsRemaining'=>$timeoutSeconds];
		}
	}

	public static function exists(): bool {
	    return !empty(static::$session);
    }

	/**
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function refreshProjectPermissions($isAdmin=null, $wasAdmin=null): void {
		$projectPermissions=array(
			'create' =>[],
			'read'   =>[],
			'readOne'=>[],
			'update' =>[],
			'delete' =>[]
		);
		$memberGroups=array();
		$visibleGroups=array();
		$visibleGroupIds=array();
		$memberGroupNames=array();
		$memberGroupIds=array();
		$ownProjectIds=array();
		$canCreateProjects=false;
		$canCreateUsergroups=false;
		$specialAccessProjects=array();
		$showArchivedProjects=basesession::getShowArchivedProjects();
		basesession::getShowInactiveUsers(); //not used, but forces it to explicit boolean
		$archivedProjectIds=[];
		if(self::isLoggedIn()){ //otherwise no permissions, nothing to do

			$userId=session::getUserId();
			$memberGroups=database::queryGetAll('SELECT SQL_CALC_FOUND_ROWS ug.id, ug.name, ug.cancreateprojects, ug.cancreateusergroups
				FROM usergroup AS ug, groupmembership AS gm
				WHERE ug.id=gm.usergroupid AND gm.userid=:userid',
				array(':userid'=>$userId)
			);
			$memberGroups=$memberGroups['rows'];
			if(!empty($memberGroups)){
				$memberGroupIds=array_column($memberGroups, 'id');
				$memberGroupNames=array_column($memberGroups, 'name');
			}

			if(is_null($isAdmin)){
				$isAdmin=in_array(baseusergroup::ADMINISTRATORS, $memberGroupNames);
			} else {
				$isAdmin=(bool)$isAdmin;
			}
			session::set('isAdmin',$isAdmin);

			if(is_null($wasAdmin)){
				$wasAdmin=$isAdmin;
			}
			session::set('wasAdmin',(bool)$wasAdmin);

			//Member groups
			$memberGroups=database::queryGetAll('SELECT * FROM usergroup AS ug,groupmembership AS gm 
         		WHERE gm.usergroupid=ug.id AND gm.userid=:userid',
				[':userid'=>$userId]
			);
			if(!empty($memberGroups)){
				$memberGroups=$memberGroups['rows'];
				$memberGroupNames=array_column($memberGroups,'name');
				$memberGroupIds=array_column($memberGroups,'id');
			}

			//Visible groups
			$visibleGroups=usergroup::getAll(array('all'=>1));
			$visibleGroups=$visibleGroups['rows'];
			$visibleGroupIds=array_column($visibleGroups, 'id');

			if($isAdmin){

				//Special access projects
				$specialAccessProjects=database::queryGetAll('SELECT id,name,owner,isarchived FROM project');
				if(!empty($specialAccessProjects)){
					$specialAccessProjects=$specialAccessProjects['rows'];
				}
				//own projects
				$ownProjects=database::queryGetAll('SELECT id,name,owner,isarchived FROM project WHERE owner=:userid', [':userid'=>$userId]);
				if(!empty($ownProjects)){
					$ownProjectIds=array_column($ownProjects['rows'],'id');
				}

				//Usergroup-based permissions
				//Not needed - special access covers all projects

				//archived projects
				$archivedProjects=database::queryGetAll('SELECT id,owner,name,isarchived FROM project where isarchived=1');
				if(isset($archivedProjects['rows'])){
					$archivedProjectIds=array_column($archivedProjects['rows'], 'id');
				}

				//can create groups and projects?
				$canCreateUsergroups=true;
				$canCreateProjects=true;

			} else {

				//Special access projects
				$specialAccessProjects=database::queryGetAll('SELECT id,name,owner,isarchived FROM project WHERE owner=:userid', [':userid'=>$userId]) ?? [];
				if(!empty($specialAccessProjects)){
					$specialAccessProjects=$specialAccessProjects['rows'];
				}

				//own projects
				$ownProjects=$specialAccessProjects;
				if(!empty($ownProjects)){
					$ownProjectIds=array_column($ownProjects,'id');
				}

				//Usergroup-based permissions
				$permissions=database::queryGetAll('SELECT SQL_CALC_FOUND_ROWS proj.id AS projectid, proj.isarchived as isarchived, perm.type AS type FROM project AS proj, permission AS perm, groupmembership AS gm
						WHERE gm.userid=:userid AND gm.usergroupid=perm.usergroupid AND perm.projectid=proj.id
					',
					array(':userid'=>$userId)
				);
				if(!empty($permissions) && !empty($permissions['rows']) ){
					foreach($permissions['rows'] as $p){
						$isArchivedProject=1*$p['isarchived'];
						if(!$isArchivedProject){
							if('read'==$p['type']){
								$projectPermissions['readOne'][]=$p['projectid'];
							}
							$projectPermissions[$p['type']][]=$p['projectid'];
						} else {
							//No create/update/delete in archived projects
							if('read'==$p['type']){
								$projectPermissions['readOne'][]=$p['projectid'];
								if($showArchivedProjects){
									$projectPermissions['read'][]=$p['projectid'];
								}
							}
						}
					}
				}

				//can create groups and projects?
				if(!empty($memberGroups)){
					foreach($memberGroups as $g){
						if(1*$g['cancreateprojects']){ $canCreateProjects=true; }
						if(1*$g['cancreateusergroups']){ $canCreateUsergroups=true; }
					}
				}

			}
		}

		//Handle admin and project owner rights
		foreach ($specialAccessProjects as $project) {
			$projectId=$project['id'];
			if(1*$project['isarchived']){
				//Project owner and admin should always be able to getById/Name archived projects...
				$projectPermissions['readOne'][]=$projectId;
				if($showArchivedProjects){
					//...but archived projects shouldn't show up in searches unless showArchivedProjects is true.
					$projectPermissions['read'][]=$projectId;
				}
			} else {
				//Not archived. Project owner or admin can do all of these.
				$projectPermissions['read'][]=$projectId;
				$projectPermissions['readOne'][]=$projectId;
				$projectPermissions['create'][]=$projectId;
				$projectPermissions['delete'][]=$projectId;
				$projectPermissions['update'][]=$projectId;
			}
		}

		//can create groups and projects?
		session::set('canCreateProjects',$canCreateProjects);
		session::set('canCreateUsergroups',$canCreateUsergroups);

		//special permission for dummy sessions
		if(DummySession::DUMMYSESSION===session_id()){
			$projectPermissions['read'][]=baseproject::getSharedProjectId();
			$projectPermissions['readOne'][]=baseproject::getSharedProjectId();
//			$projectPermissions['read'][]=baseproject::getDefaultProjectId();
//			$projectPermissions['readOne'][]=baseproject::getDefaultProjectId();
		}

		session::set('ownProjectIds', $ownProjectIds);
		session::set('archivedProjectIds',$archivedProjectIds);
		session::set('projectPermissions',$projectPermissions);
		session::set('memberGroups',$memberGroups);
		session::set('memberGroupIds',$memberGroupIds);
		session::set('memberGroupNames',$memberGroupNames);
		session::set('visibleGroups',$visibleGroups);
		session::set('visibleGroupIds',$visibleGroupIds);
		if(!file_exists(config::getWwwRoot().'/install')){
			session::refreshUserConfig();
		}
	}

	/**
	 * @throws ServerException
	 * @throws BadRequestException
	 */
	public static function setShowArchivedProjects($show): array {
		$show=(boolean)$show;
		session::set(self::SHOW_ARCHIVED_PROJECTS,$show);
		session::refreshProjectPermissions();
		return [self::SHOW_ARCHIVED_PROJECTS =>$show];
	}

	public static function getShowArchivedProjects(): bool {
		$show=basesession::get(self::SHOW_ARCHIVED_PROJECTS);
		if(is_null($show)){
			basesession::setShowArchivedProjects(false);
			return false;
		}
		return $show;
	}

	public static function setShowInactiveUsers($show): array {
		$show=(boolean)$show;
		session::set(self::SHOW_INACTIVE_USERS,$show);
		return [self::SHOW_INACTIVE_USERS =>$show];
	}

	public static function getShowInactiveUsers(): bool {
		$show=basesession::get(self::SHOW_INACTIVE_USERS);
		if(is_null($show)){
			basesession::setShowInactiveUsers(false);
			return false;
		}
		return $show;
	}

	/**
     * @param $name
     * @return mixed
     */
	public static function get($name): mixed {
		return session::$session->get($name);
	}

    /**
     * @param $name
     * @param $value
     */
	public static function set($name, $value): void {
		session::$session->set($name, $value);
	}

    /**
     *
     */
	public static function destroy(): void {
		session::$session->destroy();
	}

	public static function getSessionId(): string {
		$sessionId=session_id();
		if(PHP_MAJOR_VERSION>=8 && PHP_MINOR_VERSION>=1){
			$sessionId=htmlspecialchars($sessionId, ENT_QUOTES); //Needed for >=PHP8.1 - see docs
		} else {
			$sessionId=htmlspecialchars($sessionId);
		}
		return $sessionId;
	}

    /**
     */
	public static function isAdmin(): bool {
		return (bool)session::$session->get('isAdmin');
	}

	/**
	 * Temporarily become an administrator. This is needed in limited scenarios, where a user action
	 * may result in files being added to a project that they cannot normally modify. It should be
	 * used sparingly. Use session::revertAdmin() to return the admin status to what it was before.
	 * @throws ServerException
	 * @throws BadRequestException
	 */
    public static function becomeAdmin(): void {
        $isAdmin=session::isAdmin();
        session::set('wasAdmin',$isAdmin);
        session::set('isAdmin',true);
		session::refreshProjectPermissions(true);
    }

	/**
	 * Revoke the session's administrator rights. This is needed mainly in test.
	 * Use session::revertAdmin() to return the admin status to what it was before.
	 * @throws BadRequestException
	 * @throws ServerException
	 */
    public static function revokeAdmin(): void {
        $isAdmin=session::isAdmin();
        session::set('wasAdmin',$isAdmin);
        session::set('isAdmin',false);
		session::refreshProjectPermissions(false);
    }

	/**
	 * Reverts the admin status to what it was before calling session::becomeAdmin() or session::revokeAdmin.
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function revertAdmin(): void {
	    $wasAdmin=session::get('wasAdmin');
	    session::set('isAdmin',$wasAdmin);
		session::refreshProjectPermissions($wasAdmin);
	}

    /**
     * Throws a ForbiddenException if the current session does not have administrator privileges.
     * @throws ForbiddenException
     */
	public static function requireAdmin(): void {
	    if(!session::isAdmin()){
	        throw new ForbiddenException('You must be an administrator to do this');
        }
    }

    /**
     * @return bool
     */
	public static function canCreateUsergroups(): bool {
		return (bool)session::$session->get('canCreateUsergroups');
	}

    /**
     * @return bool
     */
	public static function canCreateProjects(): bool {
		return (bool)session::$session->get('canCreateProjects');
	}

    /**
     * @return bool
     */
	public static function isLoggedIn(): bool {
		return !empty(session::$session->get('user'));
	}

    /**
     * @return mixed
     */
	public static function getUser(): mixed {
		return session::get('user');
	}

    /**
     * @return int
     */
	public static function getUserId(): int {
		$ret=session::get('userId');
		if($ret){ return $ret; }
		return 0;
	}

	/**
	 * @return string|null
	 */
	public static function getUsername(): ?string {
		return session::get('username');
	}

	public static function getOwnProjectIds() {
		return basesession::get('ownProjectIds');
	}

    /**
     * @return mixed
     */
	public static function getVisibleGroups(): mixed {
		return session::get('visibleGroups');
	}

    /**
     * @return mixed
     */
	public static function getVisibleGroupIds(): mixed {
		return session::get('visibleGroupIds');
	}

    /**
     * @return mixed
     */
	public static function getMemberGroups(): mixed {
		return session::get('memberGroups');
	}

    /**
     * @return mixed
     */
	public static function getMemberGroupIds(): mixed {
		return session::get('memberGroupIds');
	}

	/**
	 * @param $request
	 * @return array
	 * @throws AuthorizationRequiredException
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function login($request): array {
		$badLoginAttempts=(int)(session::get('badLoginAttempts'));
		$delayEnd=(int)(session::get('loginDelayUntil'));
		$delayRemaining=$delayEnd-time();
		$username = @trim($request['username']);
		$password = @trim($request['password']);
		$authenticatorType = 'database'; //LATER from config
		if (empty($username) || empty($password)) {
			throw new AuthorizationRequiredException('Username and password are required');
		}
		if($delayRemaining>1){
			$error = "Try again in $delayRemaining seconds";
		} else {
			$authenticated = call_user_func_array($authenticatorType . 'authenticator::authenticate', array($username, $password));
			if ($authenticated) {
				session::set('badLoginAttempts',0);
				session::set('loginDelayUntil', null);
				return basesession::setUserToSession($username);
			}
			$error = 'Username or password incorrect';
			$badLoginAttempts++;
			session::set('loginDelayUntil', time()+$badLoginAttempts);
		}
		session::set('badLoginAttempts',$badLoginAttempts);
		throw new AuthorizationRequiredException($error);
	}

	/**
	 * @throws BadRequestException
	 * @throws AuthorizationRequiredException
	 * @throws ServerException
	 */
	public static function setUserToSession($username): array {
		$user=user::getByName($username);
		$result=database::queryGetOne('SELECT isactive FROM user WHERE id=:id', array(':id'=>$user['id']));
		if(!$result['isactive']){
			throw new AuthorizationRequiredException('Your account is not active. See your administrator.');
		}
		$csrfToken=basesession::generateCsrfToken();
		session::set('user',$user);
		session::set('userId',$user['id']);
		session::set('username',$username);
		session::set('lastAccess',time());
		session::refreshProjectPermissions();
		return array(
			'success'=>true,
			'username'=>$username,
			'userid'=>$user['id'],
			'isadmin'=>$user['isadmin'], //Used by login page if code and DB versions do not match
			'sid'=>session::getSessionId(),
			'csrfToken'=>$csrfToken,
			'projectPermissions'=>session::get("projectPermissions"),
		);
	}

    /**
     * @param $parameters
     * @return bool
     * @throws AuthorizationRequiredException
     */
	public static function validateCsrfToken($parameters): bool {
		if(!isset($parameters['csrfToken'])){
			throw new AuthorizationRequiredException('CSRF token not supplied');
		} else if($parameters['csrfToken'] != session::get('csrfToken')){
			throw new AuthorizationRequiredException('CSRF Token Mismatch');
		}
		unset($parameters['csrfToken']);
		return true;
	}

    /**
     * Logs the current user out and destroys their session.
	 * @return array
     */
	public static function logout(): array {
		session::destroy();
		return array(
			'success'=>true,
			'loggedOut'=>true		);
	}

    /**
     * @param $ids
     * @return array|null
     * @throws BadRequestException
     * @throws ServerException
     */
	private static function getProjects($ids): ?array {
		if(empty($ids)){ return []; }
		$inClause=implode(',', $ids);
		if(!preg_match('/^[0-9,]+$/', $inClause)){
			throw new BadRequestException('Bad project ID list passed to session::getProjects');
		}
		$inClause='id IN ('. $inClause .')';
		if(1==count($ids)){ $inClause='id='.$ids[0]; }
		$userId=session::getUserId();
		$sql='SELECT * FROM project WHERE isarchived=FALSE AND (owner='.$userId.' OR '.$inClause.') ';
		$result=database::queryGetAll($sql);
		if(!is_array($result)){ return []; }
		return $result;
	}

    /**
     * @param bool $withDetails
     * @return array|null
     * @throws BadRequestException
     * @throws ServerException
     */
	public static function getCreateProjects(bool $withDetails=false): ?array {
		$perms=session::get('projectPermissions');
		if(!$withDetails){ return $perms['create'] ?? []; }
		return session::getProjects($perms['create']);
	}

	/**
	 * @param bool $withDetails
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getReadProjects(bool $withDetails=false): ?array {
		$perms=session::get('projectPermissions');
		if(!$withDetails){ return $perms['read'] ?? []; }
		return session::getProjects($perms['read'] ?? []);
	}

	/**
	 * @param bool $withDetails
	 * @return array|null
	 * @throws BadRequestException
	 * @throws ServerException
	 */
	public static function getReadOneProjects(bool $withDetails=false): ?array {
		$perms=session::get('projectPermissions');
		if(!$withDetails){ return $perms['readOne'] ?? []; }
		return session::getProjects($perms['readOne'] ?? []);
	}

    /**
     * @param bool $withDetails
     * @return array|null
     * @throws BadRequestException
     * @throws ServerException
     */
	public static function getUpdateProjects(bool $withDetails=false): ?array {
		$perms=session::get('projectPermissions');
		if(!$withDetails){ return $perms['update'] ?? []; }
		return session::getProjects($perms['update'] ?? []);
	}

    /**
     * @param bool $withDetails
     * @return array|null
     * @throws BadRequestException
     * @throws ServerException
     */
	public static function getDeleteProjects(bool $withDetails=false): ?array {
		$perms=session::get('projectPermissions');
		if(!$withDetails){ return $perms['delete'] ?? []; }
		return session::getProjects($perms['delete']) ?? [];
	}

    /**
     * @param null $accessType
     * @param bool $forceSharedProject
     * @return ?array
     * @throws ServerException
     * @throws BadRequestException
     */
	public static function getProjectPermissions($accessType=null, bool $forceSharedProject=false): ?array {
		$perms=session::get('projectPermissions');
		if($forceSharedProject){
			$sharedProjectId=project::getSharedProjectId();
			if(!$sharedProjectId){ throw new ServerException('Could not get shared project ID'); }
			foreach($perms as $k=>$v){
				if(!in_array($sharedProjectId, $v)){
					$perms[$k][]=$sharedProjectId;
				}
			}
		}
		if(null==$accessType){ return $perms ?? []; }
		if(!in_array($accessType, array('create','read','readOne','update','delete'))){
			throw new ServerException('Tried to get project permissions for unknown access type '.$accessType);
		}
		return $perms[$accessType] ?? [];
	}


    /**
     * @throws ServerException
     */
	public static function refreshUserConfig(): void {
		session::set('userConfig', null);
		try {
			$userConfig=userconfig::getAll();
			session::set('userConfig', $userConfig);
		} catch(BadRequestException){
			//probably in installer and table doesn't exist.
		}
	}

    /**
     * Generates a token for CSRF prevention.
     * @see https://www.owasp.org/index.php/PHP_CSRF_Guard
     * @return string
     */
	private static function generateCsrfToken(): string {
		if (function_exists("hash_algos") and in_array("sha512",hash_algos())){
			$token=hash("sha512",mt_rand(0,mt_getrandmax()));
		} else {
			$token=' ';
			for ($i=0;$i<128;++$i){
				$r=mt_rand(0,35);
				if ($r<26){
					$c=chr(ord('a')+$r);
				} else {
					$c=chr(ord('0')+$r-26);
				}
				$token.=$c;
			}
		}
		session::set('csrfToken', $token);
		return $token;
	}

	/**
	 * @throws ServerException
	 */
	public static function getAll() {
		throw new ServerException('getAll not implemented in basesession');
	}

}

class PhpSession {

    /**
     * PhpSession constructor.
     * @param null $sessionId
     */
    public function __construct($sessionId=null){
		if($sessionId){ session_id($sessionId); }
		session_start();
	}

    /**
     * @param $name
     * @return mixed
     */
	public function get($name): mixed {
		if(isset($_SESSION[$name])){
			return $_SESSION[$name];
		}
		return null;
	}

    /**
     * @param $name
     * @param $value
     */
	public function set($name, $value):void {
		$_SESSION[$name]=$value;
	}

    /**
     * @return string
     */
	public function getSessionId(): string {
		return session_id();		
	}

    /**
     *
     */
	public function destroy(): void {
		session_destroy();
	}
}

class DummySession {

	const DUMMYSESSION = 'DUMMYSESSION';
	private array $session;

    /**
     * DummySession constructor.
     */
	public function __construct(){
		$this->session=array();
		session_id(DummySession::DUMMYSESSION);
	}

	/**
	 * @param string $name
	 * @return mixed
	 */
	public function get(string $name): mixed {
		if(isset($this->session[$name])){
			return $this->session[$name];
		} 
		return null;
	}

	/**
	 * @param string $name
	 * @param mixed $value
	 */
	public function set(string $name, mixed $value): void {
		$this->session[$name]=$value;
	}

    /**
     * @return string
     */
	public function getSessionId(): string {
		return self::DUMMYSESSION;
	}

    /**
     *
     */
	public function destroy(): void {
		unset($this->session);
	}
}