# -*- mode: qore; indent-tabs-mode: nil -*-
#! @file JsonRpcHandler.qc JSON-RPC handler class definition

# to be registered as a handler in the HttpServer class

#! JsonRpcHandler class definition; to be registered as a handler in the HttpServer class
class JsonRpcHandler inherits public AbstractHttpRequestHandler {
    #! implementation of the handler
    const Version = "0.1";

    #! internal methods of the handler (introspection)
    const InternalMethods = (("function" : "help",
			      "help"     : "shows a list of JSON-RPC methods registered with this handler",
			      "text"     : "help",
			      "logopt"   : 2
			      ),
			     ("function" : "listMethods",
			      "help"     : "lists JSON-RPC method names registered with this handler",
			      "text"     : "system.listMethods",
			      "logopt"   : 2
			       ),
			     ("function" : "system_describe",
			      "help"     : "returns the service description object as per the JSON-RPC 1.1 spec",
			      "text"     : "system.describe",
			      "logopt"   : 2
			      ));

    #! default content type
    const JsonRpcContentType = "application/json";

    private {
        list $.methods = ();
        hash $.mi;
        int $.loglevel;
    }

    #! creates the handler with the given method list
    constructor(AbstractAuthenticator $auth, list $methods) : AbstractHttpRequestHandler($auth) {
	# add internal methods
	foreach my hash $im in (JsonRpcHandler::InternalMethods)
	    $.addMethodInternal($im + ( "internal" : True ));

	foreach my hash $m in ($methods) {
	    if (!exists $m.name)
		throw "JSON-RPC-CONSTRUCTOR-ERROR", sprintf("expecting 'name' key in method hash (%n)", $m);
	    if (!exists $m.function)
		throw "JSON-RPC-CONSTRUCTOR-ERROR", sprintf("expecting 'function' key in method hash (%n)", $m);

	    if (!exists $m.text)
		throw "JSON-RPC-CONSTRUCTOR-ERROR", sprintf("expecting 'text' key in method hash (%n)", $m);
	    delete $m.internal;
	    $.addMethodInternal($m);
	}
    }

    #! adds a method to the handler dynamically
    addMethod(string $name, any $func, string $text, string $help, any $logopt, any $cmark) {
	if (!exists $func)
	    throw "JSON-RPC-HANDLER-ADD-METHOD-ERROR", "expecting function name and text name as hash key in argument";

	$.addMethodInternal(( "name"     : $name,
			      "function" : $func,
			      "text"     : $text,
			      "help"     : $help,
			      "logopt"   : $logopt,
			      "cmark"    : $cmark ));
    }

    private addMethodInternal(hash $h) {
	# check for duplicate in method index
	my any $i = $.mi.($h.text);
	if (!exists $i)
	    $i = elements $.methods;

	if (!exists $h.name)
	    $h.name = sprintf("^%s\$", $h.text);
	$.methods[$i] = $h;
    }

    private hash help() {
	my hash $h;
	foreach my hash $m in ($.methods) {
	    $h.($m.text).description = $m.help;
	    if (exists $m.params)
		$h.($m.text).params  = $m.params;
	}
	return $h;
    }

    private hash system_describe() {
	my string $address = "http://localhost/JSON";
	my list $procs = ();

	foreach my hash $m in ($.methods)
	    if ($m.text != "service.describe")
		$procs += ( "name" : $m.text,
			    "summary" : $m.help );

	return ( "sdversion" : "1.0",
		 "name" : "Qore JSON-RPC Handler",
		 "id" : $address,
		 "version" : JsonRpcHandler::Version,
		 "summary" : "provides a JSON-RPC handler to the HTTP server",
		 #"address" : $address,
		 "procs" : $procs );
    }

    private list listMethods() {
	my list $l = ();
	foreach my hash $m in ($.methods)
	    $l += $m.text;
	return $l;
    }

    private log(hash $context, string $str) {
        my string $msg = "JSON-RPC ";
        if (exists $context.user)
            $msg += sprintf("user %s ", $context.user);
        $msg += sprintf("from %s: ", $context.source);
        $msg += vsprintf($str, $argv);

        call_function_args($context.logfunc, $msg);
    }

