/* geoclue_stumbler-http-proxy.c
 *
 * Copyright 2024 Christopher Talbot
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "geoclue-stumbler-http-proxy"

#include <libsoup/soup.h>
#include <json-glib/json-glib.h>

#include "config.h"
#include "geoclue-stumbler-http-dbus.h"
#include "geoclue-stumbler-http-proxy.h"

struct _GeoclueStumblerHttpProxy
{
  GObject         parent_instance;

  SoupServer *server;
  SoupSession *web;
  char *save_path;
  JsonParser *parser;

  char *submission_url;
  guint service_registration_id;
  gboolean debug;
};

G_DEFINE_TYPE (GeoclueStumblerHttpProxy, geoclue_stumbler_httpproxy, G_TYPE_OBJECT)

static void
emit_upload_error (GeoclueStumblerHttpProxy *self)
{
  GDBusConnection *connection = geoclue_stumbler_dbus_get_connection ();
  g_autoptr(GError) error = NULL;

  g_dbus_connection_emit_signal (connection,
                                 NULL,
                                 GEOCLUE_STUMBLER_PATH,
                                 GEOCLUE_STUMBLER_SERVICE,
                                 "UploadError",
                                 NULL,
                                 &error);

  if (error != NULL) {
    g_warning ("Error in Signal Emitting: %s\n", error->message);
    error = NULL;
  }
}

static void
emit_upload_success (GeoclueStumblerHttpProxy *self)
{
  GDBusConnection *connection = geoclue_stumbler_dbus_get_connection ();
  g_autoptr(GError) error = NULL;

  g_dbus_connection_emit_signal (connection,
                                 NULL,
                                 GEOCLUE_STUMBLER_PATH,
                                 GEOCLUE_STUMBLER_SERVICE,
                                 "UploadSuccess",
                                 NULL,
                                 &error);

  if (error != NULL) {
    g_warning ("Error in Signal Emitting: %s\n", error->message);
    error = NULL;
  }
}

typedef struct _message_payload message_payload;

struct _message_payload {
  GeoclueStumblerHttpProxy *self;
  char *absolute_file_path;
};

static void
message_payload_free (gpointer data)
{
  message_payload *payload = data;

  if (!payload)
    return;

  g_free (payload->absolute_file_path);
  g_free (payload);
}

static void on_upload_callback (SoupSession  *session,
                                GAsyncResult *result,
                                gpointer user_data)
{
  message_payload *payload = user_data;
  GeoclueStumblerHttpProxy *self;
  GBytes *bytes;
  SoupMessage *msg;
  g_autofree char *absolute_file_path = NULL;
  g_autoptr(GError) error = NULL;

  self = payload->self;
  absolute_file_path = g_strdup (payload->absolute_file_path);
  message_payload_free (payload);

  bytes = soup_session_send_and_read_finish (session, result, &error);
  if (error) {
    g_warning ("Error in Uploading: %s", error->message);
    emit_upload_error (self);
    return;
  }

  msg = soup_session_get_async_result_message (session, result);
  if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg))) {
    g_warning ("Fail to upload (http status = %03u)", soup_message_get_status (msg));
    emit_upload_error (self);
  } else {
    g_debug ("Upload successful!");
    emit_upload_success (self);
    unlink (absolute_file_path);
  }

  g_bytes_unref (bytes);
  g_clear_object (&msg);
}

static gboolean
upload_submissions_timeout (gpointer user_data)
{
  GeoclueStumblerHttpProxy *self = user_data;
  GDir *dir;
  const char *file_path;

  dir = g_dir_open (self->save_path, 0, NULL);
  if (dir == NULL) {
    g_warning ("Cannot open directory: %s", self->save_path);
    return G_SOURCE_REMOVE;
  }

  g_debug ("Submitting Data");
  while ((file_path = g_dir_read_name (dir)) != NULL) {
     GFile *file;
     g_autofree char *file_contents = NULL;
     gsize file_contents_length;
     g_autoptr(GError) error = NULL;
     g_autofree char *absolute_file_path = NULL;
     SoupMessage *msg;
     GBytes *request_body = NULL;
     message_payload *payload;
     gboolean parsing_success = FALSE;

     g_debug ("Processing file: %s", file_path);

     absolute_file_path = g_build_filename (self->save_path , file_path, NULL);
     file = g_file_new_for_path (absolute_file_path);
     g_file_load_contents (file, NULL, &file_contents, &file_contents_length, NULL, &error);
     g_clear_object (&file);

    if (error != NULL) {
      g_warning ("Error in loading contents: %s\n", error->message);
      g_clear_error (&error);
      continue;
    }

    if (file_contents_length == 0) {
      g_warning ("Empty File");
      unlink (absolute_file_path);
      continue;
    }

    /* Check that the data is a valid JSON object */
    parsing_success = json_parser_load_from_data(self->parser, file_contents, file_contents_length, &error);

    if (error) {
      g_warning ("Error parsing JSON text: %s\n", error->message);
      g_clear_error (&error);
      unlink (absolute_file_path);
      continue;
    }

    if (!parsing_success) {
      g_warning ("Error parsing JSON text");
      unlink (absolute_file_path);
      continue;
    }

    msg = soup_message_new (SOUP_METHOD_POST, self->submission_url);
    request_body = g_bytes_new (file_contents, file_contents_length);
    soup_message_set_request_body_from_bytes (msg, "application/json", request_body);

    payload = g_try_new0 (message_payload, 1);
    payload->self = self;
    payload->absolute_file_path = g_strdup (absolute_file_path);

    soup_session_send_and_read_async (self->web, msg, G_PRIORITY_DEFAULT, NULL,
                                      (GAsyncReadyCallback)on_upload_callback, payload);
    g_debug ("POST %" G_GSIZE_FORMAT " bytes to %s", file_contents_length, self->submission_url);

    g_bytes_unref (request_body);
  }

  g_dir_close (dir);

  return G_SOURCE_REMOVE;
}

