// SPDX-License-Identifier: LGPL-2.1+

#include "nm-default.h"

#include "nm-http-client.h"

#include <curl/curl.h>

#include "nm-cloud-setup-utils.h"

#define NM_CURL_DEBUG 0

/*****************************************************************************/

typedef struct {
	GMainContext *context;
	CURLM *mhandle;
	GSource *mhandle_source_timeout;
	GHashTable *source_sockets_hashtable;
} NMHttpClientPrivate;

struct _NMHttpClient {
	GObject parent;
	NMHttpClientPrivate _priv;
};

struct _NMHttpClientClass {
	GObjectClass parent;
};

G_DEFINE_TYPE (NMHttpClient, nm_http_client, G_TYPE_OBJECT);

#define NM_HTTP_CLIENT_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMHttpClient, NM_IS_HTTP_CLIENT)

/*****************************************************************************/

#define _NMLOG2(level, edata, ...) \
	G_STMT_START { \
		EHandleData *_edata = (edata); \
		\
		_NMLOG (level, \
		        "http-request["NM_HASH_OBFUSCATE_PTR_FMT", \"%s\"]: " \
		        _NM_UTILS_MACRO_FIRST (__VA_ARGS__), \
		        NM_HASH_OBFUSCATE_PTR (_edata), \
		        (_edata)->url \
		        _NM_UTILS_MACRO_REST (__VA_ARGS__)); \
	} G_STMT_END

/*****************************************************************************/

G_LOCK_DEFINE_STATIC (_my_curl_initalized_lock);
static bool _my_curl_initialized = FALSE;

__attribute__((destructor))
static void
_my_curl_global_cleanup (void)
{
	G_LOCK (_my_curl_initalized_lock);
	if (_my_curl_initialized) {
		_my_curl_initialized = FALSE;
		curl_global_cleanup ();
	}
	G_UNLOCK (_my_curl_initalized_lock);
}

static void
nm_http_client_curl_global_init (void)
{
	G_LOCK (_my_curl_initalized_lock);
	if (!_my_curl_initialized) {
		_my_curl_initialized = TRUE;
		if (curl_global_init (CURL_GLOBAL_ALL) != CURLE_OK) {
			/* Even if this fails, we are partly initialized. WTF. */
			_LOGE ("curl: curl_global_init() failed!");
		}
	}
	G_UNLOCK (_my_curl_initalized_lock);
}

/*****************************************************************************/

GMainContext *
nm_http_client_get_main_context (NMHttpClient *self)
{
	g_return_val_if_fail (NM_IS_HTTP_CLIENT (self), NULL);

	return NM_HTTP_CLIENT_GET_PRIVATE (self)->context;
}

/*****************************************************************************/

static GSource *
_source_attach (NMHttpClient *self,
                GSource *source)
{
	return nm_g_source_attach (source, NM_HTTP_CLIENT_GET_PRIVATE (self)->context);
}

/*****************************************************************************/

typedef struct {
	long response_code;
	GBytes *response_data;
} GetResult;

static void
_get_result_free (gpointer data)
{
	GetResult *get_result = data;

	g_bytes_unref (get_result->response_data);
	nm_g_slice_free (get_result);
}

typedef struct {
	GTask *task;
	GSource *timeout_source;
	CURLcode ehandle_result;
	CURL *ehandle;
	char *url;
	GString *recv_data;
	struct curl_slist *headers;
	gssize max_data;
	gulong cancellable_id;
} EHandleData;

static void
_ehandle_free_ehandle (EHandleData *edata)
{
	if (edata->ehandle) {
		NMHttpClient *self = g_task_get_source_object (edata->task);
		NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);

		curl_multi_remove_handle (priv->mhandle, edata->ehandle);
		curl_easy_cleanup (g_steal_pointer (&edata->ehandle));
	}
}

static void
_ehandle_free (EHandleData *edata)
{
	nm_assert (!edata->ehandle);
	nm_assert (!edata->timeout_source);

	g_object_unref (edata->task);

	if (edata->recv_data)
		g_string_free (edata->recv_data, TRUE);
	if (edata->headers)
		curl_slist_free_all (edata->headers);
	g_free (edata->url);
	nm_g_slice_free (edata);
}

