/************************************************************************
 * UnralIRCd JSON functions, src/json.c
 * (C) 2021-.. Bram Matthys (Syzop) and the UnrealIRCd Team
 * License: GPLv2 or later
 */

#include "unrealircd.h"

/** @file
 * @brief JSON functions - used for logging and RPC.
 */

/** Are we currently in the logging code? */
int log_json_filter = 0;

/** Calculate expansion of a JSON string thanks to double escaping.
 * orig => JSON => IRC
 *    " => \"   => \\"
 *    \ => \\   => \\\\
 */
int json_dump_string_length(const char *s)
{
	int len = 0;
	for (; *s; s++)
	{
		if (*s == '\\')
			len += 4;
		else if (*s == '"')
			len += 3;
		else
			len++;
	}
	return len;
}

/** Convert a regular string value to a JSON string.
 * In UnrealIRCd, this must be used instead of json_string()
 * as we may use non-UTF8 sequences. Also, this takes care
 * of using json_null() if the string was NULL, which is
 * usually what we want as well.
 * @param s	Input string
 * @returns a json string value or json null value.
 */
json_t *json_string_unreal(const char *s)
{
	char buf1[512], buf2[512];
	char *verified_s;
	const char *stripped;

	if (s == NULL)
		return json_null();

	if (log_json_filter)
	{
		stripped = StripControlCodesEx(s, buf1, sizeof(buf1), UNRL_STRIP_LOW_ASCII|UNRL_STRIP_KEEP_LF);
		verified_s = unrl_utf8_make_valid(buf1, buf2, sizeof(buf2), 0);
	} else {
		verified_s = unrl_utf8_make_valid(s, buf2, sizeof(buf2), 0);
	}

	return json_string(verified_s);
}

const char *json_object_get_string(json_t *j, const char *name)
{
	json_t *v = json_object_get(j, name);
	return v ? json_string_value(v) : NULL;
}

/** Get integer value of a JSON object.
 * @param j		The JSON object that should contain a 'name' item
 * @param name		The item to search for
 * @param default_value	The value to return when the JSON object 'name' is not found
 *			or not an integer.
 * @returns The integer value, or default_value if the object does not exist or is not an integer.
 */
int json_object_get_integer(json_t *j, const char *name, int default_value)
{
	json_t *v = json_object_get(j, name);
	if (!v || !json_is_integer(v))
		return default_value;
	return json_integer_value(v);
}

int json_object_get_boolean(json_t *j, const char *name, int default_value)
{
	json_t *v = json_object_get(j, name);
	if (!v)
		return default_value;
	if (json_is_true(v))
		return 1;
	return 0;
}

#define json_string __BAD___DO__NOT__USE__JSON__STRING__PLZ

const char *json_get_value(json_t *t)
{
	static char buf[32];

	if (json_is_string(t))
		return json_string_value(t);

	if (json_is_integer(t))
	{
		snprintf(buf, sizeof(buf), "%lld", (long long)json_integer_value(t));
		return buf;
	}

	if (json_is_boolean(t))
	{
		if (json_is_true(t))
			return "true";
		return "false";
	}

	if (json_is_array(t))
		return "<array>";

	return NULL;
}

json_t *json_timestamp(time_t v)
{
	const char *ts = timestamp_iso8601(v);
	if (ts)
		return json_string_unreal(ts);
	return json_null();
}

const char *timestamp_iso8601_now(void)
{
	struct timeval t;
	struct tm *tm;
	time_t sec;
	static char buf[64];

	gettimeofday(&t, NULL);
	sec = t.tv_sec;
	tm = gmtime(&sec);

	snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
		tm->tm_year + 1900,
		tm->tm_mon + 1,
		tm->tm_mday,
		tm->tm_hour,
		tm->tm_min,
		tm->tm_sec,
		(int)(t.tv_usec / 1000));

	return buf;
}

const char *timestamp_iso8601(time_t v)
{
	struct tm *tm;
	static char buf[64];

	if (v == 0)
		return NULL;

	tm = gmtime(&v);

	if (tm == NULL)
		return NULL;

	snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
		tm->tm_year + 1900,
		tm->tm_mon + 1,
		tm->tm_mday,
		tm->tm_hour,
		tm->tm_min,
		tm->tm_sec,
		0);

	return buf;
}

