/*
   This file is part of GNUnet.
   Copyright (C) 2012-2015 GNUnet e.V.

   GNUnet is free software: you can redistribute it and/or modify it
   under the terms of the GNU Affero General Public License as published
   by the Free Software Foundation, either version 3 of the License,
   or (at your option) any later version.

   GNUnet 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
   Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */
/**
 * @author Martin Schanzenbach
 * @author Philippe Buschmann
 * @file peerinfo/plugin_rest_peerinfo.c
 * @brief GNUnet Peerinfo REST plugin
 */

#include "platform.h"
#include "gnunet_rest_plugin.h"
#include "gnunet_peerinfo_service.h"
#include "gnunet_transport_service.h"
#include "gnunet_rest_lib.h"
#include "gnunet_json_lib.h"
#include "microhttpd.h"
#include <jansson.h>

/**
 * Peerinfo Namespace
 */
#define GNUNET_REST_API_NS_PEERINFO "/peerinfo"

/**
 * Peerinfo parameter peer
 */
#define GNUNET_REST_PEERINFO_PEER "peer"

/**
 * Peerinfo parameter friend
 */
#define GNUNET_REST_PEERINFO_FRIEND "friend"

/**
 * Peerinfo parameter array
 */
#define GNUNET_REST_PEERINFO_ARRAY "array"

/**
 * Error message Unknown Error
 */
#define GNUNET_REST_PEERINFO_ERROR_UNKNOWN "Unknown Error"

/**
 * How long until we time out during address lookup?
 */
#define TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 5)
/**
 * The configuration handle
 */
const struct GNUNET_CONFIGURATION_Handle *cfg;

/**
 * HTTP methods allows for this plugin
 */
static char *allow_methods;

/**
 * Handle to PEERINFO
 */
static struct GNUNET_PEERINFO_Handle *peerinfo_handle;

/**
 * @brief struct returned by the initialization function of the plugin
 */
struct Plugin
{
  const struct GNUNET_CONFIGURATION_Handle *cfg;
};


/**
 * Record we keep for each printable address.
 */
struct AddressRecord
{
  /**
   * Current address-to-string context (if active, otherwise NULL).
   */
  struct GNUNET_TRANSPORT_AddressToStringContext *atsc;

  /**
   * Address expiration time
   */
  struct GNUNET_TIME_Absolute expiration;

  /**
   * Printable address.
   */
  char *result;

  /**
   * Print context this address record belongs to.
   */
  struct PrintContext *pc;
};


/**
 * Structure we use to collect printable address information.
 */
struct PrintContext
{
  /**
   * Kept in DLL.
   */
  struct PrintContext *next;

  /**
   * Kept in DLL.
   */
  struct PrintContext *prev;

  /**
   * Identity of the peer.
   */
  struct GNUNET_PeerIdentity peer;

  /**
   * List of printable addresses.
   */
  struct AddressRecord *address_list;

  /**
   * Number of completed addresses in @e address_list.
   */
  unsigned int num_addresses;

  /**
   * Number of addresses allocated in @e address_list.
   */
  unsigned int address_list_size;

  /**
   * Current offset in @e address_list (counted down).
   */
  unsigned int off;

  /**
   * Hello was friend only, #GNUNET_YES or #GNUNET_NO
   */
  int friend_only;

  /**
   * RequestHandle
   */
  struct RequestHandle *handle;
};

/**
 * Head of list of print contexts.
 */
static struct PrintContext *pc_head;

/**
 * Tail of list of print contexts.
 */
static struct PrintContext *pc_tail;

/**
 * The request handle
 */
struct RequestHandle
{
  /**
   * DLL
   */
  struct RequestHandle *next;

  /**
   * DLL
   */
  struct RequestHandle *prev;

  /**
   * JSON temporary array
   */
  json_t *temp_array;

  /**
   * Expiration time string
   */
  char *expiration_str;

  /**
   * Address string
   */
  const char *address;

  /**
   * Iteration peer public key
   */
  char *pubkey;

  /**
   * JSON response
   */
  json_t *response;

  /**
   * Handle to PEERINFO it
   */
  struct GNUNET_PEERINFO_IteratorContext *list_it;


  /**
   * Rest connection
   */
  struct GNUNET_REST_RequestHandle *rest_handle;

  /**
   * Desired timeout for the lookup (default is no timeout).
   */
  struct GNUNET_TIME_Relative timeout;