static void
_ehandle_complete (EHandleData *edata,
                   GError *error_take)
{
	GetResult *get_result;
	gs_free char *str_tmp_1 = NULL;
	long response_code = -1;

	nm_clear_pointer (&edata->timeout_source, nm_g_source_destroy_and_unref);

	nm_clear_g_cancellable_disconnect (g_task_get_cancellable (edata->task),
	                                   &edata->cancellable_id);

	if (error_take) {
		if (nm_utils_error_is_cancelled (error_take))
			_LOG2T (edata, "cancelled");
		else
			_LOG2D (edata, "failed with %s", error_take->message);
	} else if (edata->ehandle_result != CURLE_OK) {
		_LOG2D (edata, "failed with curl error \"%s\"", curl_easy_strerror (edata->ehandle_result));
		nm_utils_error_set (&error_take,
		                    NM_UTILS_ERROR_UNKNOWN,
		                    "failed with curl error \"%s\"",
		                    curl_easy_strerror (edata->ehandle_result));
	}

	if (error_take) {
		_ehandle_free_ehandle (edata);
		g_task_return_error (edata->task, error_take);
		_ehandle_free (edata);
		return;
	}

	if (curl_easy_getinfo (edata->ehandle,
	                       CURLINFO_RESPONSE_CODE,
	                       &response_code) != CURLE_OK)
		_LOG2E (edata, "failed to get response code from curl easy handle");

	_LOG2D (edata, "success getting %"G_GSIZE_FORMAT" bytes (response code %ld)",
	        edata->recv_data->len,
	        response_code);

	_LOG2T (edata, "received %"G_GSIZE_FORMAT" bytes: [[%s]]",
	       edata->recv_data->len,
	       nm_utils_buf_utf8safe_escape (edata->recv_data->str, edata->recv_data->len, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, &str_tmp_1));

	_ehandle_free_ehandle (edata);

	get_result = g_slice_new (GetResult);
	*get_result = (GetResult) {
		.response_code = response_code,
		/* This ensures that response_data is always NUL terminated. This is an important guarantee
		 * that NMHttpClient makes. */
		.response_data = g_string_free_to_bytes (g_steal_pointer (&edata->recv_data)),
	};

	g_task_return_pointer (edata->task, get_result, _get_result_free);

	_ehandle_free (edata);
}

/*****************************************************************************/

static size_t
_get_writefunction_cb (char *ptr, size_t size, size_t nmemb, void *user_data)
{
	EHandleData *edata = user_data;
	gsize nconsume;

	/* size should always be 1, but still. Multiply them to be sure. */
	nmemb *= size;

	if (edata->max_data >= 0) {
		nm_assert (edata->recv_data->len <= edata->max_data);
		nconsume = (((gsize) edata->max_data) - edata->recv_data->len);
		if (nconsume > nmemb)
			nconsume = nmemb;
	} else
		nconsume = nmemb;

	g_string_append_len (edata->recv_data, ptr, nconsume);
	return nconsume;
}

static gboolean
_get_timeout_cb (gpointer user_data)
{
	_ehandle_complete (user_data,
	                   g_error_new_literal (NM_UTILS_ERROR,
	                                        NM_UTILS_ERROR_UNKNOWN,
	                                        "HTTP request timed out"));
	return G_SOURCE_REMOVE;
}

static void
_get_cancelled_cb (GObject *object, gpointer user_data)
{
	EHandleData *edata = user_data;
	GError *error = NULL;

	nm_clear_g_signal_handler (g_task_get_cancellable (edata->task),
	                           &edata->cancellable_id);
	nm_utils_error_set_cancelled (&error, FALSE, NULL);
	_ehandle_complete (edata, error);
}

