<?php
/**
 * Classe de gestion des API (REST).
 * @author Valentin CARRUESCO
 * @category Plugin
 * @license MIT
 */

class Api{
	public $slug,$description;

	function __construct($slug=null,$description=null){
		if(isset($slug)) $this->slug = $slug;
		if(isset($description)) $this->description = $description;
		$this->route = array();
		return $this;
	}

	function route($slug = null,$description = null,$method = null,$callback){
		$route =  new ApiRoute($slug,$description,$method,$callback);
		$this->route[$route->slug][$method] = $route;
	}

	function register(){
		Plugin::addHook('api',function(&$apis){
			$apis[$this->slug] = $this;
		});
	}

	//retourne la liste des headers de la requete courante (clé normalisées)
	public static function headers(){
		$headers = array();
		foreach(getallheaders() as $key=>$value)
			$headers[mb_strtolower($key)] = $value;
		return $headers;
	}

	//définis si la requete courante est via api ou en http classique (ou autre)
	public static function requested(){
		return preg_match('/\/api\/v[0-9]*\//i', $_SERVER['REQUEST_URI']);
	}

	//SI un JWToken est fournis en header Authorization: Bearer <token>, on récupere la session associée
	//si la session a expirée on la relance
	public static function token_session(){
		if(php_sapi_name() == 'cli') return;
		if(!self::requested()) return;

		$headers = self::headers();
		if(!isset($headers['authorization'])) return;

		try{
			$authorization = explode(' ',$headers['authorization']);
			if(count($authorization)!=2 || strtolower($authorization[0])!='bearer') throw new Exception("Bad Authorization header (required Authorization: Bearer <JWToken>)",403);
			global $conf,$myUser;

			$token = JWToken::parse(trim($authorization[1]),$conf->get('jwtauth_secret'));
			//si une session est en cours
			if(session_status() == PHP_SESSION_ACTIVE){
				//si la session courant a le mauvais id (ex une session anonyme ou expirée) on la détruit et on la recréé avec le bon id
				if( session_id() !=$token['attributes']['session_id']){
					session_destroy();
					session_id($token['attributes']['session_id']);
					session_start();
				}
			}else{
				//si aucun session en cours, on la créé avec le bon id
				session_id($token['attributes']['session_id']);
				session_start();
			}


			if(!isset($_SESSION['currentUser'])) throw new Exception('Token session has expired',498);
		}catch(Exception $e){
			$response['error'] = $e->getMessage();
			$response['errorCode'] = $e->getCode();
			echo json_encode($response);
			exit;
		}
	}

	public static function run(){
		global $_,$myUser;
		$response = array();

		try{
			set_error_handler(function( $errno ,  $errstr , $errfile ,  $errline){
				throw new Exception("Error ".$errno." Processing Request : ".$errfile." => ".$errstr.' L'.$errline, 500);
			});
			// register_shutdown_function(function(){
			// 	$error = error_get_last();
			// 	throw new Exception("Error 0 Processing Request ".$error['file'], 500);
			// });

			$command = explode('/',$_['command']);
			if(count($command)<1) throw new Exception("Unspecified API");

			$headers = self::headers();

			//basic auth (deprecated, utiliser plutot JWToken et Authorization Bearer)
			if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])){
				$user = User::check($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']);
				if(!$user) throw new Exception('Le compte spécifié est inexistant');
				$myUser = $user;
			}

			$request = array(
				'version' =>array_shift($command),
				'module' =>array_shift($command),
				'route' =>array_shift($command),
				'body' =>file_get_contents("php://input"),
				'headers' =>$headers,
				'method' => strtoupper($_SERVER['REQUEST_METHOD']),
				'parameters' => $_,
				'pathes' => array_filter($command)
			);
			$code = 200;
			$headers = array(
				'Content-Type: application/json'
			);
			$apis = array();
			Plugin::callHook('api',array(&$apis));
			//format de l'url : /api/v{version}/
			if(!isset($request['version']) || !is_numeric(substr($request['version'],1)) || substr($request['version'], 0,1)!='v') throw new Exception("Api version is missing or invalid, specify ".ROOT_URL."/api/{version} for valid requests", 404);

			if($request['module'] == 'schema'){
				if(!$myUser->connected()) throw new Exception("Credentials are missing",401);
				foreach ($apis as $api) {
					$routes= array();
					foreach($api->route as $route){

						foreach($route as $method=>$infos){
							if(!isset($routes[$infos->slug])) $routes[$infos->slug] = array() ;
							$routes[$infos->slug][] = $infos->method.' '.$api->slug .'/'.$infos->pattern .': '. $infos->description ;
						}
					}
					$response[] =  array( $api->slug => $api->description,'routes'=> $routes);
				}
			}else{
				if(!isset($apis[$request['module']])) throw new Exception("Api '".$request['module']."' is missing, see ".ROOT_URL."/api/{version}/schema for available calls", 404);
				$api = $apis[$request['module']];

				if(!isset($api->route[$request['route']]))  throw new Exception("Route '".$api->slug.'/'.$request['route']."' is missing, see ".ROOT_URL."/api/schema?pretty for available calls", 404);
				$route = $api->route[$request['route']];


				if(!isset($route[$request['method']]))  throw new Exception("Method ".$request['method']." '".$api->slug.'/'.$route->slug."' is not allowed", 405);
				$method = $route[$request['method']];
				$callback = $method->callback;

				$callback($request,$response);

				if(isset($response['code'])) $code = $response['code'];
				if(isset($response['headers'])) $headers = array_merge($headers,$response['headers']);

				unset($response['code']);
				unset($response['headers']);
			}
		} catch(Exception $e) {
			$response['error'] = $e->getMessage();
			$code = $e->getCode();
			if(empty($code)) $code = 0;
			$response['errorCode'] = $code;
		}
		$codes = array(
			200 => 'HTTP/1.1 200 Ok',
			201 => 'HTTP/1.1 201 Created',
			204 => 'HTTP/1.1 204 No Content',
			207 => 'HTTP/1.1 207 Multi-Status',
			400 => 'HTTP/1.0 400 Bad Request',
			401 => 'HTTP/1.0 401 Unauthorized',
			403 => 'HTTP/1.1 403 Forbidden',
			404 => 'HTTP/1.1 404 Not Found',
			404 => 'HTTP/1.1 405 Method Not Allowed',
			406 => 'HTTP/1.1 406 Not acceptable',
			409 => 'HTTP/1.1 409 Conflict',
			415 => 'HTTP/1.1 415 Unsupported Media Type',
			423 => 'HTTP/1.1 423 Locked',
			500 => 'HTTP/1.1 500 Internal Server Error',
			501 => 'HTTP/1.1 501 Not implemented',
			0 => 'HTTP/1.1 0 Unknown Error',
		);

		if (ob_get_length()) ob_end_clean();
		header(isset($codes[$code]) ? $codes[$code] : $codes[0]);

		foreach ($headers as $header)
			header($header);

		echo (isset($_['pretty'])) ? json_encode($response,JSON_PRETTY_PRINT) : json_encode($response);
		exit();
	}
}

class ApiRoute{
	public $slug,$description,$callback,$method,$pathes,$parameters,$pattern;
	function __construct($slug='default',$description='No description',$method='GET',$callback){
		$this->pattern = $slug;

		$infos = explode('?',$slug);
		if(isset($infos[1])) $this->parameters = $infos[1];
		$infos = explode('/',$infos[0]);

		$this->slug = array_shift($infos);
		$this->pathes = $infos;
		$this->description = $description;
		$this->method = $method;
		$this->callback = $callback;
	}
}
