// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2020-2023 by James R.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
// \brief HTTP based master server

/*
Documentation available here.

                     <http://mb.srb2.org/MS/tools/api/v1/>
*/

#ifdef HAVE_CURL
#include <curl/curl.h>
#endif

#include "../doomdef.h"
#include "d_clisrv.h"
#include "client_connection.h"
#include "../command.h"
#include "../m_argv.h"
#include "../m_menu.h"
#include "mserv.h"
#include "i_tcp.h"/* for current_port */
#include "../i_threads.h"

/* reasonable default I guess?? */
#define DEFAULT_BUFFER_SIZE (4096)

/* I just stop myself from making macros anymore. */
#define Blame( ... ) \
	CONS_Printf("\x85" __VA_ARGS__)

#define PROTO_ANY 0
#define PROTO_V4 1
#define PROTO_V6 2

static void MasterServer_Debug_OnChange (void);

consvar_t cv_masterserver_timeout = CVAR_INIT
(
		"masterserver_timeout", "5", CV_SAVE, CV_Unsigned,
		NULL
);

consvar_t cv_masterserver_debug = CVAR_INIT
(
	"masterserver_debug", "Off", CV_SAVE|CV_CALL, CV_OnOff,
	MasterServer_Debug_OnChange
);

consvar_t cv_masterserver_token = CVAR_INIT
(
		"masterserver_token", "", CV_SAVE, NULL,
		NULL
);

#ifdef MASTERSERVER

static int hms_started;

static boolean hms_allow_ipv6;

static char *hms_api;
#ifdef HAVE_THREADS
static I_mutex hms_api_mutex;
#endif

static char *hms_server_token;
#ifndef NO_IPV6
static char *hms_server_token_ipv6;
#endif

static char hms_useragent[512];

struct HMS_buffer
{
	CURL *curl;
	char *buffer;
	int   needle;
	int    end;
};

static void
Contact_error (void)
{
	CONS_Alert(CONS_ERROR,
			"There was a problem contacting the master server...\n"
	);
}

static void
get_user_agent(char *buf, size_t len)
{
	if (snprintf(buf, len, "%s/%s (%s; %s; %i; %i) SRB2BASE/%i", SRB2APPLICATION, VERSIONSTRING, compbranch, comprevision,  MODID, MODVERSION, CODEBASE) < 0)
		I_Error("http-mserv: get_user_agent failed");
}

static void
init_user_agent_once(void)
{
	if (hms_useragent[0] != '\0')
		return;

	get_user_agent(hms_useragent, 512);
}

static size_t
HMS_on_read (char *s, size_t _1, size_t n, void *userdata)
{
	struct HMS_buffer *buffer;
	size_t blocks;

	(void)_1;

	buffer = userdata;

	if (n >= (size_t)( buffer->end - buffer->needle ))
	{
		/* resize to next multiple of buffer size */
		blocks = ( n / DEFAULT_BUFFER_SIZE + 1 );
		buffer->end += ( blocks * DEFAULT_BUFFER_SIZE );

		buffer->buffer = realloc(buffer->buffer, buffer->end);
	}

	memcpy(&buffer->buffer[buffer->needle], s, n);
	buffer->needle += n;

	return n;
}

