/*
 * Purple Zulip - Zulip Protocol Plugin
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple Zulip 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 plugin 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 plugin 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 plugin; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include <libsoup/soup.h>

#include <birb.h>

#include "purplezulipclient.h"

#include "purplezulipconstants.h"

struct _PurpleZulipClient {
	GObject parent;

	char *server_uri;
	GCancellable *cancellable;

	PurpleAccount *account;
	SoupSession *session;
	char *authorization;
};

enum {
	PROP_0,
	PROP_ACCOUNT,
	PROP_CANCELLABLE,
	PROP_SERVER_URI,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

#define PURPLE_ZULIP_CLIENT_ERROR (g_quark_from_static_string("purple-zulip-client"))

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
purple_zulip_client_set_account(PurpleZulipClient *client,
                                PurpleAccount *account)
{
	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));

	if(g_set_object(&client->account, account)) {
		g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ACCOUNT]);
	}
}

static void
purple_zulip_client_set_cancellable(PurpleZulipClient *client,
                                    GCancellable *cancellable)
{
	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));

	if(g_set_object(&client->cancellable, cancellable)) {
		g_object_notify_by_pspec(G_OBJECT(client),
		                         properties[PROP_CANCELLABLE]);
	}
}

static void
purple_zulip_client_set_server_uri(PurpleZulipClient *client,
                                   const char *server_uri)
{
	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));

	if(g_set_str(&client->server_uri, server_uri)) {
		g_object_notify_by_pspec(G_OBJECT(client),
		                         properties[PROP_SERVER_URI]);
	}
}

static void
purple_zulip_client_update_logger(PurpleZulipClient *client,
                                  GSettings *settings,
                                  const char *key)
{
	if(g_settings_get_boolean(settings, key)) {
		SoupLogger *logger = NULL;

		if(soup_session_has_feature(client->session, SOUP_TYPE_LOGGER)) {
			return;
		}

		logger = soup_logger_new(SOUP_LOGGER_LOG_BODY);

		soup_session_add_feature(client->session,
		                         SOUP_SESSION_FEATURE(logger));
		g_clear_object(&logger);
	} else {
		soup_session_remove_feature_by_type(client->session, SOUP_TYPE_LOGGER);
	}
}

/*
 * purple_zulip_client_new_api_message:
 * @method: the HTTP method
 * @path: the path relative to /api
 * @form: (transfer full) (nullable): the optional form data
 *
 * Creates a new API message with the given parameters.
 *
 * Returns: (transfer full): The new Soup message.
 *
 * Since: 3.0
 */
static SoupMessage *
purple_zulip_client_new_api_message(PurpleZulipClient *client,
                                    const char *method, const char *path,
                                    char *form)
{
	SoupMessage *message = NULL;
	SoupMessageHeaders *headers = NULL;
	char *full_uri = NULL;

	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), NULL);

	full_uri = g_build_filename(client->server_uri, "api", path, NULL);

	if(!birb_str_is_empty(form)) {
		if(birb_str_equal(method, SOUP_METHOD_DELETE)) {
			/* soup_message_new_from_encoded_form doesn't support DELETE so we
			 * do it ourselves. An issue has been opened at
			 * https://gitlab.gnome.org/GNOME/libsoup/-/issues/513 for this.
			 */
			GBytes *body = NULL;

			message = soup_message_new(method, full_uri);
			body = g_bytes_new_take(form, strlen(form));
			soup_message_set_request_body_from_bytes(message,
			                                         SOUP_FORM_MIME_TYPE_URLENCODED,
			                                         body);
			g_bytes_unref(body);
		} else {
			message = soup_message_new_from_encoded_form(method, full_uri,
			                                             form);
		}
	} else {
		message = soup_message_new(method, full_uri);
	}
	g_free(full_uri);

	/* Add any headers we might need. */
	headers = soup_message_get_request_headers(message);

	if(!birb_str_is_empty(client->authorization)) {
		soup_message_headers_replace(headers, "Authorization",
		                             client->authorization);
	}

	return message;
}

