/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include <pango/pango.h>

#include <birb.h>

#include "purpleircv3messagehandlers.h"

#include "purpleircv3connection.h"
#include "purpleircv3core.h"
#include "purpleircv3ctcphandlers.h"

/******************************************************************************
 * Helpers
 *****************************************************************************/
static PurpleConversationMember *
purple_ircv3_add_contact_to_conversation(PurpleContact *contact,
                                         PurpleConversation *conversation,
                                         gboolean announce)
{
	PurpleContactInfo *info = PURPLE_CONTACT_INFO(contact);
	PurpleConversationMember *member = NULL;
	PurpleConversationMembers *members = NULL;

	members = purple_conversation_get_members(conversation);

	member = purple_conversation_members_find_member(members, info);
	if(!PURPLE_IS_CONVERSATION_MEMBER(member)) {
		char *message = NULL;

		if(announce) {
			message = g_strdup_printf(_("%s has joined %s"),
			                          purple_contact_info_get_sid(info),
			                          purple_conversation_get_title_for_display(conversation));
		}

		member = purple_conversation_members_add_member(members, info,
		                                                announce, message);
		g_clear_pointer(&message, g_free);
	}

	return member;
}

static PurpleBadge *
purple_ircv3_badge_for_prefix(const char prefix) {
	PurpleBadge *badge = NULL;
	PurpleBadgeManager *manager = NULL;
	const char *description = NULL;
	const char *id = NULL;
	const char *mnemonic = NULL;
	int priority = 0;

	manager = purple_badge_manager_get_default();

	switch(prefix) {
	case '~':
		description = _("Founder");
		id = "ircv3-badge-founder";
		priority = 500;
		mnemonic = "~";
		break;
	case '&':
		description = _("Protected");
		id = "ircv3-badge-protected";
		priority = 400;
		mnemonic = "&";
		break;
	case '@':
		description = _("Operator");
		id = "ircv3-badge-operator";
		priority = 300;
		mnemonic = "@";
		break;
	case '%':
		description = _("Half Operator");
		id = "ircv3-badge-halfop";
		priority = 200;
		mnemonic = "%%";
		break;
	case '+':
		description = _("Voice");
		id = "ircv3-badge-voice";
		priority = 100;
		mnemonic = "+";
		break;
	}

	if(id == NULL) {
		return NULL;
	}

	badge = purple_badge_manager_find(manager, id);
	if(!PURPLE_IS_BADGE(badge)) {
		badge = purple_badge_new(id, priority, id, mnemonic);
		if(!birb_str_is_empty(description)) {
			purple_badge_set_description(badge, description);
		}

		purple_badge_manager_add(manager, badge);

		/* This is transfer none and the manager has a reference, so we unref
		 * our reference to the badge.
		 */
		g_object_unref(badge);
	}

	return badge;
}

static void
purple_ircv3_add_badge_to_member(PurpleConversationMember *member,
                                 IbisClient *client, const char prefix)
{
	PurpleBadge *badge = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member));
	g_return_if_fail(IBIS_IS_CLIENT(client));

	badge = purple_ircv3_badge_for_prefix(prefix);
	if(PURPLE_IS_BADGE(badge)) {
		PurpleBadges *badges = NULL;

		badges = purple_conversation_member_get_badges(member);
		purple_badges_add_badge(badges, badge);
	}
}

static void
purple_ircv3_remove_badge_from_member(PurpleConversationMember *member,
                                      IbisClient *client, const char prefix)
{
	PurpleBadge *badge = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member));
	g_return_if_fail(IBIS_IS_CLIENT(client));

	badge = purple_ircv3_badge_for_prefix(prefix);
	if(PURPLE_IS_BADGE(badge)) {
		PurpleBadges *badges = purple_conversation_member_get_badges(member);

		/* I know this is gross, but we need to rework badges here and I wanted
		 * to get this done sooner rather than later.
		 * -- gk 2025-03-14
		 */
		purple_badges_remove_badge(badges, purple_badge_get_id(badge));
	}
}

static void
purple_ircv3_add_badges_to_member(PurpleConversationMember *member,
                                  IbisClient *client, const char *nick)
{
	char *prefixes = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member));
	g_return_if_fail(IBIS_IS_CLIENT(client));

	prefixes = ibis_client_get_source_prefix(client, nick);
	if(birb_str_is_empty(prefixes)) {
		return;
	}

	for(guint i = 0; prefixes[i] != '\0'; i++) {
		purple_ircv3_add_badge_to_member(member, client, prefixes[i]);
	}

	g_free(prefixes);
}

/******************************************************************************
 * General Commands
 *****************************************************************************/