  /**
   * ID of a task associated with the resolution process.
   */
  struct GNUNET_SCHEDULER_Task *timeout_task;

  /**
   * The plugin result processor
   */
  GNUNET_REST_ResultProcessor proc;

  /**
   * The closure of the result processor
   */
  void *proc_cls;

  /**
   * The url
   */
  char *url;

  /**
   * Error response message
   */
  char *emsg;

  /**
   * Response code
   */
  int response_code;
};

/**
 * DLL
 */
static struct RequestHandle *requests_head;

/**
 * DLL
 */
static struct RequestHandle *requests_tail;

/**
 * Cleanup lookup handle
 * @param handle Handle to clean up
 */
static void
cleanup_handle (void *cls)
{
  struct RequestHandle *handle = cls;

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Cleaning up\n");
  if (NULL != handle->timeout_task)
  {
    GNUNET_SCHEDULER_cancel (handle->timeout_task);
    handle->timeout_task = NULL;
  }
  if (NULL != handle->url)
    GNUNET_free (handle->url);
  if (NULL != handle->emsg)
    GNUNET_free (handle->emsg);
  if (NULL != handle->address)
    GNUNET_free_nz ((char *) handle->address);
  if (NULL != handle->expiration_str)
    GNUNET_free (handle->expiration_str);
  if (NULL != handle->pubkey)
    GNUNET_free (handle->pubkey);

  if (NULL != handle->temp_array)
  {
    json_decref (handle->temp_array);
    handle->temp_array = NULL;
  }
  if (NULL != handle->response)
  {
    json_decref (handle->response);
    handle->response = NULL;
  }

  if (NULL != handle->list_it)
  {
    GNUNET_PEERINFO_iterate_cancel (handle->list_it);
    handle->list_it = NULL;
  }
  if (NULL != peerinfo_handle)
  {
    GNUNET_PEERINFO_disconnect (peerinfo_handle);
    peerinfo_handle = NULL;
  }
  GNUNET_CONTAINER_DLL_remove (requests_head,
                               requests_tail,
                               handle);
  GNUNET_free (handle);
}


/**
 * Task run on errors.  Reports an error and cleans up everything.
 *
 * @param cls the `struct RequestHandle`
 */
static void
do_error (void *cls)
{
  struct RequestHandle *handle = cls;
  struct MHD_Response *resp;
  json_t *json_error = json_object ();
  char *response;

  if (NULL == handle->emsg)
    handle->emsg = GNUNET_strdup (GNUNET_REST_PEERINFO_ERROR_UNKNOWN);

  json_object_set_new (json_error, "error", json_string (handle->emsg));

  if (0 == handle->response_code)
    handle->response_code = MHD_HTTP_OK;
  response = json_dumps (json_error, 0);
  resp = GNUNET_REST_create_response (response);
  MHD_add_response_header (resp, "Content-Type", "application/json");
  handle->proc (handle->proc_cls, resp, handle->response_code);
  json_decref (json_error);
  GNUNET_free (response);
  GNUNET_SCHEDULER_add_now (&cleanup_handle, handle);
}


/**
 * Function that assembles the response.
 *
 * @param cls the `struct RequestHandle`
 */
static void
peerinfo_list_finished (void *cls)
{
  struct RequestHandle *handle = cls;
  char *result_str;
  struct MHD_Response *resp;

  if (NULL == handle->response)
  {
    handle->response_code = MHD_HTTP_NOT_FOUND;
    handle->emsg = GNUNET_strdup ("No peers found");
    GNUNET_SCHEDULER_add_now (&do_error, handle);
    return;
  }

  result_str = json_dumps (handle->response, 0);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Result %s\n", result_str);
  resp = GNUNET_REST_create_response (result_str);
  GNUNET_assert (MHD_NO != MHD_add_response_header (resp,
                                                    "Content-Type",
                                                    "application/json"));
  handle->proc (handle->proc_cls, resp, MHD_HTTP_OK);
  GNUNET_free (result_str);
  GNUNET_SCHEDULER_add_now (&cleanup_handle, handle);
}


/**
 * Iterator callback to go over all addresses and count them.
 *
 * @param cls `struct PrintContext *` with `off` to increment
 * @param address the address
 * @param expiration expiration time
 * @return #GNUNET_OK to keep the address and continue
 */
static int
count_address (void *cls,
               const struct GNUNET_HELLO_Address *address,
               struct GNUNET_TIME_Absolute expiration)
{
  struct PrintContext *pc = cls;

