<?php
error_reporting(E_ERROR);
date_default_timezone_set('UTC');
ini_set('post_max_size','32M');
ini_set('upload_max_filesize','32M');

//define class autoloader
spl_autoload_register(function($className){
    $paths=array(
        '../classes/',
        '../classes/core/',
        '../classes/core/exception/',
        '../classes/core/authentication/',
        '../classes/core/interface/',
        '../classes/model/',
        '../classes/device/',
    );
    foreach($paths as $path){
        if(file_exists($path.$className.'.class.php')){
            include_once($path.$className.'.class.php');
        }
    }
});

//connect to the database
database::connect();
database::begin();

//set response type
$_REQUEST['responseType']='application/json';

$exceptionParameters=array();
$data=array();
try {

    $wwwroot=config::getWwwRoot();
    $uri=strtok($_SERVER['REQUEST_URI'],'?');
    $uri=rtrim($uri,'/');
    $uriArgs=explode('/', substr($uri,1));

    //get arguments from URI - ignore initial /device/
    array_shift($uriArgs);
    if(3===count($uriArgs)){
        $scope=$uriArgs[0];
        $action=$uriArgs[1];
        $apiKey=$uriArgs[2];
    } else if(2===count($uriArgs)){
        $scope=$uriArgs[0];
        $action=$uriArgs[1];
    } else if(1===count($uriArgs)){
        $scope=$uriArgs[0];
        echo json_encode(array('message'=>'Device access: '.$scope));
        exit;
    } else if(0===count($uriArgs)){
        echo json_encode(array('message'=>'This is the device access.'));
        exit;
    } else {
        throw new BadRequestException('POST to /device/DEVICE_SCOPE/DEVICE_ACTION');
    }

	$allowGet=false;
	if(is_numeric($action)){
		$parameters=[];
		$parameters[]=$action;
		$allowGet=true;
		$action="get";
	}

    //Do we have an API key, and is it valid for the scope (handler class)?
    //Accept API key in an X-IceBear-API-Key, Authorization, or Authorization: Bearer header,
    //or as third URI element, e.g., /device/SCOPE/METHOD/API_KEY
    if(!isset($apiKey)){
        $requestHeaders=apache_request_headers();
        if(isset($requestHeaders['X-IceBear-API-Key'])) {
            $apiKey = $requestHeaders['X-IceBear-API-Key'];
        } else if(isset($requestHeaders['Authorization'])){
            $header=$requestHeaders['Authorization'];
            $apiKey=trim(str_replace('Bearer', '', $header));
        } else {
            throw new AuthorizationRequiredException('No X-IceBear-API-Key or Authorization: Bearer header');
        }
    }

    //Did they send us a POST? It would be "cleaner" to insist on GET for read-only operations,
    //but there may be too much data to URL-encode, and the specs are vague on whether GET can
    //have a body, meaning that someone's proxy will probably eat it. So it's POST all the way.
    $method=$_SERVER['REQUEST_METHOD'];
	if(! ( ($allowGet && "get"===strtolower($method)) || "post"===strtolower($method) ) ){
		throw new BadRequestException("Use HTTP POST");
    }
    apikey::validate($apiKey, $scope);

    $logLevel=apikey::getLogLevel($apiKey);

    //Start logging
    $logFileName='DeviceAccess_'.$scope.'.log';
    $iceBoxName=config::getIceBoxName();
    if($iceBoxName){ $logFileName=$iceBoxName.'_'; }
    Log::init($logLevel, $logFileName);
    Log::info("Request received, scope is $scope, action is $action");

    //attempt to start session
    session::init(new PhpSession());
	Device::setScopeUserToSession($scope);
	session::setShowArchivedProjects(1);
	session::setShowInactiveUsers(1);

    //Convert post body JSON to an array
	if(!isset($parameters)){
		$postBody=file_get_contents("php://input");
		$parameters=json_decode($postBody, true);
		if(null===$parameters){
			$parameters=json_decode(urldecode($postBody), true);
		}
		if(null===$parameters){
			throw new BadRequestException('Request body could not be decoded from JSON (bad character?), or exceeds recursion limit');
		}
		if(!$parameters){
			$parameters=array($parameters);
		}
		if(!$parameters){
			throw new BadRequestException('JSON in post body is invalid or exceeds recursion limit: '.$postBody);
		}
	}
    $data=handleDeviceApiRequest($scope, $action, $parameters);

    //TODO if method starts with get, abort
    database::commit();

    respond($data,200);
} catch(Exception $e){
    database::abort();
    $statusCode=$e->getCode();
    $trace=$e->getTrace();
    $prettyTrace=array();
    $prettyTrace[]=$e->getFile().' line '.$e->getLine();
	$logLines=[];
	$logLines[]='Exception thrown: '.$e->getMessage();
	$logLines[]='Stack trace:';
    foreach($trace as $t){
        $traceLine='';
        $line=(isset($t['line'])) ? $t['line'] : '--';
        $file=(isset($t['file'])) ? str_replace(config::getWwwRoot(), '', (str_replace('\\','/',$t['file']))) : '--';
        if (array_key_exists('class',$t)){
            $traceLine=sprintf("%s:%s %s::%s",
                $file,
                $line,
                $t['class'],
                $t['function']
            );
        } else {
            $traceLine=sprintf("%s:%s %s",
                $file,
                $line,
                $t['function']
            );
        }
        $prettyTrace[]=$traceLine;
		$logLines[]=' - '.$traceLine;
    }
	if(Log::isInited() && !empty($logLines)){
		foreach ($logLines as $line){
			Log::error($line);
		}
	}

    respond(array(
        'error'=>$e->getMessage(),
        'parameters'=>$exceptionParameters,
        'thrownat'=>$prettyTrace[0],
        'trace'=>$prettyTrace
    ),$statusCode);
} finally {
	if(Log::isInited()){
		Log::end();
	}
    if(session::getSessionId()){
        session::revokeAdmin();
    }
}