FUNCDEBUG static struct HMS_buffer *
HMS_connect (int proto, const char *format, ...)
{
	va_list ap;
	CURL *curl;
	char *url;
	char *quack_token;
	size_t seek;
	size_t token_length;
	struct HMS_buffer *buffer;

#ifdef NO_IPV6
	if (proto == PROTO_V6)
		return NULL;
#endif

	if (! hms_started)
	{
		hms_allow_ipv6 = !M_CheckParm("-noipv6");
		if (curl_global_init(CURL_GLOBAL_ALL) != 0)
		{
			Contact_error();
			Blame("From curl_global_init.\n");
			return NULL;
		}
		else
		{
			atexit(curl_global_cleanup);
			hms_started = 1;
		}
	}

	curl = curl_easy_init();

	if (! curl)
	{
		Contact_error();
		Blame("From curl_easy_init.\n");
		return NULL;
	}

	if (cv_masterserver_token.string && cv_masterserver_token.string[0])
	{
		quack_token = curl_easy_escape(curl, cv_masterserver_token.string, 0);
		token_length = ( sizeof "?token="-1 )+ strlen(quack_token);
	}
	else
	{
		quack_token = NULL;
		token_length = 0;
	}

#ifdef HAVE_THREADS
	I_lock_mutex(&hms_api_mutex);
#endif

	init_user_agent_once();

	seek = strlen(hms_api) + 1;/* + '/' */

	va_start (ap, format);
	url = malloc(seek + vsnprintf(0, 0, format, ap) + token_length + 1);
	va_end (ap);

	sprintf(url, "%s/", hms_api);

#ifdef HAVE_THREADS
	I_unlock_mutex(hms_api_mutex);
#endif

	va_start (ap, format);
	seek += vsprintf(&url[seek], format, ap);
	va_end (ap);

	if (quack_token)
		sprintf(&url[seek], "?token=%s", quack_token);

	CONS_Printf("HMS: connecting '%s'...\n", url);

	buffer = malloc(sizeof *buffer);
	buffer->curl = curl;
	buffer->end = DEFAULT_BUFFER_SIZE;
	buffer->buffer = malloc(buffer->end);
	buffer->needle = 0;

	if (cv_masterserver_debug.value)
	{
		curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
		curl_easy_setopt(curl, CURLOPT_STDERR, logstream);
	}

	if (M_CheckParm("-bindaddr") && M_IsNextParm())
	{
		curl_easy_setopt(curl, CURLOPT_INTERFACE, M_GetNextParm());
	}

	curl_easy_setopt(curl, CURLOPT_URL, url);
	curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

#ifndef NO_IPV6
	if (proto == PROTO_V6)
		curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
	if (proto == PROTO_V4)
#endif
		curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);

	curl_easy_setopt(curl, CURLOPT_TIMEOUT, cv_masterserver_timeout.value);
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HMS_on_read);
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, buffer);

	curl_easy_setopt(curl, CURLOPT_USERAGENT, hms_useragent);

	curl_free(quack_token);
	free(url);

	return buffer;
}

static int
HMS_do (struct HMS_buffer *buffer)
{
	CURLcode cc;
	long status;

	char *p;

	cc = curl_easy_perform(buffer->curl);

	if (cc != CURLE_OK)
	{
		Contact_error();
		Blame(
				"From curl_easy_perform: %s\n",
				curl_easy_strerror(cc)
		);
		return 0;
	}

	buffer->buffer[buffer->needle] = '\0';

	curl_easy_getinfo(buffer->curl, CURLINFO_RESPONSE_CODE, &status);

	if (status != 200)
	{
		p = strchr(buffer->buffer, '\n');

		if (p)
			*p = '\0';

		Contact_error();
		Blame(
				"Master server error %ld: %s%s\n",
				status,
				buffer->buffer,
				( (p) ? "" : " (malformed)" )
		);

		return 0;
	}
	else
		return 1;
}

static void
HMS_end (struct HMS_buffer *buffer)
{
	curl_easy_cleanup(buffer->curl);
	free(buffer->buffer);
	free(buffer);
}

int
HMS_fetch_rooms (int joining, int query_id)
{
	struct HMS_buffer *hms;
	int ok;

	int doing_shit;

	char *id;
	char *title;
	char *room_motd;

	int id_no;

	char *p;
	char *end;

	int i;

	(void)query_id;

	hms = HMS_connect(PROTO_ANY, "rooms");

	if (! hms)
		return 0;

	if (HMS_do(hms))
	{
		doing_shit = 1;

		p = hms->buffer;

		for (i = 0; i < NUM_LIST_ROOMS && ( end = strstr(p, "\n\n\n") );)
		{
			*end = '\0';

			id    = strtok(p, "\n");
			title = strtok(0, "\n");
			room_motd = strtok(0, "");

			if (id && title && room_motd)
			{
				id_no = atoi(id);

				/*
				Don't show the 'All' room if hosting. And it's a hack like this
				because I'm way too lazy to add another feature to the MS.
				*/
				if (joining || id_no != 0)
				{
#ifdef HAVE_THREADS
					I_lock_mutex(&ms_QueryId_mutex);
					{
						if (query_id != ms_QueryId)
							doing_shit = 0;
					}
					I_unlock_mutex(ms_QueryId_mutex);

					if (! doing_shit)
						break;
#endif

					room_list[i].header.buffer[0] = 1;

					room_list[i].id = id_no;
					strlcpy(room_list[i].name, title, sizeof room_list[i].name);
					strlcpy(room_list[i].motd, room_motd, sizeof room_list[i].motd);

					i++;
				}

				p = ( end + 3 );/* skip the three linefeeds */
			}
			else
				break;
		}

		if (doing_shit)
			room_list[i].header.buffer[0] = 0;

		ok = 1;

		if (doing_shit)
		{
#ifdef HAVE_THREADS
			I_lock_mutex(&m_menu_mutex);
#endif
			{
				for (i = 0; room_list[i].header.buffer[0]; i++)
				{
					if(*room_list[i].name != '\0')
					{
						MP_RoomMenu[i+1].text = room_list[i].name;
						roomIds[i] = room_list[i].id;
						MP_RoomMenu[i+1].status = IT_STRING|IT_CALL;
					}
				}
			}
#ifdef HAVE_THREADS
			I_unlock_mutex(m_menu_mutex);
#endif
		}
	}
	else
		ok = 0;

	HMS_end(hms);

	return ok;
}