void
nm_http_client_get (NMHttpClient *self,
                    const char *url,
                    int timeout_msec,
                    gssize max_data,
                    const char *const *http_headers,
                    GCancellable *cancellable,
                    GAsyncReadyCallback callback,
                    gpointer user_data)
{
	NMHttpClientPrivate *priv;
	EHandleData *edata;
	guint i;

	g_return_if_fail (NM_IS_HTTP_CLIENT (self));
	g_return_if_fail (url);
	g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
	g_return_if_fail (timeout_msec >= 0);
	g_return_if_fail (max_data >= -1);

	priv = NM_HTTP_CLIENT_GET_PRIVATE (self);

	edata = g_slice_new (EHandleData);
	*edata = (EHandleData) {
		.task           = nm_g_task_new (self, cancellable, nm_http_client_get, callback, user_data),
		.recv_data      = g_string_sized_new (NM_MIN (max_data, 245)),
		.max_data       = max_data,
		.url            = g_strdup (url),
		.headers        = NULL,
	};

	nmcs_wait_for_objects_register (edata->task);

	_LOG2D (edata, "start get ...");

	edata->ehandle = curl_easy_init ();
	if (!edata->ehandle) {
		_ehandle_complete (edata,
		                   g_error_new_literal (NM_UTILS_ERROR,
		                                        NM_UTILS_ERROR_UNKNOWN,
		                                        "HTTP request failed to create curl handle"));
		return;
	}

	curl_easy_setopt (edata->ehandle, CURLOPT_URL, url);

	curl_easy_setopt (edata->ehandle, CURLOPT_WRITEFUNCTION, _get_writefunction_cb);
	curl_easy_setopt (edata->ehandle, CURLOPT_WRITEDATA, edata);
	curl_easy_setopt (edata->ehandle, CURLOPT_PRIVATE, edata);

	if (http_headers) {
		for (i = 0; http_headers[i]; ++i) {
			struct curl_slist *tmp;

			tmp = curl_slist_append (edata->headers,
			                         http_headers[i]);
			if (!tmp) {
				curl_slist_free_all (tmp);
				_LOGE ("curl: curl_slist_append() failed adding %s", http_headers[i]);
				continue;
			}
			edata->headers = tmp;
		}

		curl_easy_setopt (edata->ehandle, CURLOPT_HTTPHEADER, edata->headers);
	}

	if (timeout_msec > 0) {
		edata->timeout_source = _source_attach (self,
		                                        nm_g_timeout_source_new (timeout_msec,
		                                                                 G_PRIORITY_DEFAULT,
		                                                                 _get_timeout_cb,
		                                                                 edata,
		                                                                 NULL));
	}

	curl_multi_add_handle (priv->mhandle, edata->ehandle);

	if (cancellable) {
		gulong signal_id;

		signal_id = g_cancellable_connect (cancellable,
		                                   G_CALLBACK (_get_cancelled_cb),
		                                   edata,
		                                   NULL);
		if (signal_id == 0) {
			/* the request is already cancelled. Return. */
			return;
		}
		edata->cancellable_id = signal_id;
	}
}

gboolean
nm_http_client_get_finish (NMHttpClient *self,
                           GAsyncResult *result,
                           long *out_response_code,
                           GBytes **out_response_data,
                           GError **error)
{
	GetResult *get_result;

	g_return_val_if_fail (NM_IS_HTTP_CLIENT (self), FALSE);
	g_return_val_if_fail (nm_g_task_is_valid (result, self, nm_http_client_get), FALSE);

	get_result = g_task_propagate_pointer (G_TASK (result), error);
	if (!get_result) {
		NM_SET_OUT (out_response_code, -1);
		NM_SET_OUT (out_response_data, NULL);
		return FALSE;
	}

	NM_SET_OUT (out_response_code, get_result->response_code);

	/* response_data is binary, but is also guaranteed to be NUL terminated! */
	NM_SET_OUT (out_response_data, g_steal_pointer (&get_result->response_data));

	_get_result_free (get_result);

	return TRUE;
}

/*****************************************************************************/

typedef struct {
	GTask *task;
	char *uri;
	const char *const *http_headers;
	NMHttpClientPollGetCheckFcn check_fcn;
	gpointer check_user_data;
	GBytes *response_data;
	gsize request_max_data;
	long response_code;
	int request_timeout_ms;
} PollGetData;