gboolean
purple_ircv3_message_handler_away(G_GNUC_UNUSED IbisClient *client,
                                  G_GNUC_UNUSED const char *command,
                                  IbisMessage *message,
                                  gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurplePresence *presence = NULL;
	GStrv params = NULL;
	char *away_message = NULL;

	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         message);

	presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact));

	/* Figure out if we have a message. */
	params = ibis_message_get_params(message);
	if(params != NULL) {
		away_message = g_strjoinv(" ", params);
	}

	/* We have a message so we need to set it and possibly set the presence to
	 * away.
	 */
	if(!birb_str_is_empty(away_message)) {
		purple_presence_set_message(presence, away_message);

		purple_presence_set_primitive(presence,
		                              PURPLE_PRESENCE_PRIMITIVE_AWAY);
	} else {
		purple_presence_set_message(presence, NULL);

		purple_presence_set_primitive(presence,
		                              PURPLE_PRESENCE_PRIMITIVE_AVAILABLE);
	}

	g_clear_pointer(&away_message, g_free);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_join(G_GNUC_UNUSED IbisClient *client,
                                  G_GNUC_UNUSED const char *command,
                                  IbisMessage *message,
                                  gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurpleConversation *conversation = NULL;
	GStrv params = NULL;
	const char *conversation_name = NULL;

	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         message);

	params = ibis_message_get_params(message);

	/* A normal join command has the channel as the only parameter. */
	if(g_strv_length(params) == 1) {
		conversation_name = params[0];
	} else {
		/* TODO: write this to join to the status window saying we didn't know
		 * how to parse it.
		 */

		return TRUE;
	}

	conversation = purple_ircv3_connection_find_or_create_conversation(connection,
	                                                                   conversation_name,
	                                                                   FALSE);

	/* When we join a channel, the server sends a join message back with us as
	 * the source to let us know that it was joined successfully. When we
	 * receive this message, we need to refresh the channel.
	 */
	if(purple_contact_is_own_account(contact)) {
		purple_ircv3_connection_refresh_channel(connection, conversation);
	}

	purple_ircv3_add_contact_to_conversation(contact, conversation, TRUE);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_part(G_GNUC_UNUSED IbisClient *client,
                                  G_GNUC_UNUSED const char *command,
                                  IbisMessage *message,
                                  gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleAccount *account = NULL;
	PurpleContact *contact = NULL;
	PurpleConversation *conversation = NULL;
	PurpleConversationManager *manager = NULL;
	PurpleConversationMembers *members = NULL;
	GStrv params = NULL;
	guint n_params = 0;
	char *reason = NULL;
	const char *conversation_name = NULL;

	params = ibis_message_get_params(message);
	n_params = g_strv_length(params);
	if(n_params == 0) {
		/* TODO: mention unparsable message in the status window. */
		return TRUE;
	}

	/* TODO: The spec says servers _SHOULD NOT_ send a comma separated list of
	 * channels, but we should support that at some point just in case.
	 */
	conversation_name = params[0];

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	manager = purple_conversation_manager_get_default();
	conversation = purple_conversation_manager_find_with_id(manager, account,
	                                                        conversation_name);

	if(!PURPLE_IS_CONVERSATION(conversation)) {
		/* TODO: write status message unknown channel. */

		return TRUE;
	}

	members = purple_conversation_get_members(conversation);

	/* We do want to find or create the contact, even on a part, because we
	 * could have connected to a BNC and we weren't told about the contact yet.
	 */
	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         message);

	/* If a part message was given, join the remaining parameters with a space.
	 */
	if(n_params > 1) {
		char *part_message = NULL;

		part_message = g_strjoinv(" ", params + 1);
		reason = g_strdup_printf(_("%s has left %s (%s)"),
		                         purple_contact_info_get_sid(PURPLE_CONTACT_INFO(contact)),
		                         purple_conversation_get_title_for_display(conversation),
		                         part_message);
	} else {
		reason = g_strdup_printf(_("%s has left %s"),
		                         purple_contact_info_get_sid(PURPLE_CONTACT_INFO(contact)),
		                         purple_conversation_get_title_for_display(conversation));
	}

	purple_conversation_members_remove_member(members,
	                                          PURPLE_CONTACT_INFO(contact),
	                                          TRUE, reason);

	g_clear_pointer(&reason, g_free);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_namreply(IbisClient *client,
                                      G_GNUC_UNUSED const char *command,
                                      IbisMessage *message, gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleConversation *conversation = NULL;
	GStrv params = NULL;
	GStrv nicks = NULL;
	const char *target = NULL;

	params = ibis_message_get_params(message);
	if(params == NULL) {
		g_warning("namreply received with no parameters");

		return FALSE;
	}

	if(g_strv_length(params) != 4) {
		char *body = g_strjoinv(" ", params);
		g_warning("unknown namreply format: '%s'", body);
		g_free(body);

		return FALSE;
	}

	/* params[0] holds nick of the user and params[1] holds the channel type
	 * (public/private) but we don't care about either of these.
	 */

	target = params[2];
	if(!ibis_client_is_channel(client, target)) {
		g_warning("received namreply for '%s' which is not a channel.",
		          target);

		return FALSE;
	}

	conversation = purple_ircv3_connection_find_or_create_conversation(connection,
	                                                                   target,
	                                                                   FALSE);

	/* Split the last parameter on space to get a list of all the nicks. */
	nicks = g_strsplit(params[3], " ", -1);
	if(nicks != NULL) {
		PurpleAccount *account = NULL;
		PurpleConnection *purple_connection = NULL;
		PurpleContactManager *manager = purple_contact_manager_get_default();
		PurpleConversationMembers *existing_members = NULL;
		PurpleConversationMembers *new_members = NULL;
		const char *active_nick = NULL;

		purple_connection = PURPLE_CONNECTION(connection);
		account = purple_connection_get_account(purple_connection);

		existing_members = purple_conversation_get_members(conversation);
		new_members = purple_conversation_members_new();

		/* We don't want to add ourselves and we're already in the list. */
		active_nick = ibis_client_get_active_nick(client);

		for(guint i = 0; i < g_strv_length(nicks); i++) {
			PurpleContact *contact = NULL;
			PurpleConversationMember *member = NULL;
			const char *nick = nicks[i];
			char *stripped = NULL;

			stripped = ibis_client_strip_source_prefix(client, nick);
			if(birb_str_equal(stripped, active_nick)) {
				g_free(stripped);

				continue;
			}

			contact = purple_contact_manager_find_with_id(manager, account,
			                                              stripped);
			if(!PURPLE_IS_CONTACT(contact)) {
				contact = purple_contact_new(account, stripped);
				purple_contact_manager_add(manager, contact);
			}

			/* Check if the member is already in the existing members list.
			 * This can happen if the user sends a NAMES command and surely
			 * other ways are possible. */
			member = purple_conversation_members_find_member(existing_members,
			                                                 PURPLE_CONTACT_INFO(contact));

			/* If the member doesn't exist, add them. */
			if(!PURPLE_IS_CONVERSATION_MEMBER(member)) {
				member = purple_conversation_members_add_member(new_members,
				                                                PURPLE_CONTACT_INFO(contact),
				                                                FALSE, NULL);
			}

			purple_ircv3_add_badges_to_member(member, client, nick);

			g_free(stripped);
			g_clear_object(&contact);
		}

		purple_conversation_members_extend(existing_members, new_members);
	}

	g_strfreev(nicks);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_tagmsg(IbisClient *client,
                                    G_GNUC_UNUSED const char *command,
                                    IbisMessage *ibis_message, gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurpleConversation *conversation = NULL;
	GStrv params = NULL;
	IbisTags *tags = NULL;
	const char *target = NULL;
	const char *value = NULL;

	params = ibis_message_get_params(ibis_message);
	tags = ibis_message_get_tags(ibis_message);

	if(params == NULL) {
		g_warning("tagmsg received with no parameters");

		return FALSE;
	}

	if(g_strv_length(params) != 1) {
		char *body = g_strjoinv(" ", params);
		g_warning("unknown tagmsg message format: '%s'", body);
		g_free(body);

		return FALSE;
	}

	/* Find or create the contact. */
	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         ibis_message);

	/* Find or create the conversation. */
	target = params[0];
	if(!ibis_client_is_channel(client, target)) {
		target = purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact));
	}

	conversation = purple_ircv3_connection_find_or_create_conversation(connection,
	                                                                   target,
	                                                                   TRUE);

	purple_ircv3_add_contact_to_conversation(contact, conversation, FALSE);

	/* Handle typing notifications. */
	value = ibis_tags_lookup(tags, IBIS_TAG_TYPING);
	if(!birb_str_is_empty(value)) {
		PurpleConversationMember *member = NULL;
		PurpleConversationMembers *members = NULL;
		PurpleTypingState state = PURPLE_TYPING_STATE_NONE;
		guint timeout = 1;

		members = purple_conversation_get_members(conversation);
		member = purple_conversation_members_find_member(members,
		                                                 PURPLE_CONTACT_INFO(contact));

		if(birb_str_equal(value, IBIS_TYPING_ACTIVE)) {
			state = PURPLE_TYPING_STATE_TYPING;
			timeout = 6;
		} else if(birb_str_equal(value, IBIS_TYPING_PAUSED)) {
			state = PURPLE_TYPING_STATE_PAUSED;
			timeout = 30;
		}

		purple_conversation_member_set_typing_state(member, state, timeout);
	}

	return TRUE;
}