static void
handle_method_call_service (GDBusConnection       *connection,
                            const gchar           *sender,
                            const gchar           *object_path,
                            const gchar           *interface_name,
                            const gchar           *method_name,
                            GVariant              *parameters,
                            GDBusMethodInvocation *invocation,
                            gpointer               user_data)
{
  if (g_strcmp0 (method_name, "SubmitSubmissions") == 0) {
    GeoclueStumblerHttpProxy *self = user_data;
    GVariant *variantstatus;
    char *dict, *url;
    g_variant_get (parameters, "(sv)", &dict, &variantstatus);

    if (g_strcmp0 (dict, "URL") == 0) {
      g_variant_get (variantstatus, "s", &url);
      g_free (self->submission_url);
      self->submission_url = g_strdup (url);
      g_debug ("URL To submit is: %s", self->submission_url);

      g_timeout_add (100, upload_submissions_timeout, self);

      g_dbus_method_invocation_return_value (invocation, NULL);
    } else {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_INVALID_ARGS,
                                             "Cannot find the Property requested!");
      return;
    }

    return;
  } else {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_INVALID_ARGS,
                                             "Cannot find the method requested!");
      return;
  }
}

static void
emit_submission_added (GeoclueStumblerHttpProxy *self, const char *data)
{
  GDBusConnection *connection = geoclue_stumbler_dbus_get_connection ();
  g_autoptr(GError) error = NULL;
  GVariant *changedproperty;

  changedproperty = g_variant_new_parsed ("('submission', <%s>)", data);

  g_dbus_connection_emit_signal (connection,
                                 NULL,
                                 GEOCLUE_STUMBLER_PATH,
                                 GEOCLUE_STUMBLER_SERVICE,
                                 "SubmissionAdded",
                                 changedproperty,
                                 &error);

  if (error != NULL) {
    g_warning ("Error in Signal Emitting: %s\n", error->message);
    error = NULL;
  }
}

static char *
get_os_info (void)
{
  g_autofree char *pretty_name = NULL;
  g_autofree char *os_name = g_get_os_info (G_OS_INFO_KEY_NAME);
  g_autofree char *os_version = g_get_os_info (G_OS_INFO_KEY_VERSION);

  if (os_name && os_version)
    return g_strdup_printf ("%s; %s", os_name, os_version);

  pretty_name = g_get_os_info (G_OS_INFO_KEY_PRETTY_NAME);
  if (pretty_name)
    return g_steal_pointer (&pretty_name);

  /* Translators: Not marked as translatable as debug output should stay English */
  return g_strdup ("Unknown");
}

#define USER_AGENT (PACKAGE_NAME "/" PACKAGE_VERSION)

static char *
get_user_agent (void)
{
  g_autofree char *os_info = get_os_info ();
  return g_strdup_printf ("%s (%s)", USER_AGENT, os_info);
}