static void
_poll_get_data_free (gpointer data)
{
	PollGetData *poll_get_data = data;

	g_free (poll_get_data->uri);

	nm_clear_pointer (&poll_get_data->response_data, g_bytes_unref);
	g_strfreev ((char **) poll_get_data->http_headers);

	nm_g_slice_free (poll_get_data);
}

static void
_poll_get_probe_start_fcn (GCancellable *cancellable,
                           gpointer probe_user_data,
                           GAsyncReadyCallback callback,
                           gpointer user_data)
{
	PollGetData *poll_get_data = probe_user_data;

	/* balanced by _poll_get_probe_finish_fcn() */
	g_object_ref (poll_get_data->task);

	nm_http_client_get (g_task_get_source_object (poll_get_data->task),
	                    poll_get_data->uri,
	                    poll_get_data->request_timeout_ms,
	                    poll_get_data->request_max_data,
	                    poll_get_data->http_headers,
	                    cancellable,
	                    callback,
	                    user_data);
}

static gboolean
_poll_get_probe_finish_fcn (GObject *source,
                            GAsyncResult *result,
                            gpointer probe_user_data,
                            GError **error)
{
	PollGetData *poll_get_data = probe_user_data;
	_nm_unused gs_unref_object GTask *task = poll_get_data->task; /* balance ref from _poll_get_probe_start_fcn() */
	gboolean success;
	gs_free_error GError *local_error = NULL;
	gs_unref_bytes GBytes *response_data = NULL;
	long response_code = -1;

	success = nm_http_client_get_finish (g_task_get_source_object (poll_get_data->task),
	                                     result,
	                                     &response_code,
	                                     &response_data,
	                                     &local_error);

	if (!success) {
		if (nm_utils_error_is_cancelled (local_error)) {
			g_propagate_error (error, g_steal_pointer (&local_error));
			return TRUE;
		}
		return FALSE;
	}

	if (poll_get_data->check_fcn) {
		success = poll_get_data->check_fcn (response_code,
		                                    response_data,
		                                    poll_get_data->check_user_data,
		                                    &local_error);
	} else
		success = (response_code == 200);

	if (local_error) {
		g_propagate_error (error, g_steal_pointer (&local_error));
		return TRUE;
	}

	if (!success)
		return FALSE;

	poll_get_data->response_code = response_code;
	poll_get_data->response_data = g_steal_pointer (&response_data);
	return TRUE;
}

static void
_poll_get_done_cb (GObject *source,
                   GAsyncResult *result,
                   gpointer user_data)
{
	PollGetData *poll_get_data = user_data;
	gs_free_error GError *error = NULL;
	gboolean success;

	success = nmcs_utils_poll_finish (result, NULL, &error);

	if (error)
		g_task_return_error (poll_get_data->task, g_steal_pointer (&error));
	else
		g_task_return_boolean (poll_get_data->task, success);

	g_object_unref (poll_get_data->task);
}

void
nm_http_client_poll_get (NMHttpClient *self,
                         const char *uri,
                         int request_timeout_ms,
                         gssize request_max_data,
                         int poll_timeout_ms,
                         int ratelimit_timeout_ms,
                         const char *const *http_headers,
                         GCancellable *cancellable,
                         NMHttpClientPollGetCheckFcn check_fcn,
                         gpointer check_user_data,
                         GAsyncReadyCallback callback,
                         gpointer user_data)
{
	nm_auto_pop_gmaincontext GMainContext *context = NULL;
	PollGetData *poll_get_data;

	g_return_if_fail (NM_IS_HTTP_CLIENT (self));
	g_return_if_fail (uri && uri[0]);
	g_return_if_fail (request_timeout_ms >= -1);
	g_return_if_fail (request_max_data >= -1);
	g_return_if_fail (poll_timeout_ms >= -1);
	g_return_if_fail (ratelimit_timeout_ms >= -1);
	g_return_if_fail (!cancellable || G_CANCELLABLE (cancellable));

	poll_get_data = g_slice_new (PollGetData);
	*poll_get_data = (PollGetData) {
		.task               = nm_g_task_new (self, cancellable, nm_http_client_poll_get, callback, user_data),
		.uri                = g_strdup (uri),
		.request_timeout_ms = request_timeout_ms,
		.request_max_data   = request_max_data,
		.check_fcn          = check_fcn,
		.check_user_data    = check_user_data,
		.response_code      = -1,
		.http_headers       =  NM_CAST_STRV_CC (g_strdupv ((char **) http_headers)),
	};

	nmcs_wait_for_objects_register (poll_get_data->task);

	g_task_set_task_data (poll_get_data->task,
	                      poll_get_data,
	                      _poll_get_data_free);

	context = nm_g_main_context_push_thread_default_if_necessary (nm_http_client_get_main_context (self));

	nmcs_utils_poll (poll_timeout_ms,
	                 ratelimit_timeout_ms,
	                 0,
	                 _poll_get_probe_start_fcn,
	                 _poll_get_probe_finish_fcn,
	                 poll_get_data,
	                 cancellable,
	                 _poll_get_done_cb,
	                 poll_get_data);
}