int
HMS_register (void)
{
	struct HMS_buffer *hms;
	int ok;

	char post[256];

	char *title;

	hms = HMS_connect(PROTO_V4, "rooms/%d/register", cv_masterserver_room_id.value);

	if (! hms)
		return 0;

	title = curl_easy_escape(hms->curl, cv_servername.string, 0);

	snprintf(post, sizeof post,
			"port=%d&"
			"title=%s&"
			"version=%s",

			current_port,

			title,

			SRB2VERSION
	);

	curl_free(title);

	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);

	ok = HMS_do(hms);

	if (ok)
	{
		hms_server_token = strdup(strtok(hms->buffer, "\n"));
	}

	HMS_end(hms);

#ifndef NO_IPV6
	if (!hms_allow_ipv6)
		return ok;

	hms = HMS_connect(PROTO_V6, "rooms/%d/register", cv_masterserver_room_id.value);

	if (! hms)
		return 0;

	curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);

	ok = HMS_do(hms);

	if (ok)
	{
		hms_server_token_ipv6 = strdup(strtok(hms->buffer, "\n"));
	}

	HMS_end(hms);
#endif

	return ok;
}

int
HMS_unlist (void)
{
	struct HMS_buffer *hms;
	int ok = 0;

	if (hms_server_token)
	{
		hms = HMS_connect(PROTO_V4, "servers/%s/unlist", hms_server_token);

		if (! hms)
			return 0;

		curl_easy_setopt(hms->curl, CURLOPT_POST, 1);
		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDSIZE, 0);

		ok = HMS_do(hms);
		HMS_end(hms);

		free(hms_server_token);
	}

#ifndef NO_IPV6
	if (hms_server_token_ipv6 && hms_allow_ipv6)
	{
		hms = HMS_connect(PROTO_V6, "servers/%s/unlist", hms_server_token_ipv6);

		if (! hms)
			return 0;

		curl_easy_setopt(hms->curl, CURLOPT_POST, 1);
		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDSIZE, 0);

		ok = HMS_do(hms);
		HMS_end(hms);

		free(hms_server_token_ipv6);
	}
#endif

	return ok;
}

int
HMS_update (void)
{
	struct HMS_buffer *hms;
	int ok = 0;

	char post[256];

	char *title;

	title = curl_easy_escape(NULL, cv_servername.string, 0);

	snprintf(post, sizeof post,
			"title=%s",
			title
	);

	curl_free(title);

	if (hms_server_token)
	{
		hms = HMS_connect(PROTO_V4, "servers/%s/update", hms_server_token);

		if (! hms)
			return 0;

		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);

		ok = HMS_do(hms);
		HMS_end(hms);
	}

#ifndef NO_IPV6
	if (hms_server_token_ipv6 && hms_allow_ipv6)
	{
		hms = HMS_connect(PROTO_V6, "servers/%s/update", hms_server_token_ipv6);

		if (! hms)
			return ok;

		curl_easy_setopt(hms->curl, CURLOPT_POSTFIELDS, post);

		ok = HMS_do(hms);
		HMS_end(hms);
	}
#endif

	return ok;
}