static gpointer
purple_zulip_client_parse_response(SoupSession *session, GType response_type,
                                   GAsyncResult *result, GError **error)
{
	PurpleZulipBasicResponse *basic_response = NULL;
	GError *local_error = NULL;
	GBytes *response = NULL;
	JsonNode *node = NULL;
	JsonParser *parser = NULL;
	SoupMessage *message = NULL;
	SoupStatus status = SOUP_STATUS_NONE;
	gpointer ret = NULL;
	gconstpointer raw = NULL;
	gsize length = 0;
	gboolean success = FALSE;

	response = soup_session_send_and_read_finish(session, result,
	                                             &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		g_clear_pointer(&response, g_bytes_unref);

		return NULL;
	}

	message = soup_session_get_async_result_message(session, result);

	parser = json_parser_new_immutable();

	raw = g_bytes_get_data(response, &length);

	success = json_parser_load_from_data(parser, raw, (gssize)length,
	                                     &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		g_clear_object(&parser);
		g_clear_pointer(&response, g_bytes_unref);

		return NULL;
	}

	if(!success) {
		g_set_error_literal(error, PURPLE_ZULIP_CLIENT_ERROR, 0,
		                    "failed to parser json");

		g_clear_object(&parser);
		g_clear_pointer(&response, g_bytes_unref);

		return NULL;
	}

	/* We should be done with bytes now, so clear it. */
	g_clear_pointer(&response, g_bytes_unref);

	node = json_parser_get_root(parser);

	/* Deserialize the basic response. */
	basic_response = json_boxed_deserialize(PURPLE_ZULIP_TYPE_BASIC_RESPONSE,
	                                        node);
	if(basic_response == NULL) {
		g_set_error_literal(error, PURPLE_ZULIP_CLIENT_ERROR, 0,
		                    "failed to parse response from the server");

		g_clear_object(&parser);

		return NULL;
	}

	status = soup_message_get_status(message);
	if(!SOUP_STATUS_IS_SUCCESSFUL(status)) {
		g_set_error(error, PURPLE_ZULIP_CLIENT_ERROR, status,
		            "%s: %s", basic_response->code, basic_response->message);

		g_clear_object(&parser);
		g_clear_pointer(&basic_response, purple_zulip_basic_response_free);

		return NULL;
	}

	if(response_type != G_TYPE_NONE) {
		/* We got a successful response, so parse the type that was requested. */
		ret = json_boxed_deserialize(response_type, node);
	}

	/* Cleanup. */
	g_clear_object(&parser);
	g_clear_pointer(&basic_response, purple_zulip_basic_response_free);

	return ret;
}

static void
purple_zulip_client_update_basic_authorization(PurpleZulipClient *client,
                                               const char *username,
                                               const char *password)
{
	char *authorization = NULL;

	if(!birb_str_is_empty(username) || !birb_str_is_empty(password)) {
		GString *str = NULL;
		char *encoded = NULL;

		str = g_string_new(username);
		g_string_append(str, ":");
		if(!birb_str_is_empty(password)) {
			g_string_append(str, password);
		}

		encoded = g_base64_encode((gconstpointer)str->str, str->len);

		authorization = g_strdup_printf("Basic %s", encoded);

		g_string_free(str, TRUE);
		g_free(encoded);
	}

	g_clear_pointer(&client->authorization, birb_str_wipe);

	/* This will be the duped string from above or NULL. */
	client->authorization = authorization;
}

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
purple_zulip_client_developer_mode_changed_cb(GSettings *settings,
                                              const char *key,
                                              gpointer data)
{
	purple_zulip_client_update_logger(data, settings, key);
}

static void
purple_zulip_client_soup_fetch_api_key_cb(GObject *source,
                                          GAsyncResult *result,
                                          gpointer data)
{
	PurpleZulipBasicResponse *response = NULL;
	SoupSession *session = SOUP_SESSION(source);
	GError *error = NULL;
	GTask *task = data;

	response = purple_zulip_client_parse_response(session,
	                                              PURPLE_ZULIP_TYPE_FETCH_API_KEY_RESPONSE,
	                                              result, &error);
	if(error != NULL) {
		g_task_return_error(task, error);
	} else {
		g_task_return_pointer(task, response,
		                      (GDestroyNotify)purple_zulip_fetch_api_key_response_free);
	}

	g_clear_object(&task);
}

static void
purple_zulip_client_get_server_settings_cb(GObject *source,
                                           GAsyncResult *result,
                                           gpointer data)
{
	PurpleZulipBasicResponse *response = NULL;
	SoupSession *session = SOUP_SESSION(source);
	GError *error = NULL;
	GTask *task = data;

	response = purple_zulip_client_parse_response(session,
	                                              PURPLE_ZULIP_TYPE_SERVER_SETTINGS_RESPONSE,
	                                              result, &error);
	if(error != NULL) {
		g_task_return_error(task, error);
	} else {
		g_task_return_pointer(task, response,
		                      (GDestroyNotify)purple_zulip_server_settings_response_free);
	}

	g_clear_object(&task);
}