gboolean
nm_http_client_poll_get_finish (NMHttpClient *self,
                                GAsyncResult *result,
                                long *out_response_code,
                                GBytes **out_response_data,
                                GError **error)
{
	PollGetData *poll_get_data;
	GTask *task;
	gboolean success;
	gs_free_error GError *local_error = NULL;

	g_return_val_if_fail (NM_HTTP_CLIENT (self), FALSE);
	g_return_val_if_fail (nm_g_task_is_valid (result, self, nm_http_client_poll_get), FALSE);

	task = G_TASK (result);

	success = g_task_propagate_boolean (task, &local_error);
	if (   local_error
	    || !success) {
		if (local_error)
			g_propagate_error (error, g_steal_pointer (&local_error));
		NM_SET_OUT (out_response_code, -1);
		NM_SET_OUT (out_response_data, NULL);
		return FALSE;
	}

	poll_get_data = g_task_get_task_data (task);

	NM_SET_OUT (out_response_code, poll_get_data->response_code);
	NM_SET_OUT (out_response_data, g_steal_pointer (&poll_get_data->response_data));

	return TRUE;
}

/*****************************************************************************/

static void
_mhandle_action (NMHttpClient *self,
                 int sockfd,
                 int ev_bitmask)
{
	NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
	EHandleData *edata;
	CURLMsg *msg;
	CURLcode eret;
	int m_left;
	CURLMcode ret;
	int running_handles;

	ret = curl_multi_socket_action (priv->mhandle, sockfd, ev_bitmask, &running_handles);
	if (ret != CURLM_OK) {
		_LOGE ("curl: curl_multi_socket_action() failed: (%d) %s", ret, curl_multi_strerror (ret));
		/* really unexpected. Not clear how to handle this. */
	}

	while ((msg = curl_multi_info_read (priv->mhandle, &m_left))) {

		if (msg->msg != CURLMSG_DONE)
			continue;

		eret = curl_easy_getinfo (msg->easy_handle, CURLINFO_PRIVATE, (char **) &edata);

		nm_assert (eret == CURLE_OK);
		nm_assert (edata);

		edata->ehandle_result = msg->data.result;
		_ehandle_complete (edata, NULL);
	}
}

static gboolean
_mhandle_socket_cb (int fd,
                    GIOCondition condition,
                    gpointer user_data)
{
	int ev_bitmask = 0;

	if (condition & G_IO_IN)
		ev_bitmask |= CURL_CSELECT_IN;
	if (condition & G_IO_OUT)
		ev_bitmask |= CURL_CSELECT_OUT;
	if (condition & G_IO_ERR)
		ev_bitmask |= CURL_CSELECT_ERR;

	_mhandle_action (user_data, fd, ev_bitmask);
	return G_SOURCE_CONTINUE;
}