    private hash callMethod(hash $context, any $params) {
	my string $method = $context.method;
	# find method function
	my hash $found;
	foreach my hash $m in ($.methods) {
	    if (regex($method, $m.name)) {
		$found = $m;
		break;
	    }
	}

	if (exists $found) {
	    # add context marker, if any
	    $context.cmark = $found.cmark;
	    $context.function = $found.function;

	    if (($found.logopt & HttpServer::LP_LEVELMASK) <= $.loglevel && exists $context.logfunc) {
		my string $msg = $method;
		# add arguments to log message
		if ($found.logopt & HttpServer::LP_LOGPARAMS) {
		    $msg += sprintf("(");
		    my int $i = 0;
                    foreach my any $arg in ($params) {
			if (inlist($i++, $found.maskargs))
			    $msg += "<masked>, ";
			else if (type($arg) == Type::Hash && elements $arg) {
			    $msg += "(";
			    foreach my string $k in (keys $arg) {
				if ($k == $found.maskkey)
				    $msg += sprintf("%s=<masked>, ", $k);
			        else
				    $msg += sprintf("%s=%n, ", $k, $arg.$k);
			    }
			    splice $msg, -2, 2;
			    $msg += "), ";
			}
			else
			    $msg += sprintf("%n, ", $arg);
		    }
		    # remove the last two characters from the string if any were added
		    if ($i)
			splice $msg, -2, 2;
		    $msg += ")";
		}

                $.log($context, $msg);
	    }
	    #printf("about to call function '%s' (method=%s params=%n)\n", $found.function, $method, $params);
	    my any $rv;
	    if (type($params) == Type::List)
		unshift $params, $context;
	    else if (exists $params)
		$params = ($context, $params);
	    else
		$params = $context;

	    if ($found.internal)
		$rv = callObjectMethodArgs($self, $found.function, $params);
	    else
		$rv = call_function_args($found.function, $params);

            my hash $h.body = makeJSONRPCResponseString("1.1", $context.jsonid, $rv);
	    return $h;
	}
	else {
	    my string $str = sprintf("JSON-RPC-SERVER-UNKNOWN-METHOD: unknown method %n", $method);
	    return ( "code"   : 200,
		     "hdr"    : ( "Content-Type" : JsonRpcContentType ),
		     "body"   : makeJSONRPC11ErrorString(105, $str, $context.jsonid) );
        }
    }

    private do_param(any $value, reference $param) {
	if (exists $param) {
	    if (type($param) != Type::List)
		$param = list($param);
	    $param += $value;
	}
	else
	    $param = $value;
    }

    #! method called by HttpServer to handle a request
    hash handleRequest(hash $context, hash $hdr, *data $body, reference $close) {
	#printf("jsonrpc handler context=%n hdr=%n body=%n\n", $context, $hdr, $body);

	my hash $jsonrpc;

	if ($hdr.method == "GET") {
	    my string $path = substr($hdr.path, index($hdr.path, "/") + 1);

	    if (!strlen($path))
		return ( "code" : 501,
			 "desc" : "invalid HTTP GET: no path/JSON-RPC method name given" );

	    my list $args = split("?", $path);
	    $jsonrpc.method = shift $args;

	    $args = split("&", $args[0]);

	    # process arguments
	    if (elements $args) {
		my any $params;
		foreach my any $arg in ($args) {
		    my int $i;
		    if (($i = index($arg, "=")) == -1)
			continue;

		    my string $key = substr($arg, 0, $i);
		    my string $value = substr($arg, $i + 1);
		    # if it is a positional parameter
		    if (int($key) == $key) {
			# check for reasonable argument limits, otherwise ignore
			if ($key > 0 && $key < 9999)
			    $.do_param($value, \$params[$key - 1]);
		    }
		    else
			$.do_param($value, \$params.$key);
		}
		$jsonrpc.params = $params;
		#printf("params=%N\n", $params);
	    }

	    if (index($jsonrpc.method, ".") == -1)
		$jsonrpc.method = "omq.system." + $jsonrpc.method;
	}
	else {
	    if ($hdr.method != "POST")
		return ( "code" : 501,
			 "body" : sprintf("don't know how to handle method %n", $hdr.method) );

	    # accept any content-type with "json" in it, otherwise throw an error
	    #if ($hdr."content-type" !~ /json/)
	    #	return ( "code" : 501,
	    #		 "body" : sprintf("don't know how to handle content-type %n", $hdr."content-type") );

	    try {
		$jsonrpc = parseJSON($body);
	    }
	    catch (hash $ex) {
		my string $estr = sprintf("%s: %s", $ex.err, $ex.desc);
		return ( "code"   : 500,
			 "errlog" : $estr,
			 "body"   : $estr );
	    }
	}
	$context.method = $jsonrpc.method;
	$context.jsonid = $jsonrpc.id;
	try {
	    #printf("msg=%s\nxmlrpc=%N\n", $body, $jsonrpc);fflush();
	    return ( "code" : 200, "hdr" : ( "Content-Type" : JsonRpcContentType ) ) + $.callMethod($context, $jsonrpc.params);
	}
	catch (hash $ex) {
	    # show complete exception trace if system debug option is enabled
            my string $str = sprintf("%s: %s", $ex.err, $ex.desc);

	    return ( "code"   : 200,
		     "errlog" : $str,
		     "hdr"    : ( "Content-Type" : JsonRpcContentType ),
		     "body"   : makeJSONRPC11ErrorString(104, $str, $jsonrpc.id, $ex.arg) );
	}
    }
}