  if (0 == GNUNET_TIME_absolute_get_remaining (expiration).rel_value_us)
  {
    return GNUNET_OK;          /* ignore expired address */
  }

  pc->off++;
  return GNUNET_OK;
}


/**
 * Print the collected address information to the console and free @a pc.
 *
 * @param pc printing context
 */
static void
dump_pc (struct PrintContext *pc)
{
  struct RequestHandle *handle;
  unsigned int i;
  json_t *response_entry;
  json_t *temp_array;
  json_t *object;
  json_t *address;
  json_t *expires;
  json_t *friend_and_peer_json;
  char *friend_and_peer;

  temp_array = json_array ();
  response_entry = json_object ();

  for (i = 0; i < pc->num_addresses; i++)
  {
    if (NULL != pc->address_list[i].result)
    {
      object = json_object ();
      address = json_string (pc->address_list[i].result);
      expires = json_string (
        GNUNET_STRINGS_absolute_time_to_string (
          pc->address_list[i].expiration));
      json_object_set (object, "address", address);
      json_object_set (object, "expires", expires);

      json_decref (address);
      json_decref (expires);

      json_array_append (temp_array, object);
      json_decref (object);
      GNUNET_free (pc->address_list[i].result);
    }
  }

  if (0 < json_array_size (temp_array))
  {
    GNUNET_asprintf (&friend_and_peer,
                     "%s%s",
                     (GNUNET_YES == pc->friend_only) ? "F2F:" : "",
                     GNUNET_i2s_full (&pc->peer));
    friend_and_peer_json = json_string (friend_and_peer);
    json_object_set (response_entry,
                     GNUNET_REST_PEERINFO_PEER,
                     friend_and_peer_json);
    json_object_set (response_entry,
                     GNUNET_REST_PEERINFO_ARRAY,
                     temp_array);
    json_array_append (pc->handle->response, response_entry);
    json_decref (friend_and_peer_json);
    GNUNET_free (friend_and_peer);
  }

  json_decref (temp_array);
  json_decref (response_entry);

  GNUNET_free (pc->address_list);
  GNUNET_CONTAINER_DLL_remove (pc_head,
                               pc_tail,
                               pc);
  handle = pc->handle;
  GNUNET_free (pc);

  if ((NULL == pc_head) &&
      (NULL == handle->list_it))
  {
    GNUNET_SCHEDULER_add_now (&peerinfo_list_finished, handle);
  }
}


/**
 * Function to call with a human-readable format of an address
 *
 * @param cls closure
 * @param address NULL on error, otherwise 0-terminated printable UTF-8 string
 * @param res result of the address to string conversion:
 *        if #GNUNET_OK: address was valid (conversion to
 *                       string might still have failed)
 *        if #GNUNET_SYSERR: address is invalid
 */
static void
process_resolved_address (void *cls,
                          const char *address,
                          int res)
{
  struct AddressRecord *ar = cls;
  struct PrintContext *pc = ar->pc;

  if (NULL != address)
  {
    if (0 != strlen (address))
    {
      if (NULL != ar->result)
        GNUNET_free (ar->result);
      ar->result = GNUNET_strdup (address);
    }
    return;
  }
  ar->atsc = NULL;
  if (GNUNET_SYSERR == res)
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                _ ("Failure: Cannot convert address to string for peer `%s'\n"),
                GNUNET_i2s (&ar->pc->peer));
  pc->num_addresses++;
  if (pc->num_addresses == pc->address_list_size)
    dump_pc (ar->pc);
}


/**
 * Iterator callback to go over all addresses.
 *
 * @param cls closure
 * @param address the address
 * @param expiration expiration time
 * @return #GNUNET_OK to keep the address and continue
 */
static int
print_address (void *cls,
               const struct GNUNET_HELLO_Address *address,
               struct GNUNET_TIME_Absolute expiration)
{
  struct PrintContext *pc = cls;
  struct AddressRecord *ar;

  if (0 == GNUNET_TIME_absolute_get_remaining (expiration).rel_value_us)
  {
    return GNUNET_OK;          /* ignore expired address */
  }

  GNUNET_assert (0 < pc->off);
  ar = &pc->address_list[--pc->off];
  ar->pc = pc;
  ar->expiration = expiration;
  GNUNET_asprintf (&ar->result,
                   "%s:%lu:%u",
                   address->transport_name,
                   (unsigned long) address->address_length,
                   address->local_info);
  ar->atsc = GNUNET_TRANSPORT_address_to_string (cfg,
                                                 address,
                                                 GNUNET_NO,
                                                 TIMEOUT,
                                                 &process_resolved_address,
                                                 ar);
  return GNUNET_OK;
}