static int
_mhandle_socketfunction_cb (CURL *e_handle, curl_socket_t fd, int what, void *user_data, void *socketp)
{
	GSource *source_socket;
	NMHttpClient *self = user_data;
	NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);

	(void) _NM_ENSURE_TYPE (int, fd);

	g_hash_table_remove (priv->source_sockets_hashtable, GINT_TO_POINTER (fd));

	if (what != CURL_POLL_REMOVE) {
		GIOCondition condition = 0;

		if (what == CURL_POLL_IN)
			condition = G_IO_IN;
		else if (what == CURL_POLL_OUT)
			condition = G_IO_OUT;
		else if (what == CURL_POLL_INOUT)
			condition = G_IO_IN | G_IO_OUT;
		else
			condition = 0;

		if (condition) {
			source_socket = nm_g_unix_fd_source_new (fd,
			                                         condition,
			                                         G_PRIORITY_DEFAULT,
			                                         _mhandle_socket_cb,
			                                         self,
			                                                       NULL);
			g_source_attach (source_socket, priv->context);

			g_hash_table_insert (priv->source_sockets_hashtable,
			                     GINT_TO_POINTER (fd),
			                     source_socket);
		}
	}

	return CURLM_OK;
}

static gboolean
_mhandle_timeout_cb (gpointer user_data)
{
	_mhandle_action (user_data, CURL_SOCKET_TIMEOUT, 0);
	return G_SOURCE_REMOVE;
}

static int
_mhandle_timerfunction_cb (CURLM *multi, long timeout_msec, void *user_data)
{
	NMHttpClient *self = user_data;
	NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);

	nm_clear_pointer (&priv->mhandle_source_timeout, nm_g_source_destroy_and_unref);
	if (timeout_msec >= 0) {
		priv->mhandle_source_timeout = _source_attach (self,
		                                               nm_g_timeout_source_new (NM_MIN (timeout_msec, G_MAXINT),
		                                                                        G_PRIORITY_DEFAULT,
		                                                                        _mhandle_timeout_cb,
		                                                                        self,
		                                                                        NULL));
	}
	return 0;
}

/*****************************************************************************/

static void
nm_http_client_init (NMHttpClient *self)
{
	NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);

	priv->source_sockets_hashtable = g_hash_table_new_full (nm_direct_hash,
	                                                        NULL,
	                                                        NULL,
	                                                        (GDestroyNotify) nm_g_source_destroy_and_unref);
}

static void
constructed (GObject *object)
{
	NMHttpClient *self = NM_HTTP_CLIENT (object);
	NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);

	priv->context = g_main_context_ref_thread_default ();

	priv->mhandle = curl_multi_init ();
	if (!priv->mhandle)
		_LOGE ("curl: failed to create multi-handle");
	else {
		curl_multi_setopt (priv->mhandle, CURLMOPT_SOCKETFUNCTION, _mhandle_socketfunction_cb);
		curl_multi_setopt (priv->mhandle, CURLMOPT_SOCKETDATA, self);
		curl_multi_setopt (priv->mhandle, CURLMOPT_TIMERFUNCTION, _mhandle_timerfunction_cb);
		curl_multi_setopt (priv->mhandle, CURLMOPT_TIMERDATA, self);
	}

	G_OBJECT_CLASS (nm_http_client_parent_class)->constructed (object);
}

NMHttpClient *
nm_http_client_new (void)
{
	return g_object_new (NM_TYPE_HTTP_CLIENT, NULL);
}

static void
dispose (GObject *object)
{
	NMHttpClient *self = NM_HTTP_CLIENT (object);
	NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);

	nm_clear_pointer (&priv->mhandle, curl_multi_cleanup);
	nm_clear_pointer (&priv->source_sockets_hashtable, g_hash_table_unref);

	nm_clear_g_source_inst (&priv->mhandle_source_timeout);

	G_OBJECT_CLASS (nm_http_client_parent_class)->dispose (object);
}

static void
finalize (GObject *object)
{
	NMHttpClient *self = NM_HTTP_CLIENT (object);
	NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);

	G_OBJECT_CLASS (nm_http_client_parent_class)->finalize (object);

	g_main_context_unref (priv->context);

	curl_global_cleanup ();
}

static void
nm_http_client_class_init (NMHttpClientClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->constructed = constructed;
	object_class->dispose     = dispose;
	object_class->finalize    = finalize;

	nm_http_client_curl_global_init ();
}