static void
purple_zulip_client_remember_password_cb(GObject *source, GAsyncResult *result,
                                         gpointer data)
{
	PurpleZulipClient *client = data;
	GError *error = NULL;
	gboolean success = FALSE;
	const char *account_name = NULL;

	success = purple_credential_manager_write_password_finish(PURPLE_CREDENTIAL_MANAGER(source),
	                                                          result, &error);

	account_name = purple_account_get_name(client->account);

	if(error != NULL) {
		g_warning("failed to save password for account %s: %s",
		          account_name, error->message);

		g_clear_error(&error);

		return;
	}

	if(!success) {
		g_warning("failed to save password for account %s: unknown error",
		          account_name);
	} else {
		g_message("successfully saved the password for account %s",
		          account_name);
	}
}

/******************************************************************************
 * Credential Login
 *****************************************************************************/
static void
purple_zulip_client_fetch_api_key_cb(GObject *source, GAsyncResult *result,
                                     gpointer data)
{
	PurpleZulipClient *client = NULL;
	PurpleZulipFetchApiKeyResponse *response = NULL;
	PurpleContactInfo *info = NULL;
	GError *error = NULL;
	GTask *task = data;
	gpointer remember = NULL;

	client = PURPLE_ZULIP_CLIENT(source);
	response = purple_zulip_client_fetch_api_key_finish(client, result,
	                                                    &error);
	if(error != NULL) {
		g_task_return_error(task, error);

		g_clear_pointer(&response, purple_zulip_fetch_api_key_response_free);
		g_clear_object(&task);

		return;
	}

	remember = g_task_get_task_data(G_TASK(result));
	if(GPOINTER_TO_INT(remember) == TRUE) {
		PurpleCore *core = NULL;
		PurpleCredentialManager *manager = NULL;

		purple_account_set_remember_password(client->account, TRUE);

		core = purple_core_get_default();
		manager = purple_core_get_credential_manager(core);
		purple_credential_manager_write_password_async(manager,
		                                               client->account,
		                                               response->api_key,
		                                               NULL,
		                                               purple_zulip_client_remember_password_cb,
		                                               client);
	} else {
		purple_account_set_remember_password(client->account, FALSE);
	}

	info = purple_account_get_contact_info(client->account);
	purple_contact_info_set_id(info, response->user_id);
	purple_contact_info_set_email(info, response->email);

	purple_zulip_client_update_basic_authorization(client, response->email,
	                                               response->api_key);

	g_task_return_boolean(task, TRUE);

	g_clear_pointer(&response, purple_zulip_fetch_api_key_response_free);
	g_clear_object(&task);
}

static void
purple_zulip_client_account_request_password_cb(GObject *source,
                                                GAsyncResult *result,
                                                gpointer data)
{
	PurpleZulipClient *client = NULL;
	PurpleAccount *account = PURPLE_ACCOUNT(source);
	PurpleAccountSettings *settings = NULL;
	GError *error = NULL;
	GTask *task = data;
	char *password = NULL;
	const char *email = NULL;
	gboolean remember = FALSE;

	password = purple_account_request_password_finish(account, result, &error);
	if(error != NULL) {
		g_task_return_error(task, error);

		g_clear_object(&task);

		g_clear_pointer(&password, birb_str_wipe);

		return;
	}

	if(birb_str_is_empty(password)) {
		g_task_return_new_error_literal(task, PURPLE_ZULIP_CLIENT_ERROR, 0,
		                                _("no password was provided"));

		g_clear_object(&task);

		g_clear_pointer(&password, birb_str_wipe);

		return;
	}

	client = g_task_get_task_data(task);
	remember = purple_account_request_password_result_get_remember(account,
	                                                               result);
	settings = purple_account_get_settings(account);
	email = purple_account_settings_get_string(settings,
	                                           PURPLE_ZULIP_SETTING_EMAIL,
	                                           NULL);

	purple_zulip_client_fetch_api_key_async(client, email, password, remember,
	                                        purple_zulip_client_fetch_api_key_cb,
	                                        task);

	g_clear_pointer(&password, birb_str_wipe);
}

