/*
 * json-rpc-shell.c: trivial JSON-RPC 2.0 shell
 *
 * Copyright (c) 2014 - 2015, Přemysl Janouch 
 * All rights reserved.
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */
/// Some arbitrary limit for the history file
#define HISTORY_LIMIT 10000
#include "config.h"
#include "utils.c"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
// --- Main program ------------------------------------------------------------
static struct app_context
{
	CURL *curl;                         ///< cURL handle
	char curl_error[CURL_ERROR_SIZE];   ///< cURL error info buffer
	bool pretty_print;                  ///< Whether to pretty print
	bool verbose;                       ///< Print requests
	bool trust_all;                     ///< Don't verify peer certificates
	bool auto_id;                       ///< Use automatically generated ID's
	int64_t next_id;                    ///< Next autogenerated ID
	iconv_t term_to_utf8;               ///< Terminal encoding to UTF-8
	iconv_t term_from_utf8;             ///< UTF-8 to terminal encoding
}
g_ctx;
#define PARSE_FAIL(...)                                                        \
	BLOCK_START                                                                \
		print_error (__VA_ARGS__);                                             \
		goto fail;                                                             \
	BLOCK_END
static bool
parse_response (struct app_context *ctx, struct str *buf)
{
	json_error_t e;
	json_t *response;
	if (!(response = json_loadb (buf->str, buf->len, JSON_DECODE_ANY, &e)))
	{
		print_error ("failed to parse the response: %s", e.text);
		return false;
	}
	bool success = false;
	if (!json_is_object (response))
		PARSE_FAIL ("the response is not a JSON object");
	json_t *v;
	if (!(v = json_object_get (response, "jsonrpc")))
		print_warning ("`%s' field not present in response", "jsonrpc");
	else if (!json_is_string (v) || strcmp (json_string_value (v), "2.0"))
		print_warning ("invalid `%s' field in response", "jsonrpc");
	json_t *result = json_object_get (response, "result");
	json_t *error  = json_object_get (response, "error");
	json_t *data   = NULL;
	if (!result && !error)
		PARSE_FAIL ("neither `result' nor `error' present in response");
	if (result && error)
		// Prohibited by the specification but happens in real life (null)
		print_warning ("both `result' and `error' present in response");
	if (error)
	{
		if (!json_is_object (error))
			PARSE_FAIL ("invalid `%s' field in response", "error");
		json_t *code    = json_object_get (error, "code");
		json_t *message = json_object_get (error, "message");
		if (!code)
			PARSE_FAIL ("missing `%s' field in error response", "code");
		if (!message)
			PARSE_FAIL ("missing `%s' field in error response", "message");
		if (!json_is_integer (code))
			PARSE_FAIL ("invalid `%s' field in error response", "code");
		if (!json_is_string (message))
			PARSE_FAIL ("invalid `%s' field in error response", "message");
		json_int_t code_val = json_integer_value (code);
		char *utf8 = xstrdup_printf ("error response: %" JSON_INTEGER_FORMAT
			" (%s)", code_val, json_string_value (message));
		char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
		if (!s)
			print_error ("character conversion failed for `%s'", "error");
		else
			printf ("%s\n", s);
		free (s);
		data = json_object_get (error, "data");
	}
	if (data)
	{
		char *utf8 = json_dumps (data, JSON_ENCODE_ANY);
		char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
		free (utf8);
		if (!s)
			print_error ("character conversion failed for `%s'", "error data");
		else
			printf ("error data: %s\n", s);
		free (s);
	}
	if (result)
	{
		int flags = JSON_ENCODE_ANY;
		if (ctx->pretty_print)
			flags |= JSON_INDENT (2);
		char *utf8 = json_dumps (result, flags);
		char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
		free (utf8);
		if (!s)
			print_error ("character conversion failed for `%s'", "result");
		else
			printf ("%s\n", s);
		free (s);
	}
	success = true;
fail:
	json_decref (response);
	return success;
}
static bool
is_valid_json_rpc_id (json_t *v)
{
	return json_is_string (v) || json_is_integer (v)
		|| json_is_real (v) || json_is_null (v);  // These two shouldn't be used
}
static bool
is_valid_json_rpc_params (json_t *v)
{
	return json_is_array (v) || json_is_object (v);
}
static bool
isspace_ascii (int c)
{
	return strchr (" \f\n\r\t\v", c);
}
static size_t
write_callback (char *ptr, size_t size, size_t nmemb, void *user_data)
{
	struct str *buf = user_data;
	str_append_data (buf, ptr, size * nmemb);
	return size * nmemb;
}
#define RPC_FAIL(...)                                                          \
	BLOCK_START                                                                \
		print_error (__VA_ARGS__);                                             \
		goto fail;                                                             \
	BLOCK_END