gboolean
purple_ircv3_message_handler_privmsg(IbisClient *client, const char *command,
                                     IbisMessage *ibis_message, gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurpleConversation *conversation = NULL;
	PurpleConversationMember *member = NULL;
	PurpleMessage *message = NULL;
	GDateTime *dt = NULL;
	IbisCTCPMessage *ctcp_message = NULL;
	IbisTags *tags = NULL;
	GStrv params = NULL;
	const char *target = NULL;
	gboolean announce = TRUE;

	params = ibis_message_get_params(ibis_message);
	ctcp_message = ibis_message_get_ctcp_message(ibis_message);
	tags = ibis_message_get_tags(ibis_message);

	if(params == NULL) {
		g_warning("privmsg received with no parameters");

		return FALSE;
	}

	if(g_strv_length(params) != 2) {
		char *body = g_strjoinv(" ", params);
		g_warning("unknown privmsg message format: '%s'", body);
		g_free(body);

		return FALSE;
	}

	/* Find or create the contact. */
	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         ibis_message);

	target = params[0];
	if(!ibis_client_is_channel(client, target)) {
		target = purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact));
	}

	if(!ibis_client_get_registered(client)) {
		conversation = purple_ircv3_connection_get_status_conversation(connection);
		announce = FALSE;
	}

	if(IBIS_IS_CTCP_MESSAGE(ctcp_message)) {
		if(ibis_ctcp_message_is_command(ctcp_message, IBIS_CTCP_ACTION)) {
			GStrv ctcp_params = NULL;
			char *ctcp_body = NULL;
			char *stripped = NULL;

			if(!PURPLE_IS_CONVERSATION(conversation)) {
				conversation = purple_ircv3_connection_find_or_create_conversation(connection,
				                                                                   target,
				                                                                   TRUE);
			}

			member = purple_ircv3_add_contact_to_conversation(contact,
			                                                  conversation,
			                                                  announce);

			ctcp_params = ibis_ctcp_message_get_params(ctcp_message);
			ctcp_body = g_strjoinv(" ", ctcp_params);
			stripped = ibis_formatting_strip(ctcp_body);
			g_free(ctcp_body);

			message = purple_message_new(member, stripped);
			g_free(stripped);

			purple_message_set_action(message, TRUE);
		} else {
			char *body = NULL;

			announce = FALSE;

			if(!PURPLE_IS_CONVERSATION(conversation)) {
				conversation = purple_ircv3_connection_get_status_conversation(connection);
			}
			member = purple_ircv3_add_contact_to_conversation(contact,
			                                                  conversation,
			                                                  announce);

			body = g_strdup_printf(_("requested CTCP '%s' (to %s) from %s"),
			                       ibis_ctcp_message_get_command(ctcp_message),
			                       params[0],
			                       purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact)));

			message = purple_message_new(member, body);
			purple_message_set_event(message, TRUE);
			g_free(body);
		}
	} else {
		PangoAttrList *attrs = NULL;
		char *stripped = NULL;

		/* If we received this message before registration has completed,
		 * conversation will be set to the status conversation.
		 */
		if(!PURPLE_IS_CONVERSATION(conversation)) {
			conversation = purple_ircv3_connection_find_or_create_conversation(connection,
			                                                                   target,
			                                                                   TRUE);
		}

		member = purple_ircv3_add_contact_to_conversation(contact,
		                                                  conversation,
		                                                  announce);

		stripped = ibis_formatting_parse(params[1], &attrs);
		message = purple_message_new(member, stripped);
		purple_message_set_attributes(message, attrs);
		g_clear_pointer(&stripped, g_free);
		g_clear_pointer(&attrs, pango_attr_list_unref);
	}

	if(birb_str_equal(command, IBIS_MSG_NOTICE)) {
		purple_message_set_notice(message, TRUE);
	}

	if(IBIS_IS_TAGS(tags)) {
		const char *raw_tag = NULL;

		/* Grab the msgid if one was provided. */
		raw_tag = ibis_tags_lookup(tags, "msgid");
		if(!birb_str_is_empty(raw_tag)) {
			purple_message_set_id(message, raw_tag);
		}

		/* Determine the timestamp of the message. */
		raw_tag = ibis_tags_lookup(tags, "time");
		if(!birb_str_is_empty(raw_tag)) {
			GTimeZone *tz = g_time_zone_new_utc();

			dt = g_date_time_new_from_iso8601(raw_tag, tz);
			g_time_zone_unref(tz);

			purple_message_set_timestamp(message, dt);
			g_date_time_unref(dt);
		}
	}

	/* If the server didn't provide a time, use the current local time. */
	if(dt == NULL) {
		purple_message_set_timestamp_now(message);
	}

	purple_conversation_write_message(conversation, message);

	g_clear_object(&message);

	/* If the message contained a CTCP message and was a PRIVMSG, we then need
	 * to attempt to handle it.
	 */
	if(IBIS_IS_CTCP_MESSAGE(ctcp_message) &&
	   birb_str_equal(command, IBIS_MSG_PRIVMSG))
	{
		purple_ircv3_ctcp_handler(connection, client, ibis_message);
	}

	return TRUE;
}