static void
purple_zulip_client_login_read_password_cb(GObject *source,
                                           GAsyncResult *result,
                                           gpointer data)
{
	PurpleZulipClient *client = NULL;
	PurpleAccountSettings *settings = NULL;
	PurpleCredentialManager *manager = PURPLE_CREDENTIAL_MANAGER(source);
	GError *error = NULL;
	GTask *task = data;
	char *password = NULL;
	const char *email = NULL;

	password = purple_credential_manager_read_password_finish(manager, result,
	                                                          &error);
	if(error != NULL) {
		g_task_return_error(task, error);

		g_clear_object(&task);
		g_clear_pointer(&password, g_free);

		return;
	}

	client = g_task_get_task_data(task);

	if(birb_str_is_empty(password)) {
		purple_account_request_password_async(client->account, NULL,
		                                      purple_zulip_client_account_request_password_cb,
		                                      task);

		g_clear_pointer(&password, g_free);

		return;
	}

	/* Update the authorization header. */
	settings = purple_account_get_settings(client->account);

	email = purple_account_settings_get_string(settings,
	                                           PURPLE_ZULIP_SETTING_EMAIL,
	                                           NULL);

	purple_zulip_client_update_basic_authorization(client, email, password);

	g_clear_pointer(&password, g_free);

	g_task_return_boolean(task, TRUE);
	g_clear_object(&task);
}

static void
purple_zulip_client_login_check_server_settings_cb(GObject *source,
                                                   GAsyncResult *result,
                                                   gpointer data)
{
	PurpleZulipClient *client = PURPLE_ZULIP_CLIENT(source);
	PurpleZulipServerSettingsResponse *response = NULL;
	PurpleCredentialManager *manager = NULL;
	GError *error = NULL;
	GTask *task = data;

	response = purple_zulip_client_get_server_settings_finish(client, result,
	                                                          &error);
	if(error != NULL || response == NULL) {
		if(error != NULL) {
			g_task_return_error(task, error);
		} else {
			g_task_return_new_error_literal(task, PURPLE_ZULIP_CLIENT_ERROR, 0,
			                                _("unknown error"));
		}

		g_clear_pointer(&response, purple_zulip_server_settings_response_free);
		g_clear_object(&task);

		return;
	}

	if(response->is_incompatible) {
		g_task_return_new_error_literal(task, PURPLE_ZULIP_CLIENT_ERROR, 0,
		                                _("the server thinks this client is "
		                                  "incompatible."));

		g_clear_pointer(&response, purple_zulip_server_settings_response_free);
		g_clear_object(&task);

		return;
	}

	if(!response->email_auth_enabled) {
		g_task_return_new_error_literal(task, PURPLE_ZULIP_CLIENT_ERROR, 0,
		                                _("the server does not support email "
		                                  "authentication"));

		g_clear_pointer(&response, purple_zulip_server_settings_response_free);
		g_clear_object(&task);

		return;
	}

	/* The server checks out, lets try to grab the password from the credential
	 * manager.
	 */
	manager = purple_core_get_credential_manager(purple_core_get_default());
	purple_credential_manager_read_password_async(manager,
	                                              client->account,
	                                              client->cancellable,
	                                              purple_zulip_client_login_read_password_cb,
	                                              task);

	g_clear_pointer(&response, purple_zulip_server_settings_response_free);
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleZulipClient, purple_zulip_client,
                               G_TYPE_OBJECT, G_TYPE_FLAG_FINAL, {})

static void
purple_zulip_client_finalize(GObject *obj) {
	PurpleZulipClient *client = PURPLE_ZULIP_CLIENT(obj);

	g_clear_object(&client->cancellable);
	g_clear_pointer(&client->server_uri, g_free);
	g_clear_object(&client->account);
	g_clear_object(&client->session);
	g_clear_pointer(&client->authorization, g_free);

	G_OBJECT_CLASS(purple_zulip_client_parent_class)->finalize(obj);
}