static bool
try_advance (const char **p, const char *text)
{
	size_t len = strlen (text);
	if (strncmp (*p, text, len))
		return false;
	*p += len;
	return true;
}
static bool
validate_content_type (const char *type)
{
	const char *content_types[] =
	{
		"application/json-rpc",  // obsolete
		"application/json"
	};
	const char *tails[] =
	{
		"; charset=utf-8",
		"; charset=UTF-8",
		""
	};
	bool found = false;
	for (size_t i = 0; i < N_ELEMENTS (content_types); i++)
		if ((found = try_advance (&type, content_types[i])))
			break;
	if (!found)
		return false;
	for (size_t i = 0; i < N_ELEMENTS (tails); i++)
		if ((found = try_advance (&type, tails[i])))
			break;
	if (!found)
		return false;
	return !*type;
}
static void
make_json_rpc_call (struct app_context *ctx,
	const char *method, json_t *id, json_t *params)
{
	json_t *request = json_object ();
	json_object_set_new (request, "jsonrpc", json_string ("2.0"));
	json_object_set_new (request, "method",  json_string (method));
	if (id)      json_object_set (request, "id",     id);
	if (params)  json_object_set (request, "params", params);
	char *req_utf8 = json_dumps (request, 0);
	if (ctx->verbose)
	{
		char *req_term = iconv_xstrdup
			(ctx->term_from_utf8, req_utf8, -1, NULL);
		if (!req_term)
			print_error ("%s: %s", "verbose", "character conversion failed");
		else
			printf ("%s\n", req_term);
		free (req_term);
	}
	struct str buf;
	str_init (&buf);
	if (curl_easy_setopt (ctx->curl, CURLOPT_POSTFIELDS, req_utf8)
	 || curl_easy_setopt (ctx->curl, CURLOPT_POSTFIELDSIZE_LARGE,
		(curl_off_t) -1)
	 || curl_easy_setopt (ctx->curl, CURLOPT_WRITEDATA, &buf)
	 || curl_easy_setopt (ctx->curl, CURLOPT_WRITEFUNCTION, write_callback))
		RPC_FAIL ("cURL setup failed");
	CURLcode ret;
	if ((ret = curl_easy_perform (ctx->curl)))
		RPC_FAIL ("HTTP request failed: %s", ctx->curl_error);
	long code;
	char *type;
	if (curl_easy_getinfo (ctx->curl, CURLINFO_RESPONSE_CODE, &code)
	 || curl_easy_getinfo (ctx->curl, CURLINFO_CONTENT_TYPE, &type))
		RPC_FAIL ("cURL info retrieval failed");
	if (code != 200)
		RPC_FAIL ("unexpected HTTP response code: %ld", code);
	bool success = false;
	if (id)
	{
		if (!type)
			print_warning ("missing `Content-Type' header");
		else if (!validate_content_type (type))
			print_warning ("unexpected `Content-Type' header: %s", type);
		success = parse_response (ctx, &buf);
	}
	else
	{
		printf ("[Notification]\n");
		if (buf.len)
			print_warning ("we have been sent data back for a notification");
		else
			success = true;
	}
	if (!success)
	{
		char *s = iconv_xstrdup (ctx->term_from_utf8,
			buf.str, buf.len + 1, NULL);
		if (!s)
			print_error ("character conversion failed for `%s'",
				"raw response data");
		else
			printf ("%s: %s\n", "raw response data", s);
		free (s);
	}
fail:
	str_free (&buf);
	free (req_utf8);
	json_decref (request);
}
static void
process_input (struct app_context *ctx, char *user_input)
{
	char *input;
	size_t len;
	if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len)))
	{
		print_error ("character conversion failed for `%s'", "user input");
		goto fail;
	}
	// Cut out the method name first
	char *p = input;
	while (*p && isspace_ascii (*p))
		p++;
	// No input
	if (!*p)
		goto fail;
	char *method = p;
	while (*p && !isspace_ascii (*p))
		p++;
	if (*p)
		*p++ = '\0';
	// Now we go through this madness, just so that the order can be arbitrary
	json_error_t e;
	size_t args_len = 0;
	json_t *args[2] = { NULL, NULL }, *id = NULL, *params = NULL;
	while (true)
	{
		// Jansson is too stupid to just tell us that there was nothing
		while (*p && isspace_ascii (*p))
			p++;
		if (!*p)
			break;
		if (args_len == N_ELEMENTS (args))
		{
			print_error ("too many arguments");
			goto fail_parse;
		}
		if (!(args[args_len] = json_loadb (p, len - (p - input),
			JSON_DECODE_ANY | JSON_DISABLE_EOF_CHECK, &e)))
		{
			print_error ("failed to parse JSON value: %s", e.text);
			goto fail_parse;
		}
		p += e.position;
		args_len++;
	}
	for (size_t i = 0; i < args_len; i++)
	{
		json_t **target;
		if (is_valid_json_rpc_id (args[i]))
			target = &id;
		else if (is_valid_json_rpc_params (args[i]))
			target = ¶ms;
		else
		{
			print_error ("unexpected value at index %zu", i);
			goto fail_parse;
		}
		if (*target)
		{
			print_error ("cannot specify multiple `id' or `params'");
			goto fail_parse;
		}
		*target = json_incref (args[i]);
	}
	if (!id && ctx->auto_id)
		id = json_integer (ctx->next_id++);
	make_json_rpc_call (ctx, method, id, params);
