/* GNU Guix --- Functional package management for GNU
   Copyright © 2020 Maxime Devos <maxime.devos@student.kuleuven.be>

   This file is part of GNU Guix.

   GNU Guix 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.

   GNU Guix 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 GNU Guix.  If not, see <http://www.gnu.org/licenses/>. */

#include <stdio.h>
#include <gnunet/gnunet_config.h>
#include <gnunet/gnunet_service_lib.h>
#include <gnunet/gnunet_dht_service.h>
#include "rehash_service.h"
#include "extra_gnunet_protocols.h"
#include "rehash.h"
#include "rehash_crypto.h"
#include "rehash_dht.h"

static struct GNUNET_DHT_Handle *dht_handle;

/**
 * Callback to initialise the rehash service
 */
static void
init_cb(void *cls, const struct GNUNET_CONFIGURATION_Handle *cfg,
        struct GNUNET_SERVICE_Handle *sh)
{
  /* TODO: reasonable length */
  dht_handle = GNUNET_DHT_connect(cfg, 16);
  /* TODO: what to do */
  GNUNET_assert(dht_handle != NULL);
}

/**
 * Callback for when a client connects to the service.
 */
static void *
connect_cb(void *cls, struct GNUNET_SERVICE_Client *c,
           struct GNUNET_MQ_Handle *mq)
{
  return c;
}

/**
 * Callback for when a client is disconnected from the service.
 */
static void
disconnect_cb(void *cls, struct GNUNET_SERVICE_Client *c, void *internal_cls)
{
}