static void
purple_zulip_client_get_property(GObject *obj, guint param_id, GValue *value,
                                 GParamSpec *pspec)
{
	PurpleZulipClient *client = PURPLE_ZULIP_CLIENT(obj);

	switch(param_id) {
	case PROP_ACCOUNT:
		g_value_set_object(value, purple_zulip_client_get_account(client));
		break;
	case PROP_CANCELLABLE:
		g_value_set_object(value, purple_zulip_client_get_cancellable(client));
		break;
	case PROP_SERVER_URI:
		g_value_set_string(value, purple_zulip_client_get_server_uri(client));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_zulip_client_set_property(GObject *obj, guint param_id,
                                 const GValue *value, GParamSpec *pspec)
{
	PurpleZulipClient *client = PURPLE_ZULIP_CLIENT(obj);

	switch(param_id) {
	case PROP_CANCELLABLE:
		purple_zulip_client_set_cancellable(client, g_value_get_object(value));
		break;
	case PROP_SERVER_URI:
		purple_zulip_client_set_server_uri(client, g_value_get_string(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_zulip_client_init(PurpleZulipClient *client) {
	PurpleCore *core = NULL;
	PurpleUi *ui = NULL;
	GSettings *settings = NULL;
	char *user_agent = NULL;

	core = purple_core_get_default();
	ui = purple_core_get_ui(core);
	user_agent = purple_ui_get_user_agent(ui);

	client->session = soup_session_new_with_options(
		"user-agent", user_agent,
		NULL);

	g_clear_pointer(&user_agent, g_free);

	settings = purple_core_new_settings(core, "im.pidgin.Purple.Core");
	g_signal_connect_object(settings,
	                        "changed::developer-mode",
	                        G_CALLBACK(purple_zulip_client_developer_mode_changed_cb),
	                        client,
	                        G_CONNECT_DEFAULT);
	purple_zulip_client_update_logger(client, settings, "developer-mode");
}

static void
purple_zulip_client_class_finalize(G_GNUC_UNUSED PurpleZulipClientClass *klass)
{
}

static void
purple_zulip_client_class_init(PurpleZulipClientClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->finalize = purple_zulip_client_finalize;
	obj_class->get_property = purple_zulip_client_get_property;
	obj_class->set_property = purple_zulip_client_set_property;

	properties[PROP_ACCOUNT] = g_param_spec_object(
		"account", NULL, NULL,
		PURPLE_TYPE_ACCOUNT,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	properties[PROP_CANCELLABLE] = g_param_spec_object(
		"cancellable", NULL, NULL,
		G_TYPE_CANCELLABLE,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	properties[PROP_SERVER_URI] = g_param_spec_string(
		"server-uri", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * API
 *****************************************************************************/
PurpleAccount *
purple_zulip_client_get_account(PurpleZulipClient *client) {
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), NULL);

	return client->account;
}

GCancellable *
purple_zulip_client_get_cancellable(PurpleZulipClient *client) {
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), NULL);

	return client->cancellable;
}

const char *
purple_zulip_client_get_server_uri(PurpleZulipClient *client) {
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), NULL);

	return client->server_uri;
}

PurpleZulipClient *
purple_zulip_client_new(const char *server_uri, GCancellable *cancellable) {
	return g_object_new(
		PURPLE_ZULIP_TYPE_CLIENT,
		"cancellable", cancellable,
		"server-uri", server_uri,
		NULL);
}

void
purple_zulip_client_register(GPluginNativePlugin *plugin) {
	purple_zulip_client_register_type(G_TYPE_MODULE(plugin));
}

void
purple_zulip_client_login_async(PurpleZulipClient *client,
                                PurpleAccount *account,
                                GAsyncReadyCallback callback,
                                gpointer data)
{
	GTask *task = NULL;

	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	purple_zulip_client_set_account(client, account);

	task = g_task_new(client, client->cancellable, callback, data);
	g_task_set_task_data(task, g_object_ref(client), g_object_unref);

	purple_zulip_client_get_server_settings_async(client,
	                                              purple_zulip_client_login_check_server_settings_cb,
	                                              task);
}

gboolean
purple_zulip_client_login_finish(PurpleZulipClient *client,
                                 GAsyncResult *result,
                                 GError **error)
{
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), FALSE);
	g_return_val_if_fail(g_task_is_valid(result, client), FALSE);

	return g_task_propagate_boolean(G_TASK(result), error);
}

void
purple_zulip_client_fetch_api_key_async(PurpleZulipClient *client,
                                        const char *username,
                                        const char *password,
                                        gboolean remember,
                                        GAsyncReadyCallback callback,
                                        gpointer data)
{
	GTask *task = NULL;
	SoupMessage *message = NULL;
	char *form = NULL;

	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));

	task = g_task_new(G_OBJECT(client), client->cancellable, callback, data);
	g_task_set_task_data(task, GINT_TO_POINTER(remember), NULL);

	form = soup_form_encode(
		"username", username,
		"password", password,
		NULL);

	message = purple_zulip_client_new_api_message(client, SOUP_METHOD_POST,
	                                              "/v1/fetch_api_key", form);

	soup_session_send_and_read_async(client->session, message,
	                                 G_PRIORITY_DEFAULT, client->cancellable,
	                                 purple_zulip_client_soup_fetch_api_key_cb,
	                                 task);

	g_clear_object(&message);
}