fail_parse:
	if (id)      json_decref (id);
	if (params)  json_decref (params);
	for (size_t i = 0; i < args_len; i++)
		json_decref (args[i]);
fail:
	free (input);
}
static void
on_winch (EV_P_ ev_signal *handle, int revents)
{
	(void) loop;
	(void) handle;
	(void) revents;
	// This fucks up big time on terminals with automatic wrapping such as
	// rxvt-unicode or newer VTE when the current line overflows, however we
	// can't do much about that
	rl_resize_terminal ();
}
static void
on_readline_input (char *line)
{
	if (!line)
	{
		rl_callback_handler_remove ();
		ev_break (EV_DEFAULT_ EVBREAK_ONE);
		return;
	}
	if (*line)
		add_history (line);
	// Stupid readline forces us to use a global variable
	process_input (&g_ctx, line);
	free (line);
}
static void
on_tty_readable (EV_P_ ev_io *handle, int revents)
{
	(void) loop;
	(void) handle;
	if (revents & EV_READ)
		rl_callback_read_char ();
}
static void
parse_program_arguments (struct app_context *ctx, int argc, char **argv,
	char **origin, char **endpoint)
{
	static const struct opt opts[] =
	{
		{ 'd', "debug", NULL, 0, "run in debug mode" },
		{ 'h', "help", NULL, 0, "display this help and exit" },
		{ 'V', "version", NULL, 0, "output version information and exit" },
		{ 'a', "auto-id", NULL, 0, "automatic `id' fields" },
		{ 'o', "origin", "O", 0, "set the HTTP Origin header" },
		{ 'p', "pretty", NULL, 0, "pretty-print the responses" },
		{ 't', "trust-all", NULL, 0, "don't care about SSL/TLS certificates" },
		{ 'v', "verbose", NULL, 0, "print the request before sending" },
		{ 0, NULL, NULL, 0, NULL }
	};
	struct opt_handler oh;
	opt_handler_init (&oh, argc, argv, opts,
		"ENDPOINT", "Trivial JSON-RPC shell.");
	int c;
	while ((c = opt_handler_get (&oh)) != -1)
	switch (c)
	{
	case 'd':
		g_debug_mode = true;
		break;
	case 'h':
		opt_handler_usage (&oh, stdout);
		exit (EXIT_SUCCESS);
	case 'V':
		printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
		exit (EXIT_SUCCESS);
	case 'a':
		ctx->auto_id = true;
		break;
	case 'o':
		*origin = optarg;
		break;
	case 'p':
		ctx->pretty_print = true;
		break;
	case 't':
		ctx->trust_all = true;
		break;
	case 'v':
		ctx->verbose = true;
		break;
	default:
		print_error ("wrong options");
		opt_handler_usage (&oh, stderr);
		exit (EXIT_FAILURE);
	}
	argc -= optind;
	argv += optind;
	if (argc != 1)
	{
		opt_handler_usage (&oh, stderr);
		exit (EXIT_FAILURE);
	}
	*endpoint = argv[0];
	opt_handler_free (&oh);
}
int
main (int argc, char *argv[])
{
	char *origin = NULL;
	char *endpoint = NULL;
	parse_program_arguments (&g_ctx, argc, argv, &origin, &endpoint);
	if (strncmp (endpoint, "http://", 7)
	 && strncmp (endpoint, "https://", 8))
		exit_fatal ("the endpoint address must begin with"
			" either `http://' or `https://'");
	CURL *curl;
	if (!(g_ctx.curl = curl = curl_easy_init ()))
		exit_fatal ("cURL initialization failed");
	struct curl_slist *headers = NULL;
	headers = curl_slist_append (headers, "Content-Type: application/json");
	if (origin)
	{
		origin = xstrdup_printf ("Origin: %s", origin);
		headers = curl_slist_append (headers, origin);
	}
	if (curl_easy_setopt (curl, CURLOPT_POST,           1L)
	 || curl_easy_setopt (curl, CURLOPT_NOPROGRESS,     1L)
	 || curl_easy_setopt (curl, CURLOPT_ERRORBUFFER,    g_ctx.curl_error)
	 || curl_easy_setopt (curl, CURLOPT_HTTPHEADER,     headers)
	 || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER,
			g_ctx.trust_all ? 0L : 1L)
	 || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST,
			g_ctx.trust_all ? 0L : 2L)
	 || curl_easy_setopt (curl, CURLOPT_URL,            endpoint))
		exit_fatal ("cURL setup failed");
	// We only need to convert to and from the terminal encoding
	setlocale (LC_CTYPE, "");
	char *encoding = nl_langinfo (CODESET);