void
HMS_list_servers (void)
{
	struct HMS_buffer *hms;

	char *list;
	char *p;

	hms = HMS_connect(PROTO_ANY, "servers");

	if (! hms)
		return;

	if (HMS_do(hms))
	{
		list = curl_easy_unescape(hms->curl, hms->buffer, 0, NULL);

		p = strtok(list, "\n");

		while (p != NULL)
		{
			CONS_Printf("\x80%s\n", p);
			p = strtok(NULL, "\n");
		}

		curl_free(list);
	}

	HMS_end(hms);
}

msg_server_t *
HMS_fetch_servers (msg_server_t *list, int room_number, int query_id)
{
	struct HMS_buffer *hms;

	int doing_shit;

	char local_version[9];

	char *room;

	char *address;
	char *port;
	char *title;
	char *version;

	char *end;
	char *section_end;
	char *p;

	int i;

	(void)query_id;

	if (room_number > 0)
	{
		hms = HMS_connect(PROTO_ANY, "rooms/%d/servers", room_number);
	}
	else
		hms = HMS_connect(PROTO_ANY, "servers");

	if (! hms)
		return NULL;

	if (HMS_do(hms))
	{
		doing_shit = 1;

		snprintf(local_version, sizeof local_version,
				"%s",
				SRB2VERSION
		);

		p = hms->buffer;
		i = 0;

		do
		{
			section_end = strstr(p, "\n\n");

			room = strtok(p, "\n");

			p = strtok(0, "");

			if (! p)
				break;

			while (i < MAXSERVERLIST && ( end = strchr(p, '\n') ))
			{
				*end = '\0';

				address = strtok(p, " ");
				port    = strtok(0, " ");
				title   = strtok(0, " ");
				version = strtok(0, "");

				if (address && port && title && version)
				{
#ifdef HAVE_THREADS
					I_lock_mutex(&ms_QueryId_mutex);
					{
						if (query_id != ms_QueryId)
							doing_shit = 0;
					}
					I_unlock_mutex(ms_QueryId_mutex);

					if (! doing_shit)
						break;
#endif

					if (strcmp(version, local_version) == 0)
					{
						strlcpy(list[i].ip,      address, sizeof list[i].ip);
						strlcpy(list[i].port,    port,    sizeof list[i].port);
						strlcpy(list[i].name,    title,   sizeof list[i].name);
						strlcpy(list[i].version, version, sizeof list[i].version);

						list[i].room = atoi(room);

						list[i].header.buffer[0] = 1;

						i++;
					}

					if (end == section_end)/* end of list for this room */
						break;
					else
						p = ( end + 1 );/* skip server delimiter */
				}
				else
				{
					section_end = 0;/* malformed so quit the parsing */
					break;
				}
			}

			if (! doing_shit)
				break;

			p = ( section_end + 2 );
		}
		while (section_end) ;

		if (doing_shit)
			list[i].header.buffer[0] = 0;
	}
	else
		list = NULL;

	HMS_end(hms);

	return list;
}

int
HMS_compare_mod_version (char *buffer, size_t buffer_size)
{
	struct HMS_buffer *hms;
	int ok;

	char *version;
	char *version_name;

	hms = HMS_connect(PROTO_ANY, "versions/%d", MODID);

	if (! hms)
		return 0;

	ok = 0;

	if (HMS_do(hms))
	{
		version      = strtok(hms->buffer, " ");
		version_name = strtok(0, "\n");

		if (version && version_name)
		{
			if (atoi(version) != MODVERSION)
			{
				strlcpy(buffer, version_name, buffer_size);
				ok = 1;
			}
			else
				ok = -1;
		}
	}

	HMS_end(hms);

	return ok;
}

void
HMS_set_api (char *api)
{
#ifdef HAVE_THREADS
	I_lock_mutex(&hms_api_mutex);
#endif
	{
		free(hms_api);
		hms_api = api;
	}
#ifdef HAVE_THREADS
	I_unlock_mutex(hms_api_mutex);
#endif
}

#endif/*MASTERSERVER*/

static void
MasterServer_Debug_OnChange (void)
{
#ifdef MASTERSERVER
	/* TODO: change to 'latest-log.txt' for log files revision. */
	if (cv_masterserver_debug.value)
		CONS_Printf("Master server debug messages will appear in log.txt\n");
#endif
}