static void
server_callback (SoupServer        *server,
                 SoupServerMessage *msg,
                 const char        *path,
                 GHashTable        *query,
                 gpointer           user_data)
{
  GeoclueStumblerHttpProxy *self = user_data;
  SoupMessageBody *body = NULL;
  g_autofree char *file_save_path = NULL;
  g_autofree char *uuid = NULL;
  g_autoptr(GError) error = NULL;
  g_autoptr(GFile) file = NULL;

  gboolean parsing_success = FALSE;

  if (g_strcmp0 (soup_server_message_get_method (msg), "POST") != 0) {
    soup_server_message_set_status (msg, SOUP_STATUS_BAD_REQUEST, NULL);
    g_debug ("Returning Bad Request");
    return;
  }

  body = soup_server_message_get_request_body (msg);
  /* Check that the data is a valid JSON object */

  if (body->length == 0) {
    g_debug("Empty body");
    soup_server_message_set_status (msg, SOUP_STATUS_BAD_REQUEST, NULL);
    return;
  }

  parsing_success = json_parser_load_from_data(self->parser, body->data, body->length, &error);

  if (error) {
    g_warning ("Error parsing JSON text: %s\n", error->message);
    soup_server_message_set_status (msg, SOUP_STATUS_BAD_REQUEST, NULL);
    return;
  }

  if (!parsing_success) {
    g_warning ("Error parsing JSON text");
    soup_server_message_set_status (msg, SOUP_STATUS_BAD_REQUEST, NULL);
    return;
  }

  uuid = g_uuid_string_random ();
  file_save_path = g_build_filename (self->save_path , uuid, NULL);

  file = g_file_new_for_path (file_save_path);

  /* Doing this async causes a segfault with the GFile and the
   * first few bytes are corrupted. The JSON file is small enough
   * that it shouldn't matter
   */
  g_file_replace_contents (file,
                           body->data,
                           body->length,
                           NULL,
                           FALSE,
                           G_FILE_CREATE_PRIVATE | G_FILE_CREATE_REPLACE_DESTINATION,
                           NULL,
                           NULL,
                           NULL);

  soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);

  emit_submission_added (self, body->data);
}

static const GDBusInterfaceVTable interface_vtable_service = {
  handle_method_call_service
};

static void
geoclue_stumbler_httpproxy_finalize (GObject *object)
{
  GeoclueStumblerHttpProxy *self = (GeoclueStumblerHttpProxy *)object;
  GDBusConnection *connection = geoclue_stumbler_dbus_get_connection ();

  g_free (self->save_path);
  g_clear_object (&self->server);
  g_clear_object (&self->parser);

  //Disconnect the service dbus interface
  g_dbus_connection_unregister_object (connection,
                                       self->service_registration_id);

  G_OBJECT_CLASS (geoclue_stumbler_httpproxy_parent_class)->finalize (object);
}

static void
geoclue_stumbler_httpproxy_class_init (GeoclueStumblerHttpProxyClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = geoclue_stumbler_httpproxy_finalize;
}

static void
geoclue_stumbler_httpproxy_init (GeoclueStumblerHttpProxy *self)
{
  GDBusConnection *connection = geoclue_stumbler_dbus_get_connection ();
  GDBusNodeInfo   *introspection_data = geoclue_stumbler_dbus_get_introspection_data ();
  g_autofree char *user_agent = NULL;
  g_autoptr(GError) error = NULL;

  self->server = soup_server_new ("server-header",
                                  "geoclue-stumbler ",
                                  NULL);

  soup_server_listen_local (self->server,
                            PROXY_PORT,
                            (SoupServerListenOptions) 0,
                            &error);

  if (error) {
    g_warning ("Error listening on local: %s\n", error->message);
    return;
  }

  soup_server_add_handler (self->server,
                           "/v2/geosubmit",
                           server_callback,
                           self,
                           NULL);

  self->web = soup_session_new ();
  user_agent = get_user_agent ();
  g_debug ("User agent: %s", user_agent);
  soup_session_set_user_agent (self->web, user_agent);

  self->service_registration_id = g_dbus_connection_register_object (connection,
                                                                     GEOCLUE_STUMBLER_PATH,
                                                                     introspection_data->interfaces[0],
                                                                     &interface_vtable_service,
                                                                     self,         // user_data
                                                                     NULL,    // user_data_free_func
                                                                     &error);   // GError**

  if (error)
    g_critical ("Error Registering Dbus: %s", error->message);

}

GeoclueStumblerHttpProxy *
geoclue_stumbler_httpproxy_new (const char *save_path, gboolean debug)
{
  GeoclueStumblerHttpProxy *self;

  self = g_object_new (GEOCLUE_STUMBLER_TYPE_HTTPPROXY, NULL);
  self->save_path = g_strdup (save_path);
  self->parser = json_parser_new ();
  self->submission_url = NULL;
  self->debug = debug;

  if (self->debug) {
      SoupLogger *logger;
      logger = soup_logger_new (SOUP_LOGGER_LOG_BODY);
      soup_session_add_feature (self->web,
                                SOUP_SESSION_FEATURE (logger));
      g_object_unref (logger);
  }

  return self;
}