#ifdef __linux__
	// XXX: not quite sure if this is actually desirable
	// TODO: instead retry with JSON_ENSURE_ASCII
	encoding = xstrdup_printf ("%s//TRANSLIT", encoding);
#endif // __linux__
	if ((g_ctx.term_from_utf8 = iconv_open (encoding, "UTF-8"))
		== (iconv_t) -1
	 || (g_ctx.term_to_utf8 = iconv_open ("UTF-8", nl_langinfo (CODESET)))
		== (iconv_t) -1)
		exit_fatal ("creating the UTF-8 conversion object failed: %s",
			strerror (errno));
	char *data_home = getenv ("XDG_DATA_HOME"), *home = getenv ("HOME");
	if (!data_home || *data_home != '/')
	{
		if (!home)
			exit_fatal ("where is your $HOME, kid?");
		data_home = xstrdup_printf ("%s/.local/share", home);
	}
	using_history ();
	stifle_history (HISTORY_LIMIT);
	char *history_path =
		xstrdup_printf ("%s/" PROGRAM_NAME "/history", data_home);
	(void) read_history (history_path);
	// XXX: we should use termcap/terminfo for the codes but who cares
	char *prompt = xstrdup_printf ("%c\x1b[1m%cjson-rpc> %c\x1b[0m%c",
		RL_PROMPT_START_IGNORE, RL_PROMPT_END_IGNORE,
		RL_PROMPT_START_IGNORE, RL_PROMPT_END_IGNORE);
	// readline 6.3 doesn't immediately redraw the terminal upon reception
	// of SIGWINCH, so we must run it in an event loop to remediate that
	struct ev_loop *loop = EV_DEFAULT;
	if (!loop)
		exit_fatal ("libev initialization failed");
	ev_signal winch_watcher;
	ev_io tty_watcher;
	ev_signal_init (&winch_watcher, on_winch, SIGWINCH);
	ev_signal_start (EV_DEFAULT_ &winch_watcher);
	ev_io_init (&tty_watcher, on_tty_readable, STDIN_FILENO, EV_READ);
	ev_io_start (EV_DEFAULT_ &tty_watcher);
	rl_catch_sigwinch = false;
	rl_callback_handler_install (prompt, on_readline_input);
	ev_run (loop, 0);
	putchar ('\n');
	ev_loop_destroy (loop);
	// User has terminated the program, let's save the history and clean up
	char *dir = xstrdup (history_path);
	(void) mkdir_with_parents (dirname (dir), NULL);
	free (dir);
	if (write_history (history_path))
		print_error ("writing the history file `%s' failed: %s",
			history_path, strerror (errno));
	free (history_path);
	iconv_close (g_ctx.term_from_utf8);
	iconv_close (g_ctx.term_to_utf8);
	curl_slist_free_all (headers);
	free (origin);
	curl_easy_cleanup (curl);
	return EXIT_SUCCESS;
}