PurpleZulipFetchApiKeyResponse *
purple_zulip_client_fetch_api_key_finish(PurpleZulipClient *client,
                                         GAsyncResult *result,
                                         GError **error)
{
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), NULL);
	g_return_val_if_fail(g_task_is_valid(result, client), NULL);

	return g_task_propagate_pointer(G_TASK(result), error);
}

void
purple_zulip_client_get_server_settings_async(PurpleZulipClient *client,
                                              GAsyncReadyCallback callback,
                                              gpointer data)
{
	GTask *task = NULL;
	SoupMessage *message = NULL;

	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));

	task = g_task_new(G_OBJECT(client), client->cancellable, callback, data);

	message = purple_zulip_client_new_api_message(client, SOUP_METHOD_GET,
	                                              "/v1/server_settings", NULL);

	soup_session_send_and_read_async(client->session, message,
	                                 G_PRIORITY_DEFAULT, client->cancellable,
	                                 purple_zulip_client_get_server_settings_cb,
	                                 task);

	g_clear_object(&message);
}

PurpleZulipServerSettingsResponse *
purple_zulip_client_get_server_settings_finish(PurpleZulipClient *client,
                                               GAsyncResult *result,
                                               GError **error)
{
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), NULL);
	g_return_val_if_fail(g_task_is_valid(result, client), NULL);

	return g_task_propagate_pointer(G_TASK(result), error);
}

/******************************************************************************
 * GET /api/v1/events
 *****************************************************************************/
static void
purple_zulip_client_get_events_cb(GObject *source,
                                  GAsyncResult *result,
                                  gpointer data)
{
	PurpleZulipGetEventsResponse *response = NULL;
	SoupSession *session = SOUP_SESSION(source);
	GError *error = NULL;
	GTask *task = data;

	response = purple_zulip_client_parse_response(session,
	                                              PURPLE_ZULIP_TYPE_GET_EVENTS_RESPONSE,
	                                              result,
	                                              &error);
	if(error != NULL) {
		g_task_return_error(task, error);
	} else {
		g_task_return_pointer(task, response,
		                      (GDestroyNotify)purple_zulip_get_events_response_free);
	}

	g_clear_object(&task);
}

void
purple_zulip_client_get_events_async(PurpleZulipClient *client,
                                     const char *queue_id,
                                     int last_event_id,
                                     GAsyncReadyCallback callback,
                                     gpointer data)
{
	GTask *task = NULL;
	SoupMessage *message = NULL;
	char *form = NULL;
	char *last_event_id_str = NULL;

	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));
	g_return_if_fail(!birb_str_is_empty(queue_id));

	task = g_task_new(G_OBJECT(client), client->cancellable, callback, data);

	last_event_id_str = g_strdup_printf("%d", last_event_id);
	form = soup_form_encode(
		"queue_id", queue_id,
		"last_event_id", last_event_id_str,
		NULL);
	g_clear_pointer(&last_event_id_str, g_free);

	message = purple_zulip_client_new_api_message(client, SOUP_METHOD_GET,
	                                              "/v1/events", form);

	soup_session_send_and_read_async(client->session, message,
	                                 G_PRIORITY_DEFAULT, client->cancellable,
	                                 purple_zulip_client_get_events_cb,
	                                 task);

	g_clear_object(&message);
}

PurpleZulipGetEventsResponse *
purple_zulip_client_get_events_finish(PurpleZulipClient *client,
                                      GAsyncResult *result,
                                      GError **error)
{
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), FALSE);
	g_return_val_if_fail(g_task_is_valid(result, client), FALSE);

	return g_task_propagate_pointer(G_TASK(result), error);
}

/******************************************************************************
 * DELETE /api/v1/events
 *****************************************************************************/