/**
 * Callback that processes each of the known HELLOs for the
 * iteration response construction.
 *
 * @param cls closure, NULL
 * @param peer id of the peer, NULL for last call
 * @param hello hello message for the peer (can be NULL)
 * @param err_msg message
 */
void
peerinfo_list_iteration (void *cls,
                         const struct GNUNET_PeerIdentity *peer,
                         const struct GNUNET_HELLO_Message *hello,
                         const char *err_msg)
{
  struct RequestHandle *handle = cls;
  struct PrintContext *pc;
  int friend_only;

  if (NULL == handle->response)
  {
    handle->response = json_array ();
  }

  if (NULL == peer)
  {
    handle->list_it = NULL;
    handle->emsg = GNUNET_strdup ("Error in communication with peerinfo");
    if (NULL != err_msg)
    {
      GNUNET_free (handle->emsg);
      handle->emsg = GNUNET_strdup (err_msg);
      handle->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
    }
    if (NULL == pc_head)
      GNUNET_SCHEDULER_add_now (&do_error, handle);
    return;
  }
  if (NULL == hello)
    return;

  friend_only = GNUNET_NO;
  if (NULL != hello)
    friend_only = GNUNET_HELLO_is_friend_only (hello);

  pc = GNUNET_new (struct PrintContext);
  GNUNET_CONTAINER_DLL_insert (pc_head,
                               pc_tail,
                               pc);
  pc->peer = *peer;
  pc->friend_only = friend_only;
  pc->handle = handle;
  GNUNET_HELLO_iterate_addresses (hello,
                                  GNUNET_NO,
                                  &count_address,
                                  pc);
  if (0 == pc->off)
  {
    dump_pc (pc);
    return;
  }
  pc->address_list_size = pc->off;
  pc->address_list = GNUNET_malloc (
    sizeof(struct AddressRecord) * pc->off);
  GNUNET_HELLO_iterate_addresses (hello,
                                  GNUNET_NO,
                                  &print_address,
                                  pc);
}


/**
 * Handle peerinfo GET request
 *
 * @param con_handle the connection handle
 * @param url the url
 * @param cls the RequestHandle
 */
void
peerinfo_get (struct GNUNET_REST_RequestHandle *con_handle,
              const char*url,
              void *cls)
{
  struct RequestHandle *handle = cls;
  struct GNUNET_HashCode key;
  const struct GNUNET_PeerIdentity *specific_peer;
  // GNUNET_PEER_Id peer_id;
  int include_friend_only;
  char*include_friend_only_str;

  include_friend_only = GNUNET_NO;
  GNUNET_CRYPTO_hash (GNUNET_REST_PEERINFO_FRIEND,
                      strlen (GNUNET_REST_PEERINFO_FRIEND),
                      &key);
  if (GNUNET_YES
      == GNUNET_CONTAINER_multihashmap_contains (con_handle->url_param_map,
                                                 &key))
  {
    include_friend_only_str = GNUNET_CONTAINER_multihashmap_get (
      con_handle->url_param_map, &key);
    if (0 == strcmp (include_friend_only_str, "yes"))
    {
      include_friend_only = GNUNET_YES;
    }
  }

  specific_peer = NULL;
  GNUNET_CRYPTO_hash (GNUNET_REST_PEERINFO_PEER,
                      strlen (GNUNET_REST_PEERINFO_PEER),
                      &key);
  if (GNUNET_YES
      == GNUNET_CONTAINER_multihashmap_contains (con_handle->url_param_map,
                                                 &key))
  {
    // peer_id = *(unsigned int*)GNUNET_CONTAINER_multihashmap_get (con_handle->url_param_map, &key);
    // specific_peer = GNUNET_PEER_resolve2(peer_id);
  }

  handle->list_it = GNUNET_PEERINFO_iterate (peerinfo_handle,
                                             include_friend_only,
                                             specific_peer,
                                             &peerinfo_list_iteration,
                                             handle);
}


/**
 * Respond to OPTIONS request
 *
 * @param con_handle the connection handle
 * @param url the url
 * @param cls the RequestHandle
 */