void json_expand_client_security_groups(json_t *parent, Client *client)
{
	SecurityGroup *s;
	json_t *child = json_array();
	json_object_set_new(parent, "security-groups", child);

	/* We put known-users or unknown-users at the beginning.
	 * The latter is special and doesn't actually exist
	 * in the linked list, hence the special code here,
	 * and again later in the for loop to skip it.
	 */
	if (user_allowed_by_security_group_name(client, "known-users"))
		json_array_append_new(child, json_string_unreal("known-users"));
	else
		json_array_append_new(child, json_string_unreal("unknown-users"));

	for (s = securitygroups; s; s = s->next)
		if (strcmp(s->name, "known-users") && user_allowed_by_security_group(client, s))
			json_array_append_new(child, json_string_unreal(s->name));
}

/* detail=0:	only name, id
 * detail=1:	only name, id, hostname, ip, details, geoip
 * detail=2:	everything, except 'channels'
 * detail=3:	everything, with 'channels' being a max 384 character string (meant for JSON logging only)
 * detail=4:	everything, with 'channels' object (full).
 */
void json_expand_client(json_t *j, const char *key, Client *client, int detail)
{
	char buf[BUFSIZE+1];
	json_t *child;
	json_t *user = NULL;
	time_t ts;

	if (key)
	{
		child = json_object();
		json_object_set_new(j, key, child);
	} else {
		child = j;
	}

	/* First the information that is available for ALL client types: */
	json_object_set_new(child, "name", json_string_unreal(client->name));
	json_object_set_new(child, "id", json_string_unreal(client->id));

	if (detail == 0)
		return;

	/* hostname is available for all, it just depends a bit on whether it is DNS or IP */
	if (client->user && *client->user->realhost)
		json_object_set_new(child, "hostname", json_string_unreal(client->user->realhost));
	else if (client->local && *client->local->sockhost)
		json_object_set_new(child, "hostname", json_string_unreal(client->local->sockhost));
	else
		json_object_set_new(child, "hostname", json_string_unreal(GetIP(client)));

	/* same for ip, is there for all (well, some services pseudo-users may not have one) */
	json_object_set_new(child, "ip", json_string_unreal(client->ip));
	/* client.details is always available: it is nick!user@host, nick@host, server@host
	 * server@ip, or just server.
	 */
	if (client->user)
	{
		if (IsUser(client) || !MyConnect(client))
		{
			/* Post-handshake, after register_user(), it is easy: */
			snprintf(buf, sizeof(buf), "%s!%s@%s", client->name, client->user->username, client->user->realhost);
		} else
		{
			/* In the handshake, more possibilities (ident could still be ongoing)
			 * and more speculative (a later class block may want to ignore ident,
			 * but we don't know that, so we assume that is not the case).
			 */
			const char *ident;
			char temp[USERLEN+1];
			if (IDENT_CHECK)
			{
				if (IsIdentSuccess(client))
				{
					/* ident succeeded means: use the identd and no ~ prefix */
					ident = client->ident;
				} else {
					/* ident check failed means ~ prefix */
					snprintf(temp, sizeof(temp), "~%s", client->user->username);
					ident = temp;
				}
			} else {
				/* no ident check means no ~ prefix */
				ident = client->user->username;
			}
			snprintf(buf, sizeof(buf), "%s!%s@%s", client->name, ident, client->user->realhost);
		}
		json_object_set_new(child, "details", json_string_unreal(buf));
	} else if (client->ip) {
		if (*client->name)
			snprintf(buf, sizeof(buf), "%s@%s", client->name, client->ip);
		else
			snprintf(buf, sizeof(buf), "[%s]", client->ip);
		json_object_set_new(child, "details", json_string_unreal(buf));
	} else {
		json_object_set_new(child, "details", json_string_unreal(client->name));
	}

	if (detail < 2)
	{
		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT, client, detail, child);
		return;
	}

	if (client->local && client->local->listener)
		json_object_set_new(child, "server_port", json_integer(client->local->listener->port));
	if (client->local && client->local->port)
		json_object_set_new(child, "client_port", json_integer(client->local->port));
	if ((ts = get_creationtime(client)))
		json_object_set_new(child, "connected_since", json_timestamp(ts));
	if (client->local && client->local->idle_since)
		json_object_set_new(child, "idle_since", json_timestamp(client->local->idle_since));

	if (client->user)
	{
		char buf[512];
		const char *str;
		/* client.user */
		user = json_object();
		json_object_set_new(child, "user", user);

		json_object_set_new(user, "username", json_string_unreal(client->user->username));
		if (!BadPtr(client->info))
			json_object_set_new(user, "realname", json_string_unreal(client->info));
		if (has_user_mode(client, 'x') && client->user->virthost && strcmp(client->user->virthost, client->user->realhost))
			json_object_set_new(user, "vhost", json_string_unreal(client->user->virthost));
		if (*client->user->cloakedhost)
			json_object_set_new(user, "cloakedhost", json_string_unreal(client->user->cloakedhost));
		if (client->uplink)
			json_object_set_new(user, "servername", json_string_unreal(client->uplink->name));
		if (IsLoggedIn(client))
			json_object_set_new(user, "account", json_string_unreal(client->user->account));
		json_object_set_new(user, "reputation", json_integer(GetReputation(client)));
		json_expand_client_security_groups(user, client);

		/* user modes and snomasks */
		get_usermode_string_r(client, buf, sizeof(buf));
		json_object_set_new(user, "modes", json_string_unreal(buf+1));
		if (client->user->snomask)
			json_object_set_new(user, "snomasks", json_string_unreal(client->user->snomask));

		/* if oper then we can possibly expand a bit more */
		str = get_operlogin(client);
		if (str)
			json_object_set_new(user, "operlogin", json_string_unreal(str));
		str = get_operclass(client);
		if (str)
			json_object_set_new(user, "operclass", json_string_unreal(str));
		/* For detail>2 we will include the channels.
		 * Even if the user is on 0 channels we include "channels":[]
		 * so it is clear that the user is on 0 channels and it is
		 * not because of low detail level that channels are skipped.
		 */
		if (detail > 2)
		{
			Membership *m;
			int cnt = 0;
			int len = 0;
			json_t *channels = json_array();
			json_object_set_new(user, "channels", channels);

			if (detail == 3)
			{
				/* Short format, mainly for JSON logging */
				for (m = client->user->channel; m; m = m->next)
				{
					len += json_dump_string_length(m->channel->name);
					if (len > 384)
					{
						/* Truncated */
						json_array_append_new(channels, json_string_unreal("..."));
						break;
					}
					json_array_append_new(channels, json_string_unreal(m->channel->name));
				}
			} else {
				/* Long format for JSON-RPC */
				for (m = client->user->channel; m; m = m->next)
				{
					json_t *e = json_object();
					json_object_set_new(e, "name", json_string_unreal(m->channel->name));
					if (*m->member_modes)
						json_object_set_new(e, "level", json_string_unreal(m->member_modes));
					json_array_append_new(channels, e);
				}
			}
		}
		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_USER, client, detail, child, user);
	} else
	if (IsMe(client))
	{
		json_t *server = json_object();
		json_t *features;

		/* client.server */
		json_object_set_new(child, "server", server);

		if (!BadPtr(client->info))
			json_object_set_new(server, "info", json_string_unreal(client->info));
		json_object_set_new(server, "num_users", json_integer(client->server->users));
		json_object_set_new(server, "boot_time", json_timestamp(client->server->boottime));

		/* client.server.features */
		features = json_object();
		json_object_set_new(server, "features", features);
		if (!BadPtr(client->server->features.software))
		{
			char buf[256];
			snprintf(buf, sizeof(buf), "UnrealIRCd-%s", buildid);
			json_object_set_new(features, "software", json_string_unreal(buf));
		}
		json_object_set_new(features, "protocol", json_integer(UnrealProtocol));
		if (!BadPtr(client->server->features.usermodes))
			json_object_set_new(features, "usermodes", json_string_unreal(umodestring));

		/* client.server.features.chanmodes (array) */
		{
			int i;
			char buf[512];
			json_t *chanmodes = json_array();
			json_object_set_new(features, "chanmodes", chanmodes);
			/* first one is special - wait.. is this still the case? lol. */
			snprintf(buf, sizeof(buf), "%s%s", CHPAR1, EXPAR1);
			json_array_append_new(chanmodes, json_string_unreal(buf));
			for (i=1; i < 4; i++)
				json_array_append_new(chanmodes, json_string_unreal(extchmstr[i]));
		}
		if (!BadPtr(client->server->features.nickchars))
			json_object_set_new(features, "nick_character_sets", json_string_unreal(charsys_get_current_languages()));
		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_SERVER, client, detail, child, server);
	} else
	if (IsServer(client) && client->server)
	{
		/* client.server */

		/* Whenever a server is expanded, which is rare,
		 * we should probably expand as much as info as possible:
		 */
		json_t *server = json_object();
		json_t *features;

		/* client.server */
		json_object_set_new(child, "server", server);
		if (!BadPtr(client->info))
			json_object_set_new(server, "info", json_string_unreal(client->info));
		if (client->uplink)
			json_object_set_new(server, "uplink", json_string_unreal(client->uplink->name));
		json_object_set_new(server, "num_users", json_integer(client->server->users));
		json_object_set_new(server, "boot_time", json_timestamp(client->server->boottime));
		json_object_set_new(server, "synced", json_boolean(client->server->flags.synced));
		json_object_set_new(server, "ulined", json_boolean(IsULine(client)));

		/* client.server.features */
		features = json_object();
		json_object_set_new(server, "features", features);
		if (!BadPtr(client->server->features.software))
			json_object_set_new(features, "software", json_string_unreal(client->server->features.software));
		json_object_set_new(features, "protocol", json_integer(client->server->features.protocol));
		if (!BadPtr(client->server->features.usermodes))
			json_object_set_new(features, "usermodes", json_string_unreal(client->server->features.usermodes));
		if (!BadPtr(client->server->features.chanmodes[0]))
		{
			/* client.server.features.chanmodes (array) */
			int i;
			json_t *chanmodes = json_array();
			json_object_set_new(features, "chanmodes", chanmodes);
			for (i=0; i < 4; i++)
				json_array_append_new(chanmodes, json_string_unreal(client->server->features.chanmodes[i]));
		}
		if (!BadPtr(client->server->features.nickchars))
			json_object_set_new(features, "nick_character_sets", json_string_unreal(client->server->features.nickchars));
		RunHook(HOOKTYPE_JSON_EXPAND_CLIENT_SERVER, client, detail, child, server);
	}
	RunHook(HOOKTYPE_JSON_EXPAND_CLIENT, client, detail, child);
}