/****************************************
 * Supporting functions below
 ***************************************/

/**
 * Returns the data to the client in the appropriate format, with the supplied HTTP response code.
 */
function respond($data, $responseCode): void {
    header('Access-Control-Allow-Origin: *');
    http_response_code($responseCode);
    header('Content-Type: application/json');
    $json=json_encode(sanitize($data));
    echo $json;
}

function sanitize($data){
    if(empty($data)){ return $data; }
    foreach($data as $k=>&$v){
        if(is_array($v)) {
			$v = sanitize($v);
        } else if(!is_null($v)){
			if(PHP_MAJOR_VERSION>=8 && PHP_MINOR_VERSION>=1){
				$v=htmlspecialchars($v, ENT_QUOTES); //Needed for >=PHP8.1 - see docs
			} else {
				$v=htmlspecialchars($v);
			}
        }
    }
    return $data;
}

/**
 * Calls a class method with the supplied arguments, throwing a BadRequestException if class or method does not exist.
 * @param string $className The class name
 * @param string $methodName The method name
 * @param array|null $args The arguments to pass to the method
 * @return mixed The results of the method call
 * @throws BadRequestException if the class or method does not exist
 */
function callOrThrow(string $className, string $methodName, ?array $args=array()): mixed {
	return baseobject::callOrThrow($className, $methodName, $args);
}

/**
 * @param $scope
 * @param $action
 * @param $parameters
 * @return mixed
 * @throws BadRequestException
 * @throws ForbiddenException
 * @throws NotFoundException
 * @throws ServerException
 */
function handleDeviceApiRequest($scope, $action, $parameters): mixed {
    if(!$scope || !$action) {
        throw new BadRequestException('Invalid URI. Call /device/SCOPE_NAME/METHOD_NAME');
    }
    Device::setScopeUserToSession($scope);
    return callOrThrow($scope, $action, array($parameters));
}