static void
options_cont (struct GNUNET_REST_RequestHandle *con_handle,
              const char*url,
              void *cls)
{
  struct MHD_Response *resp;
  struct RequestHandle *handle = cls;

  // independent of path return all options
  resp = GNUNET_REST_create_response (NULL);
  MHD_add_response_header (resp,
                           "Access-Control-Allow-Methods",
                           allow_methods);
  handle->proc (handle->proc_cls, resp, MHD_HTTP_OK);
  GNUNET_SCHEDULER_add_now (&cleanup_handle, handle);
  return;
}


/**
 * Function processing the REST call
 *
 * @param method HTTP method
 * @param url URL of the HTTP request
 * @param data body of the HTTP request (optional)
 * @param data_size length of the body
 * @param proc callback function for the result
 * @param proc_cls closure for callback function
 * @return GNUNET_OK if request accepted
 */
static enum GNUNET_GenericReturnValue
rest_process_request (struct GNUNET_REST_RequestHandle *rest_handle,
                      GNUNET_REST_ResultProcessor proc,
                      void *proc_cls)
{
  struct RequestHandle *handle = GNUNET_new (struct RequestHandle);
  struct GNUNET_REST_RequestHandlerError err;
  static const struct GNUNET_REST_RequestHandler handlers[] = {
    { MHD_HTTP_METHOD_GET, GNUNET_REST_API_NS_PEERINFO, &peerinfo_get },
    { MHD_HTTP_METHOD_OPTIONS, GNUNET_REST_API_NS_PEERINFO, &options_cont },
    GNUNET_REST_HANDLER_END
  };

  handle->response_code = 0;
  handle->timeout = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS,
                                                   60);
  handle->proc_cls = proc_cls;
  handle->proc = proc;
  handle->rest_handle = rest_handle;

  handle->url = GNUNET_strdup (rest_handle->url);
  if (handle->url[strlen (handle->url) - 1] == '/')
    handle->url[strlen (handle->url) - 1] = '\0';
  handle->timeout_task =
    GNUNET_SCHEDULER_add_delayed (handle->timeout,
                                  &do_error,
                                  handle);
  GNUNET_CONTAINER_DLL_insert (requests_head,
                               requests_tail,
                               handle);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Connecting...\n");
  if (GNUNET_NO == GNUNET_REST_handle_request (handle->rest_handle,
                                               handlers,
                                               &err,
                                               handle))
  {
    cleanup_handle (handle);
    return GNUNET_NO;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Connected\n");
  return GNUNET_YES;
}


/**
 * Entry point for the plugin.
 *
 * @param cls Config info
 * @return NULL on error, otherwise the plugin context
 */
void *
libgnunet_plugin_rest_peerinfo_init (void *cls)
{
  static struct Plugin plugin;
  struct GNUNET_REST_Plugin *api;

  cfg = cls;
  if (NULL != plugin.cfg)
    return NULL;                /* can only initialize once! */
  memset (&plugin, 0, sizeof(struct Plugin));
  plugin.cfg = cfg;
  api = GNUNET_new (struct GNUNET_REST_Plugin);
  api->cls = &plugin;
  api->name = GNUNET_REST_API_NS_PEERINFO;
  api->process_request = &rest_process_request;
  GNUNET_asprintf (&allow_methods,
                   "%s, %s, %s, %s, %s",
                   MHD_HTTP_METHOD_GET,
                   MHD_HTTP_METHOD_POST,
                   MHD_HTTP_METHOD_PUT,
                   MHD_HTTP_METHOD_DELETE,
                   MHD_HTTP_METHOD_OPTIONS);
  peerinfo_handle = GNUNET_PEERINFO_connect (cfg);

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              _ ("Peerinfo REST API initialized\n"));
  return api;
}


/**
 * Exit point from the plugin.
 *
 * @param cls the plugin context (as returned by "init")
 * @return always NULL
 */
void *
libgnunet_plugin_rest_peerinfo_done (void *cls)
{
  struct GNUNET_REST_Plugin *api = cls;
  struct Plugin *plugin = api->cls;

  plugin->cfg = NULL;
  while (NULL != requests_head)
    cleanup_handle (requests_head);
  if (NULL != peerinfo_handle)
    GNUNET_PEERINFO_disconnect (peerinfo_handle);

  GNUNET_free (allow_methods);
  GNUNET_free (api);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Peerinfo REST plugin is finished\n");
  return NULL;
}


/* end of plugin_rest_peerinfo.c */