static int
check_get (void *cls, const struct GNUNET_REHASH_GetMessage *msg)
{
  uint16_t header_size;
  uint32_t input_length; /* TODO perhaps uint16_t */
  header_size = ntohs(msg->header.size);
  input_length = ntohl(msg->input_length);
  if (header_size - sizeof(struct GNUNET_REHASH_GetMessage) != input_length)
  {
    GNUNET_break(0);
    return GNUNET_SYSERR;
  }

  /* Detect unsupported options */
  if (ntohl(msg->options) > 1)
  {
    GNUNET_break(0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}

static void
dht_get_iterator(void *cls,
                 struct GNUNET_TIME_Absolute exp,
                 const struct GNUNET_HashCode *key,
                 const struct GNUNET_PeerIdentity *get_path,
                 unsigned int get_path_length,
                 const struct GNUNET_PeerIdentity *put_path,
                 unsigned int put_path_length,
                 enum GNUNET_BLOCK_Type type,
                 size_t size,
                 const void *data)
{
  struct GNUNET_SERVICE_Client *client = cls;
  /* TODO: who should free get_path, put_path?*/
  /* TODO: send a GNUNET_REHASH_ResultMessage */
  struct GNUNET_MQ_Envelope *ev;
  struct GNUNET_REHASH_ResultMessage *msg;
  /* Prevent buffer overflow.
     TODO: less magic constants! */
  if (size > 64)
  {
    GNUNET_break(0);
    return;
  }

  /* Inform the client of the found message */
  ev = GNUNET_MQ_msg_extra (msg, size, GNUNET_MESSAGE_TYPE_REHASH_CLIENT_RESULT);
  msg->output_length = htonl (size);
  msg->exp = GNUNET_TIME_absolute_hton (exp);
  memcpy(&msg[1], data, size);

  GNUNET_MQ_send(GNUNET_SERVICE_client_get_mq(client), ev);
}

static void
handle_get (void *cls, const struct GNUNET_REHASH_GetMessage *msg)
{
  struct GNUNET_SERVICE_Client *c = cls;
  struct GNUNET_DHT_GetHandle *h;
  struct GNUNET_HashCode key;
  /* TODO: define protocols for anonymous GET */
  GNUNET_assert(ntohl(msg->anonymity_level) == 0);

  if (GNUNET_OK !=
      GNUNET_REHASH_obfuscated_query_from_hash
      (ntohl(msg->out_type),
       ntohl(msg->in_type),
       (const char *) &msg[1],
       ntohl(msg->input_length),
       &key))
    /* TODO bweh? disconnect? */
    return;

  /* TODO: desired replication level */
  h = GNUNET_REHASH_dht_get_start (dht_handle,
                                   &key,
                                   1,
                                   GNUNET_DHT_RO_NONE,
                                   &dht_get_iterator,
                                   cls);
  GNUNET_SERVICE_client_continue(c);
  /* TODO: free h eventually */
}

static int
check_get_stop (void *cls, const struct GNUNET_REHASH_GetStopMessage *msg)
{
  GNUNET_assert(0);
}

static void
handle_get_stop (void *cls, const struct GNUNET_REHASH_GetStopMessage *msg)
{
}

static int
check_put (void *cls, const struct GNUNET_REHASH_PutMessage *msg)
{
  uint32_t input_length;
  uint32_t output_length;
  input_length = ntohl(msg->input_length);
  output_length = ntohl(msg->output_length);
  /* Prevent overflow TODO no magic values */
  if (input_length > 64)
    return GNUNET_SYSERR;
  if (output_length > 64)
    return GNUNET_SYSERR;
  if (input_length + output_length + sizeof(*msg)
      != msg->header.size)
    return GNUNET_SYSERR;
  /* TODO prevent saving of hashes of incorrect lengths */
  return GNUNET_OK;
}

static void
handle_put (void *cls, const struct GNUNET_REHASH_PutMessage *msg)
{
  struct GNUNET_DHT_PutHandle *h;
  struct GNUNET_HashCode query;
  char *dest;
  const char *out_data;
  const char *in_data;
  ssize_t expected_length;
  ssize_t serialised_length;
  GNUNET_assert(msg->anonymity_level == 0);

  /* TODO also put into datastore */
  if (GNUNET_OK !=
      GNUNET_REHASH_obfuscated_query_from_hash
      (ntohl(msg->out_type),
       ntohl(msg->in_type),
       (const char *) &msg[1],
       ntohl(msg->input_length),
       &query))
  {
    /* TODO: ? TODO */
    GNUNET_break(0);
    return;
  }

  in_data = (const char *) &msg[1];
  out_data = in_data + ntohl(msg->input_length);
  expected_length = GNUNET_REHASH_data_size_for_mapping
    (ntohl(msg->output_length));
  dest = GNUNET_malloc (expected_length);
  serialised_length = GNUNET_REHASH_data_for_mapping
    (ntohl(msg->out_type),
     ntohl(msg->in_type),
     out_data,
     in_data,
     ntohl(msg->output_length),
     ntohl(msg->input_length),
     dest,
     expected_length);
  if (serialised_length != expected_length)
  {
    GNUNET_free (dest);
    GNUNET_break (0);
    /* TODO error message */
    return;
  }
  h = GNUNET_REHASH_dht_put (dht_handle,
                             &query,
                             ntohl(msg->replication_level),
                             GNUNET_DHT_RO_NONE,
                             serialised_length,
                             dest,
                             GNUNET_TIME_absolute_ntoh(msg->expiration_time),
                             /* TODO callback */
                             NULL,
                             NULL);
  GNUNET_free(dest);
  /* TODO free h eventually */
}

GNUNET_SERVICE_MAIN
("rehash",
 GNUNET_SERVICE_OPTION_NONE,
 &init_cb,
 &connect_cb,
 &disconnect_cb,
 NULL,
 /* TODO MQ handlers! */
 GNUNET_MQ_hd_var_size (get,
                        GNUNET_MESSAGE_TYPE_REHASH_CLIENT_GET,
                        struct GNUNET_REHASH_GetMessage,
                        NULL),
 GNUNET_MQ_hd_var_size (get_stop,
                        GNUNET_MESSAGE_TYPE_REHASH_CLIENT_GET_STOP,
                        struct GNUNET_REHASH_GetStopMessage,
                        NULL),
 GNUNET_MQ_hd_var_size (put,
                        GNUNET_MESSAGE_TYPE_REHASH_CLIENT_PUT,
                        struct GNUNET_REHASH_PutMessage,
                        NULL),
 GNUNET_MQ_handler_end ());