void json_expand_channel_ban(json_t *child, const char *banlist_name, Ban *banlist)
{
	Ban *ban;
	json_t *list, *e;

	list = json_array();
	json_object_set_new(child, banlist_name, list);
	for (ban = banlist; ban; ban = ban->next)
	{
		e = json_object();
		json_array_append_new(list, e);
		json_object_set_new(e, "name", json_string_unreal(ban->banstr));
		json_object_set_new(e, "set_by", json_string_unreal(ban->who));
		json_object_set_new(e, "set_at", json_timestamp(ban->when));
	}
}


/* detail=1 adds bans, ban_exemptions and invite_exceptions
 * detail=2 adds members
 * detail=3+ makes the members more detailed
 */
void json_expand_channel(json_t *j, const char *key, Channel *channel, int detail)
{
	char mode1[512], mode2[512], modes[512];
	json_t *child;

	if (key)
	{
		child = json_object();
		json_object_set_new(j, key, child);
	} else {
		child = j;
	}

	json_object_set_new(child, "name", json_string_unreal(channel->name));
	if (detail == 0)
		return;

	json_object_set_new(child, "creation_time", json_timestamp(channel->creationtime));
	json_object_set_new(child, "num_users", json_integer(channel->users));
	if (channel->topic)
	{
		json_object_set_new(child, "topic", json_string_unreal(channel->topic));
		json_object_set_new(child, "topic_set_by", json_string_unreal(channel->topic_nick));
		json_object_set_new(child, "topic_set_at", json_timestamp(channel->topic_time));
	}

	/* Add "mode" too */
	channel_modes(NULL, mode1, mode2, sizeof(mode1), sizeof(mode2), channel, 0);
	if (*mode2)
	{
		snprintf(modes, sizeof(modes), "%s %s", mode1+1, mode2);
		json_object_set_new(child, "modes", json_string_unreal(modes));
	} else {
		json_object_set_new(child, "modes", json_string_unreal(mode1+1));
	}

	if (detail > 1)
	{
		json_expand_channel_ban(child, "bans", channel->banlist);
		json_expand_channel_ban(child, "ban_exemptions", channel->exlist);
		json_expand_channel_ban(child, "invite_exceptions", channel->invexlist);
	}

	if (detail >= 3)
	{
		Member *u;
		json_t *list = json_array();
		json_object_set_new(child, "members", list);

		for (u = channel->members; u; u = u->next)
		{
			json_t *e = json_object();
			if (*u->member_modes)
				json_object_set_new(e, "level", json_string_unreal(u->member_modes));
			json_expand_client(e, NULL, u->client, detail-3);
			json_array_append_new(list, e);
		}
	}

	// Possibly later: If detail is set to 1 then expand more...
	RunHook(HOOKTYPE_JSON_EXPAND_CHANNEL, channel, detail, child);
}