gboolean
purple_ircv3_message_handler_topic(G_GNUC_UNUSED IbisClient *client,
                                   const char *command, IbisMessage *message,
                                   gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleConversation *conversation = NULL;
	GStrv params = NULL;
	const char *channel = NULL;
	const char *topic = NULL;
	guint n_params = 0;

	params = ibis_message_get_params(message);
	n_params = g_strv_length(params);

	if(birb_str_equal(command, IBIS_MSG_TOPIC)) {
		if(n_params != 2) {
			g_message("received TOPIC with %u parameters, expected 2",
			          n_params);

			return FALSE;
		}

		channel = params[0];
		topic = params[1];
	} else if(birb_str_equal(command, IBIS_RPL_NOTOPIC)) {
		if(n_params != 3) {
			g_message("received RPL_NOTOPIC with %u parameters, expected 3",
			          n_params);

			return FALSE;
		}

		channel = params[1];
		topic = "";
	} else if(birb_str_equal(command, IBIS_RPL_TOPIC)) {
		if(n_params != 3) {
			g_message("received RPL_TOPIC with %u parameters, expected 3",
			          n_params);

			return FALSE;
		}

		channel = params[1];
		topic = params[2];
	} else {
		g_message("unexpected command %s", command);

		return FALSE;
	}

	conversation = purple_ircv3_connection_find_or_create_conversation(connection,
	                                                                   channel,
	                                                                   TRUE);
	if(!PURPLE_IS_CONVERSATION(conversation)) {
		g_message("failed to find or create channel '%s'", channel);

		return FALSE;
	}

	purple_conversation_set_topic(conversation, topic);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_whotopic(G_GNUC_UNUSED IbisClient *client,
                                      G_GNUC_UNUSED const char *command,
                                      IbisMessage *message,
                                      gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleConversation *conversation = NULL;
	PurpleContact *contact = NULL;
	GDateTime *timestamp = NULL;
	GObject *obj = NULL;
	GStrv params = NULL;
	char *nick = NULL;
	guint n_params = 0;

	params = ibis_message_get_params(message);
	n_params = g_strv_length(params);

	/* The 333 whotopic message has parameters of `client channel source
	 * timestamp`.
	 */
	if(n_params != 4) {
		g_message("received 333 with %u parameters, expected 4", n_params);

		return FALSE;
	}

	conversation = purple_ircv3_connection_find_or_create_conversation(connection,
	                                                                   params[1],
	                                                                   TRUE);
	if(!PURPLE_IS_CONVERSATION(conversation)) {
		g_message("failed to find or create channel '%s'", params[1]);

		return FALSE;
	}

	ibis_source_parse(params[2], &nick, NULL, NULL);
	contact = purple_ircv3_connection_find_or_create_contact_from_nick(connection,
	                                                                   nick);
	g_free(nick);
	timestamp = g_date_time_new_from_unix_utc(atoi(params[3]));

	obj = G_OBJECT(conversation);
	g_object_freeze_notify(obj);
	purple_conversation_set_topic_author(conversation,
	                                     PURPLE_CONTACT_INFO(contact));
	purple_conversation_set_topic_updated(conversation, timestamp);
	g_object_thaw_notify(obj);

	g_date_time_unref(timestamp);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_channel_url(G_GNUC_UNUSED IbisClient *client,
                                         G_GNUC_UNUSED const char *command,
                                         IbisMessage *message,
                                         gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleConversation *conversation = NULL;
	GStrv params = NULL;
	guint n_params = 0;

	params = ibis_message_get_params(message);
	n_params = g_strv_length(params);

	/* The 328 numeric has parameters of `client channel :url`. */
	if(n_params != 3) {
		g_message("received 328 with %u parameters, expected 3", n_params);

		return FALSE;
	}

	conversation = purple_ircv3_connection_find_or_create_conversation(connection,
	                                                                   params[1],
	                                                                   TRUE);
	if(!PURPLE_IS_CONVERSATION(conversation)) {
		g_message("failed to find or create channel '%s'", params[1]);

		return FALSE;
	}

	purple_conversation_set_url(conversation, params[2]);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_quit(G_GNUC_UNUSED IbisClient *client,
                                  G_GNUC_UNUSED const char *command,
                                  IbisMessage *ibis_message,
                                  gpointer data)
{
	PurpleIRCv3Connection *v3_connection = data;
	PurpleAccount *account = NULL;
	PurpleConnection *connection = data;
	PurpleContact *contact = NULL;
	PurpleContactInfo *info = NULL;
	PurpleConversationManager *manager = NULL;
	PurplePresence *presence = NULL;
	GListModel *conversations = NULL;
	GStrv params = NULL;
	guint n_params = 0;
	char *message = NULL;
	char *reason = NULL;
	guint n_items = 0;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	contact = purple_ircv3_connection_find_or_create_contact(v3_connection,
	                                                         ibis_message);
	info = PURPLE_CONTACT_INFO(contact);

	if(n_params > 0) {
		reason = g_strjoinv(" ", params);
		message = g_strdup_printf("%s has quit (%s)",
		                          purple_contact_info_get_sid(info),
		                          reason);
	} else {
		message = g_strdup_printf("%s has quit",
		                          purple_contact_info_get_sid(info));
	}

	/* Update the presence to offline and if they provided a quit message, set
	 * it as the presence message.
	 */
	presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact));
	purple_presence_set_message(presence, reason);
	purple_presence_set_primitive(presence, PURPLE_PRESENCE_PRIMITIVE_OFFLINE);

	manager = purple_conversation_manager_get_default();
	account = purple_connection_get_account(connection);
	conversations = purple_conversation_manager_get_all_for_account(manager,
	                                                                account);
	n_items = g_list_model_get_n_items(G_LIST_MODEL(conversations));
	for(guint i = 0; i < n_items; i++) {
		PurpleConversation *conversation = NULL;
		PurpleConversationMembers *members = NULL;

		conversation = g_list_model_get_item(G_LIST_MODEL(conversations), i);

		members = purple_conversation_get_members(conversation);
		purple_conversation_members_remove_member(members, info, TRUE,
		                                          message);

		g_clear_object(&conversation);
	}

	g_clear_object(&conversations);
	g_clear_pointer(&message, g_free);
	g_clear_pointer(&reason, g_free);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_nick(G_GNUC_UNUSED IbisClient *client,
                                  G_GNUC_UNUSED const char *command,
                                  IbisMessage *ibis_message,
                                  gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurpleContactInfo *info = NULL;
	IbisTags *tags = NULL;
	GStrv params = NULL;
	guint n_params = 0;
	char *new_source = NULL;
	char *user = NULL;
	char *host = NULL;
	const char *source = NULL;
	const char *nick = NULL;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	if(n_params != 1) {
		g_message("received NICK with %d params, expected 1", n_params);
		return FALSE;
	}

	nick = params[0];
	source = ibis_message_get_source(ibis_message);
	ibis_source_parse(source, NULL, &user, &host);
	new_source = ibis_source_serialize(nick, user, host);
	g_clear_pointer(&user, g_free);
	g_clear_pointer(&host, g_free);

	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         ibis_message);
	info = PURPLE_CONTACT_INFO(contact);

	/* If the account tag doesn't exist, we need to update the id property of
	 * the contact.
	 */
	tags = ibis_message_get_tags(ibis_message);
	if(!ibis_tags_exists(tags, IBIS_TAG_ACCOUNT)) {
		purple_contact_info_set_id(info, nick);
	}

	purple_contact_info_set_display_name(info, nick);
	purple_contact_info_set_sid(info, new_source);
	g_clear_pointer(&new_source, g_free);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_error(IbisClient *client,
                                   G_GNUC_UNUSED const char *command,
                                   IbisMessage *ibis_message,
                                   gpointer data)
{
	PurpleIRCv3Connection *v3_connection = data;
	GError *error = NULL;
	GStrv params = NULL;
	guint n_params = 0;
	char *reason = NULL;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	if(n_params > 0) {
		reason = g_strjoinv(" ", params);
	} else {
		reason = g_strdup(_("unknown error"));
	}

	purple_ircv3_connection_write_status_message(v3_connection, ibis_message,
	                                             TRUE, FALSE);

	error = g_error_new_literal(PURPLE_IRCV3_DOMAIN, 0, reason);
	g_clear_pointer(&reason, g_free);

	ibis_client_stop(client, error);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_wallops(G_GNUC_UNUSED IbisClient *client,
                                     G_GNUC_UNUSED const char *command,
                                     IbisMessage *ibis_message,
                                     gpointer data)
{
	PurpleIRCv3Connection *v3_connection = data;
	PurpleAccount *account = NULL;
	PurpleConnection *connection = data;
	PurpleContact *contact = NULL;
	PurpleContactInfo *info = NULL;
	PurpleNotification *notification = NULL;
	PurpleNotificationManager *manager = NULL;
	GStrv params = NULL;
	char *wallops_title = NULL;
	guint n_params = 0;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	if(n_params != 1) {
		g_message("received WALLOPS with %u params, expected 1", n_params);
		return FALSE;
	}

	contact = purple_ircv3_connection_find_or_create_contact(v3_connection,
	                                                         ibis_message);
	info = PURPLE_CONTACT_INFO(contact);

	wallops_title = g_strdup_printf(_("WALLOPS from %s"),
	                                purple_contact_info_get_name_for_display(info));
	notification = purple_notification_new(NULL, wallops_title);
	g_free(wallops_title);

	account = purple_connection_get_account(connection);
	purple_notification_set_account(notification, account);
	purple_notification_set_subtitle(notification, params[0]);
	purple_notification_set_icon_name(notification, PURPLE_IRCV3_ICON_NAME);

	manager = purple_notification_manager_get_default();
	purple_notification_manager_add(manager, notification);
	g_clear_object(&notification);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_kick(G_GNUC_UNUSED IbisClient *client,
                                  G_GNUC_UNUSED const char *command,
                                  IbisMessage *ibis_message,
                                  gpointer data)
{
	PurpleIRCv3Connection *v3_connection = data;
	PurpleAccount *account = NULL;
	PurpleContact *kicker = NULL;
	PurpleContact *kickee = NULL;
	PurpleContactInfo *me = NULL;
	PurpleConversation *conversation = NULL;
	GStrv params = NULL;
	char *reason = NULL;
	const char *kickee_id = NULL;
	const char *me_id = NULL;
	guint n_params = 0;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	if(n_params < 2) {
		g_message("received KICK with %u params, need at least 2", n_params);
		return FALSE;
	}

	account = purple_connection_get_account(PURPLE_CONNECTION(v3_connection));
	me = purple_account_get_contact_info(account);
	me_id = purple_contact_info_get_id(me);

	kicker = purple_ircv3_connection_find_or_create_contact(v3_connection,
	                                                        ibis_message);

	conversation = purple_ircv3_connection_find_or_create_conversation(v3_connection,
	                                                                   params[0],
	                                                                   TRUE);

	kickee = purple_ircv3_connection_find_or_create_contact_from_nick(v3_connection,
	                                                                  params[1]);
	kickee_id = purple_contact_info_get_id(PURPLE_CONTACT_INFO(kickee));

	if(n_params > 2) {
		reason = g_strjoinv(" ", params + 2);
	} else {
		reason = g_strdup(_("no reason"));
	}

	if(birb_str_equal(kickee_id, me_id)) {
		GError *error = NULL;

		error = g_error_new(PURPLE_IRCV3_DOMAIN, 0,
		                    _("You were kicked from %s by %s: %s"),
		                    params[0],
		                    purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(kicker)),
		                    reason);
		purple_conversation_set_error(conversation, error);
		g_clear_error(&error);

		purple_conversation_set_online(conversation, FALSE);
	} else {
		PurpleConversationMembers *members = NULL;
		char *contents = NULL;

		contents = g_strdup_printf(_("%s kicked %s from %s: %s"),
		                           purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(kicker)),
		                           purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(kickee)),
		                           params[0],
		                           reason);

		members = purple_conversation_get_members(conversation);
		purple_conversation_members_remove_member(members,
		                                          PURPLE_CONTACT_INFO(kickee),
		                                          TRUE,
		                                          contents);
		g_free(contents);
	}

	g_free(reason);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_mode(IbisClient *client,
                                  G_GNUC_UNUSED const char *command,
                                  IbisMessage *ibis_message,
                                  gpointer data)
{
	PurpleIRCv3Connection *v3_connection = data;
	PurpleContact *contact = NULL;
	PurpleConversation *conversation = NULL;
	PurpleConversationMember *author = NULL;
	PurpleConversationMembers *members = NULL;
	PurpleMessage *message = NULL;
	IbisModeChange *mode_changes = NULL;
	GError *error = NULL;
	GStrv params = NULL;
	char *contents = NULL;
	char *parts = NULL;
	guint n_params = 0;
	guint n_mode_changes = 0;
	const char *target = NULL;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	if(n_params < 2) {
		g_message("received MODE with %u params, need at least 2", n_params);
		return FALSE;
	}

	target = params[0];
	if(!ibis_client_is_channel(client, target)) {
		return FALSE;
	}

	conversation = purple_ircv3_connection_find_or_create_conversation(v3_connection,
	                                                                   target,
	                                                                   TRUE);
	members = purple_conversation_get_members(conversation);

	contact = purple_ircv3_connection_find_or_create_contact(v3_connection,
	                                                         ibis_message);
	author = purple_conversation_members_find_or_add_member(members,
								PURPLE_CONTACT_INFO(contact),
								FALSE, NULL);

	mode_changes = ibis_client_parse_mode_string(client, params[1], params + 2,
	                                             &n_mode_changes, &error);
	if(error != NULL) {
		g_warning("failed to parse mode string: %s", error->message);

		g_clear_error(&error);

		return FALSE;
	}

	for(guint i = 0; i < n_mode_changes; i++) {
		PurpleContact *subject = NULL;
		PurpleConversationMember *member = NULL;
		IbisModeChange change = mode_changes[i];
		char prefix = '\0';

		prefix = ibis_client_get_prefix_for_mode(client, change.mode);
		if(prefix == '\0') {
			continue;
		}

		subject = purple_ircv3_connection_find_or_create_contact_from_nick(v3_connection,
		                                                                   change.parameter);
		member = purple_conversation_members_find_or_add_member(members,
									PURPLE_CONTACT_INFO(subject),
									FALSE, NULL);

		if(change.add) {
			purple_ircv3_add_badge_to_member(member, client, prefix);
		} else {
			purple_ircv3_remove_badge_from_member(member, client, prefix);
		}
	}

	parts = g_strjoinv(" ", params + 1);
	contents = g_strdup_printf(_("mode %s"), parts);
	g_free(parts);

	message = g_object_new(
		PURPLE_TYPE_MESSAGE,
		"author", author,
		"contents", contents,
		"event", TRUE,
		NULL);
	g_free(contents);

	purple_conversation_write_message(conversation, message);
	g_clear_object(&message);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_whoreply(IbisClient *client,
                                      G_GNUC_UNUSED const char *command,
                                      IbisMessage *ibis_message,
                                      gpointer data)
{
	PurpleIRCv3Connection *v3_connection = data;
	PurpleContact *contact = NULL;
	PurplePresence *presence = NULL;
	GStrv params = NULL;
	char *sid = NULL;
	guint n_params = 0;
	const char *flags = NULL;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	/* A standard RPL_WHOREPLY has 7 parameters:
	 *
	 * <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <realname>
	 *
	 * We ignore hopcount and realname for now as we don't have a good use case
	 * to use it.
	 */
	if(n_params < 7) {
		g_message("received RPL_WHOREPLY with %u params, need at least 7",
		          n_params);
		return FALSE;
	}

	/* Find the contact and update everything for it. */
	contact = purple_ircv3_connection_find_or_create_contact_from_nick(v3_connection,
	                                                                   params[5]);

	sid = g_strdup_printf("%s!%s@%s", params[5], params[2], params[3]);
	purple_contact_info_set_sid(PURPLE_CONTACT_INFO(contact), sid);

	/* Process the flags starting with presence. */
	flags = params[6];

	presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact));
	if(flags[0] == 'G') {
		purple_presence_set_primitive(presence,
		                              PURPLE_PRESENCE_PRIMITIVE_AWAY);
	} else {
		purple_presence_set_primitive(presence,
		                              PURPLE_PRESENCE_PRIMITIVE_AVAILABLE);
	}

	/* Increment flags to the next value. */
	flags += 1;

	/* Check if a server admin. */
	if(flags[0] == '*') {
		/* We'll need to create badge for this at some point */

		flags += 1;
	}

	/* If we got a channel update the member's prefix (badges). */
	if(!birb_str_equal(params[1], "*")) {
		PurpleConversation *conversation = NULL;
		PurpleConversationMembers *members = NULL;
		PurpleConversationMember *member = NULL;

		conversation = purple_ircv3_connection_find_or_create_conversation(v3_connection,
		                                                                   params[1],
		                                                                   TRUE);
		members = purple_conversation_get_members(conversation);

		member = purple_conversation_members_find_or_add_member(members,
									PURPLE_CONTACT_INFO(contact),
									FALSE, NULL);

		/* Now run through the remaining flags looking for channel member
		 * prefixes and adding badges for the ones we find.
		 */
		for(guint i = 0; flags[i] != '\0'; i++) {
			/* We abuse the fact that if a badge can't be found it is ignored
			 * to avoid having to check the prefix before adding the badge.
			 */
			purple_ircv3_add_badge_to_member(member, client, flags[i]);
		}
	}

	return TRUE;
}

gboolean
purple_ircv3_message_handler_nowaway(G_GNUC_UNUSED IbisClient *client,
                                     G_GNUC_UNUSED const char *command,
                                     IbisMessage *ibis_message,
                                     gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurplePresence *presence = NULL;
	GStrv params = NULL;
	guint n_params = 0;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	if(n_params < 1) {
		g_message("received RPL_NOWAWAY with %u params, need at least 1", n_params);
		return FALSE;
	}

	contact = purple_ircv3_connection_find_or_create_contact_from_nick(connection,
	                                                                   params[0]);
	presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact));
	purple_presence_set_primitive(presence,
	                              PURPLE_PRESENCE_PRIMITIVE_AWAY);

	return TRUE;
}

gboolean
purple_ircv3_message_handler_unaway(G_GNUC_UNUSED IbisClient *client,
                                    G_GNUC_UNUSED const char *command,
                                    IbisMessage *ibis_message,
                                    gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurplePresence *presence = NULL;
	GStrv params = NULL;
	guint n_params = 0;

	params = ibis_message_get_params(ibis_message);
	n_params = g_strv_length(params);

	if(n_params < 1) {
		g_message("received RPL_UNAWAY with %u params, need at least 1", n_params);
		return FALSE;
	}

	contact = purple_ircv3_connection_find_or_create_contact_from_nick(connection,
	                                                                   params[0]);
	presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact));
	purple_presence_set_primitive(presence, PURPLE_PRESENCE_PRIMITIVE_AVAILABLE);

	return TRUE;
}