static void
purple_zulip_client_delete_event_queue_cb(GObject *source,
                                          GAsyncResult *result,
                                          gpointer data)
{
	SoupSession *session = SOUP_SESSION(source);
	GError *error = NULL;
	GTask *task = data;

	purple_zulip_client_parse_response(session, G_TYPE_NONE, result, &error);
	if(error != NULL) {
		g_task_return_error(task, error);
	} else {
		g_task_return_boolean(task, TRUE);
	}

	g_clear_object(&task);
}

void
purple_zulip_client_delete_event_queue_async(PurpleZulipClient *client,
                                             const char *queue_id,
                                             GAsyncReadyCallback callback,
                                             gpointer data)
{
	GTask *task = NULL;
	SoupMessage *message = NULL;
	char *form = NULL;

	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));
	g_return_if_fail(!birb_str_is_empty(queue_id));

	task = g_task_new(G_OBJECT(client), client->cancellable, callback, data);

	form = soup_form_encode(
		"queue_id", queue_id,
		NULL);

	message = purple_zulip_client_new_api_message(client, SOUP_METHOD_DELETE,
	                                              "/v1/events", form);

	soup_session_send_and_read_async(client->session, message,
	                                 G_PRIORITY_DEFAULT, client->cancellable,
	                                 purple_zulip_client_delete_event_queue_cb,
	                                 task);

	g_clear_object(&message);
}

gboolean
purple_zulip_client_delete_event_queue_finish(PurpleZulipClient *client,
                                              GAsyncResult *result,
                                              GError **error)
{
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), FALSE);
	g_return_val_if_fail(g_task_is_valid(result, client), FALSE);

	return g_task_propagate_boolean(G_TASK(result), error);
}

/******************************************************************************
 * POST /api/v1/register
 *****************************************************************************/
static void
purple_zulip_client_register_event_queue_cb(GObject *source,
                                            GAsyncResult *result,
                                            gpointer data)
{
	PurpleZulipRegisterEventQueueResponse *response = NULL;
	SoupSession *session = SOUP_SESSION(source);
	GError *error = NULL;
	GTask *task = data;

	response = purple_zulip_client_parse_response(session,
	                                              PURPLE_ZULIP_TYPE_REGISTER_EVENT_QUEUE_RESPONSE,
	                                              result, &error);
	if(error != NULL) {
		g_task_return_error(task, error);
	} else {
		g_task_return_pointer(task, response,
		                      (GDestroyNotify)purple_zulip_register_event_queue_response_free);
	}

	g_clear_object(&task);
}

void
purple_zulip_client_register_event_queue_async(PurpleZulipClient *client,
                                               GAsyncReadyCallback callback,
                                               gpointer data)
{
	GTask *task = NULL;
	SoupMessage *message = NULL;
	JsonNode *node = NULL;
	JsonArray *array = NULL;
	char *event_types = NULL;
	char *form = NULL;

	g_return_if_fail(PURPLE_ZULIP_IS_CLIENT(client));

	task = g_task_new(G_OBJECT(client), client->cancellable, callback, data);

	/* Build the event_types JSON value. */
	array = json_array_new();
	json_array_add_string_element(array, "message");
	json_array_add_string_element(array, "realm_user");

	node = json_node_new(JSON_NODE_ARRAY);
	json_node_take_array(node, array);

	event_types = json_to_string(node, FALSE);
	g_clear_pointer(&node, json_node_unref);

	/* Build the form encoded body. */
	form = soup_form_encode(
		"event_types", event_types,
		NULL);
	g_clear_pointer(&event_types, g_free);

	/* Build and send the message. */
	message = purple_zulip_client_new_api_message(client, SOUP_METHOD_POST,
	                                              "/v1/register", form);

	soup_session_send_and_read_async(client->session, message,
	                                 G_PRIORITY_DEFAULT, client->cancellable,
	                                 purple_zulip_client_register_event_queue_cb,
	                                 task);

	g_clear_object(&message);
}

PurpleZulipRegisterEventQueueResponse *
purple_zulip_client_register_event_queue_finish(PurpleZulipClient *client,
                                                GAsyncResult *result,
                                                GError **error)
{
	g_return_val_if_fail(PURPLE_ZULIP_IS_CLIENT(client), NULL);
	g_return_val_if_fail(g_task_is_valid(result, client), NULL);

	return g_task_propagate_pointer(G_TASK(result), error);
}