void json_expand_tkl(json_t *root, const char *key, TKL *tkl, int detail)
{
	char buf[BUFSIZE];
	json_t *j;

	if (key)
	{
		j = json_object();
		json_object_set_new(root, key, j);
	} else {
		j = root;
	}

	json_object_set_new(j, "type", json_string_unreal(tkl_type_config_string(tkl))); // Eg 'kline'
	json_object_set_new(j, "type_string", json_string_unreal(tkl_type_string(tkl))); // Eg 'Soft K-Line'
	json_object_set_new(j, "set_by", json_string_unreal(tkl->set_by));
	json_object_set_new(j, "set_at", json_timestamp(tkl->set_at));
	json_object_set_new(j, "expire_at", json_timestamp(tkl->expire_at));
	*buf = '\0';
	short_date(tkl->set_at, buf);
	strlcat(buf, " GMT", sizeof(buf));
	json_object_set_new(j, "set_at_string", json_string_unreal(buf));
	if (tkl->expire_at <= 0)
	{
		json_object_set_new(j, "expire_at_string", json_string_unreal("Never"));
		json_object_set_new(j, "duration_string", json_string_unreal("permanent"));
	} else {
		*buf = '\0';
		short_date(tkl->expire_at, buf);
		strlcat(buf, " GMT", sizeof(buf));
		json_object_set_new(j, "expire_at_string", json_string_unreal(buf));
		json_object_set_new(j, "duration_string", json_string_unreal(pretty_time_val_r(buf, sizeof(buf), tkl->expire_at - tkl->set_at)));
	}
	json_object_set_new(j, "set_at_delta", json_integer(TStime() - tkl->set_at));
	if (tkl->flags & TKL_FLAG_CONFIG)
		json_object_set_new(j, "set_in_config", json_boolean(1));
	if (TKLIsServerBan(tkl))
	{
		json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0)));
		json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.serverban->reason));
	} else
	if (TKLIsNameBan(tkl))
	{
		json_object_set_new(j, "name", json_string_unreal(tkl->ptr.nameban->name));
		json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.nameban->reason));
	} else
	if (TKLIsBanException(tkl))
	{
		json_object_set_new(j, "name", json_string_unreal(tkl_uhost(tkl, buf, sizeof(buf), 0)));
		json_object_set_new(j, "reason", json_string_unreal(tkl->ptr.banexception->reason));
		json_object_set_new(j, "exception_types", json_string_unreal(tkl->ptr.banexception->bantypes));
	} else
	if (TKLIsSpamfilter(tkl))
	{
		if (tkl->ptr.spamfilter->match->str)
		{
			json_object_set_new(j, "name", json_string_unreal(tkl->ptr.spamfilter->match->str));
			json_object_set_new(j, "match_type", json_string_unreal(unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type)));
		}
		if (tkl->ptr.spamfilter->prettyrule)
			json_object_set_new(j, "rule", json_string_unreal(tkl->ptr.spamfilter->prettyrule));
		json_object_set_new(j, "ban_action", json_string_unreal(ban_actions_to_string(tkl->ptr.spamfilter->action)));
		json_object_set_new(j, "ban_duration", json_integer(tkl->ptr.spamfilter->tkl_duration));
		json_object_set_new(j, "ban_duration_string", json_string_unreal(pretty_time_val_r(buf, sizeof(buf), tkl->ptr.spamfilter->tkl_duration)));
		json_object_set_new(j, "spamfilter_targets", json_string_unreal(spamfilter_target_inttostring(tkl->ptr.spamfilter->target)));
		json_object_set_new(j, "reason", json_string_unreal(unreal_decodespace(tkl->ptr.spamfilter->tkl_reason)));
		json_object_set_new(j, "hits", json_integer(tkl->ptr.spamfilter->hits));
		json_object_set_new(j, "hits_except", json_integer(tkl->ptr.spamfilter->hits_except));
	}
}
