/*
  This file is part of TALER
  Copyright (C) 2015-2023 Taler Systems SA
  TALER 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, or (at your option) any later version.
  TALER 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
  TALER; see the file COPYING.  If not, see 
*/
/**
 * @file taler-exchange-httpd_wire.c
 * @brief Handle /wire requests
 * @author Christian Grothoff
 */
#include "platform.h"
#include 
#include "taler_dbevents.h"
#include "taler-exchange-httpd_responses.h"
#include "taler-exchange-httpd_keys.h"
#include "taler-exchange-httpd_wire.h"
#include "taler_json_lib.h"
#include "taler_mhd_lib.h"
#include 
/**
 * Information we track about wire fees.
 */
struct WireFeeSet
{
  /**
   * Kept in a DLL.
   */
  struct WireFeeSet *next;
  /**
   * Kept in a DLL.
   */
  struct WireFeeSet *prev;
  /**
   * Actual fees.
   */
  struct TALER_WireFeeSet fees;
  /**
   * Start date of fee validity (inclusive).
   */
  struct GNUNET_TIME_Timestamp start_date;
  /**
   * End date of fee validity (exclusive).
   */
  struct GNUNET_TIME_Timestamp end_date;
  /**
   * Wire method the fees apply to.
   */
  char *method;
};
/**
 * State we keep per thread to cache the /wire response.
 */
struct WireStateHandle
{
  /**
   * Cached reply for /wire response.
   */
  struct MHD_Response *wire_reply;
  /**
   * ETag for this response (if any).
   */
  char *etag;
  /**
   * head of DLL of wire fees.
   */
  struct WireFeeSet *wfs_head;
  /**
   * Tail of DLL of wire fees.
   */
  struct WireFeeSet *wfs_tail;
  /**
   * Earliest timestamp of all the wire methods when we have no more fees.
   */
  struct GNUNET_TIME_Absolute cache_expiration;
  /**
   * @e cache_expiration time, formatted.
   */
  char dat[128];
  /**
   * For which (global) wire_generation was this data structure created?
   * Used to check when we are outdated and need to be re-generated.
   */
  uint64_t wire_generation;
  /**
   * HTTP status to return with this response.
   */
  unsigned int http_status;
};
/**
 * Stores the latest generation of our wire response.
 */
static struct WireStateHandle *wire_state;
/**
 * Handler listening for wire updates by other exchange
 * services.
 */
static struct GNUNET_DB_EventHandler *wire_eh;
/**
 * Counter incremented whenever we have a reason to re-build the #wire_state
 * because something external changed.
 */
static uint64_t wire_generation;
/**
 * Free memory associated with @a wsh
 *
 * @param[in] wsh wire state to destroy
 */
static void
destroy_wire_state (struct WireStateHandle *wsh)
{
  struct WireFeeSet *wfs;
  while (NULL != (wfs = wsh->wfs_head))
  {
    GNUNET_CONTAINER_DLL_remove (wsh->wfs_head,
                                 wsh->wfs_tail,
                                 wfs);
    GNUNET_free (wfs->method);
    GNUNET_free (wfs);
  }
  MHD_destroy_response (wsh->wire_reply);
  GNUNET_free (wsh->etag);
  GNUNET_free (wsh);
}
/**
 * Function called whenever another exchange process has updated
 * the wire data in the database.
 *
 * @param cls NULL
 * @param extra unused
 * @param extra_size number of bytes in @a extra unused
 */
static void
wire_update_event_cb (void *cls,
                      const void *extra,
                      size_t extra_size)
{
  (void) cls;
  (void) extra;
  (void) extra_size;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Received /wire update event\n");
  TEH_check_invariants ();
  wire_generation++;
}
enum GNUNET_GenericReturnValue
TEH_wire_init ()
{
  struct GNUNET_DB_EventHeaderP es = {
    .size = htons (sizeof (es)),
    .type = htons (TALER_DBEVENT_EXCHANGE_KEYS_UPDATED),
  };
  wire_eh = TEH_plugin->event_listen (TEH_plugin->cls,
                                      GNUNET_TIME_UNIT_FOREVER_REL,
                                      &es,
                                      &wire_update_event_cb,
                                      NULL);
  if (NULL == wire_eh)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}
void
TEH_wire_done ()
{
  if (NULL != wire_state)
  {
    destroy_wire_state (wire_state);
    wire_state = NULL;
  }
  if (NULL != wire_eh)
  {
    TEH_plugin->event_listen_cancel (TEH_plugin->cls,
                                     wire_eh);
    wire_eh = NULL;
  }
}
/**
 * Add information about a wire account to @a cls.
 *
 * @param cls a `json_t *` object to expand with wire account details
 * @param payto_uri the exchange bank account URI to add
 * @param conversion_url URL of a conversion service, NULL if there is no conversion
 * @param debit_restrictions JSON array with debit restrictions on the account
 * @param credit_restrictions JSON array with credit restrictions on the account
 * @param master_sig master key signature affirming that this is a bank
 *                   account of the exchange (of purpose #TALER_SIGNATURE_MASTER_WIRE_DETAILS)
 */
static void
add_wire_account (void *cls,
                  const char *payto_uri,
                  const char *conversion_url,
                  const json_t *debit_restrictions,
                  const json_t *credit_restrictions,
                  const struct TALER_MasterSignatureP *master_sig)
{
  json_t *a = cls;
  if (0 !=
      json_array_append_new (
        a,
        GNUNET_JSON_PACK (
          GNUNET_JSON_pack_string ("payto_uri",
                                   payto_uri),
          GNUNET_JSON_pack_allow_null (
            GNUNET_JSON_pack_string ("conversion_url",
                                     conversion_url)),
          GNUNET_JSON_pack_array_incref ("debit_restrictions",
                                         (json_t *) debit_restrictions),
          GNUNET_JSON_pack_array_incref ("credit_restrictions",
                                         (json_t *) credit_restrictions),
          GNUNET_JSON_pack_data_auto ("master_sig",
                                      master_sig))))
  {
    GNUNET_break (0);   /* out of memory!? */
    return;
  }
}
/**
 * Closure for #add_wire_fee().
 */
struct AddContext
{
  /**
   * Wire method the fees are for.
   */
  char *wire_method;
  /**
   * Wire state we are building.
   */
  struct WireStateHandle *wsh;
  /**
   * Array to append the fee to.
   */
  json_t *a;
  /**
   * Context we hash "everything" we add into. This is used
   * to compute the etag. Technically, we only hash the
   * master_sigs, as they imply the rest.
   */
  struct GNUNET_HashContext *hc;
  /**
   * Set to the maximum end-date seen.
   */
  struct GNUNET_TIME_Absolute max_seen;
};
/**
 * Add information about a wire account to @a cls.
 *
 * @param cls a `struct AddContext`
 * @param fees the wire fees we charge
 * @param start_date from when are these fees valid (start date)
 * @param end_date until when are these fees valid (end date, exclusive)
 * @param master_sig master key signature affirming that this is the correct
 *                   fee (of purpose #TALER_SIGNATURE_MASTER_WIRE_FEES)
 */
static void
add_wire_fee (void *cls,
              const struct TALER_WireFeeSet *fees,
              struct GNUNET_TIME_Timestamp start_date,
              struct GNUNET_TIME_Timestamp end_date,
              const struct TALER_MasterSignatureP *master_sig)
{
  struct AddContext *ac = cls;
  struct WireFeeSet *wfs;
  GNUNET_CRYPTO_hash_context_read (ac->hc,
                                   master_sig,
                                   sizeof (*master_sig));
  ac->max_seen = GNUNET_TIME_absolute_max (ac->max_seen,
                                           end_date.abs_time);
  wfs = GNUNET_new (struct WireFeeSet);
  wfs->start_date = start_date;
  wfs->end_date = end_date;
  wfs->fees = *fees;
  wfs->method = GNUNET_strdup (ac->wire_method);
  GNUNET_CONTAINER_DLL_insert (ac->wsh->wfs_head,
                               ac->wsh->wfs_tail,
                               wfs);
  if (0 !=
      json_array_append_new (
        ac->a,
        GNUNET_JSON_PACK (
          TALER_JSON_pack_amount ("wire_fee",
                                  &fees->wire),
          TALER_JSON_pack_amount ("closing_fee",
                                  &fees->closing),
          GNUNET_JSON_pack_timestamp ("start_date",
                                      start_date),
          GNUNET_JSON_pack_timestamp ("end_date",
                                      end_date),
          GNUNET_JSON_pack_data_auto ("sig",
                                      master_sig))))
  {
    GNUNET_break (0);   /* out of memory!? */
    return;
  }
}
/**
 * Create the /wire response from our database state.
 *
 * @return NULL on error
 */
static struct WireStateHandle *
build_wire_state (void)
{
  json_t *wire_accounts_array;
  json_t *wire_fee_object;
  uint64_t wg = wire_generation; /* must be obtained FIRST */
  enum GNUNET_DB_QueryStatus qs;
  struct WireStateHandle *wsh;
  struct GNUNET_HashContext *hc;
  wsh = GNUNET_new (struct WireStateHandle);
  wsh->wire_generation = wg;
  wire_accounts_array = json_array ();
  GNUNET_assert (NULL != wire_accounts_array);
  qs = TEH_plugin->get_wire_accounts (TEH_plugin->cls,
                                      &add_wire_account,
                                      wire_accounts_array);
  if (0 > qs)
  {
    GNUNET_break (0);
    json_decref (wire_accounts_array);
    wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
    wsh->wire_reply
      = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED,
                              "get_wire_accounts");
    return wsh;
  }
  if (0 == json_array_size (wire_accounts_array))
  {
    json_decref (wire_accounts_array);
    wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
    wsh->wire_reply
      = TALER_MHD_make_error (TALER_EC_EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED,
                              NULL);
    return wsh;
  }
  wire_fee_object = json_object ();
  GNUNET_assert (NULL != wire_fee_object);
  wsh->cache_expiration = GNUNET_TIME_UNIT_FOREVER_ABS;
  hc = GNUNET_CRYPTO_hash_context_start ();
  {
    json_t *account;
    size_t index;
    json_array_foreach (wire_accounts_array, index, account) {
      char *wire_method;
      const char *payto_uri = json_string_value (json_object_get (account,
                                                                  "payto_uri"));
      GNUNET_assert (NULL != payto_uri);
      wire_method = TALER_payto_get_method (payto_uri);
      if (NULL == wire_method)
      {
        wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
        wsh->wire_reply
          = TALER_MHD_make_error (
              TALER_EC_EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED,
              payto_uri);
        json_decref (wire_accounts_array);
        json_decref (wire_fee_object);
        GNUNET_CRYPTO_hash_context_abort (hc);
        return wsh;
      }
      if (NULL == json_object_get (wire_fee_object,
                                   wire_method))
      {
        struct AddContext ac = {
          .wire_method = wire_method,
          .wsh = wsh,
          .a = json_array (),
          .hc = hc
        };
        GNUNET_assert (NULL != ac.a);
        qs = TEH_plugin->get_wire_fees (TEH_plugin->cls,
                                        wire_method,
                                        &add_wire_fee,
                                        &ac);
        if (0 > qs)
        {
          GNUNET_break (0);
          json_decref (ac.a);
          json_decref (wire_fee_object);
          json_decref (wire_accounts_array);
          GNUNET_free (wire_method);
          wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
          wsh->wire_reply
            = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED,
                                    "get_wire_fees");
          GNUNET_CRYPTO_hash_context_abort (hc);
          return wsh;
        }
        if (0 == json_array_size (ac.a))
        {
          json_decref (ac.a);
          json_decref (wire_accounts_array);
          json_decref (wire_fee_object);
          wsh->http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
          wsh->wire_reply
            = TALER_MHD_make_error (TALER_EC_EXCHANGE_WIRE_FEES_NOT_CONFIGURED,
                                    wire_method);
          GNUNET_free (wire_method);
          GNUNET_CRYPTO_hash_context_abort (hc);
          return wsh;
        }
        wsh->cache_expiration = GNUNET_TIME_absolute_min (ac.max_seen,
                                                          wsh->cache_expiration);
        GNUNET_assert (0 ==
                       json_object_set_new (wire_fee_object,
                                            wire_method,
                                            ac.a));
      }
      GNUNET_free (wire_method);
    }
  }
  wsh->wire_reply = TALER_MHD_MAKE_JSON_PACK (
    GNUNET_JSON_pack_array_steal ("accounts",
                                  wire_accounts_array),
    GNUNET_JSON_pack_array_steal ("wads", /* #7271 */
                                  json_array ()),
    GNUNET_JSON_pack_object_steal ("fees",
                                   wire_fee_object),
    GNUNET_JSON_pack_data_auto ("master_public_key",
                                &TEH_master_public_key));
  {
    struct GNUNET_TIME_Timestamp m;
    m = GNUNET_TIME_absolute_to_timestamp (wsh->cache_expiration);
    TALER_MHD_get_date_string (m.abs_time,
                               wsh->dat);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Setting 'Expires' header for '/wire' to '%s'\n",
                wsh->dat);
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (wsh->wire_reply,
                                           MHD_HTTP_HEADER_EXPIRES,
                                           wsh->dat));
  }
  /* Set cache control headers: our response varies depending on these headers */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (wsh->wire_reply,
                                         MHD_HTTP_HEADER_VARY,
                                         MHD_HTTP_HEADER_ACCEPT_ENCODING));
  /* Information is always public, revalidate after 1 day */
  GNUNET_break (MHD_YES ==
                MHD_add_response_header (wsh->wire_reply,
                                         MHD_HTTP_HEADER_CACHE_CONTROL,
                                         "public,max-age=86400"));
  {
    struct GNUNET_HashCode h;
    char etag[sizeof (h) * 2];
    char *end;
    GNUNET_CRYPTO_hash_context_finish (hc,
                                       &h);
    end = GNUNET_STRINGS_data_to_string (&h,
                                         sizeof (h),
                                         etag,
                                         sizeof (etag));
    *end = '\0';
    wsh->etag = GNUNET_strdup (etag);
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (wsh->wire_reply,
                                           MHD_HTTP_HEADER_ETAG,
                                           etag));
  }
  wsh->http_status = MHD_HTTP_OK;
  return wsh;
}
void
TEH_wire_update_state (void)
{
  struct GNUNET_DB_EventHeaderP es = {
    .size = htons (sizeof (es)),
    .type = htons (TALER_DBEVENT_EXCHANGE_WIRE_UPDATED),
  };
  TEH_plugin->event_notify (TEH_plugin->cls,
                            &es,
                            NULL,
                            0);
  wire_generation++;
}
/**
 * Return the current key state for this thread.  Possibly
 * re-builds the key state if we have reason to believe
 * that something changed.
 *
 * @return NULL on error
 */
struct WireStateHandle *
get_wire_state (void)
{
  struct WireStateHandle *old_wsh;
  old_wsh = wire_state;
  if ( (NULL == old_wsh) ||
       (old_wsh->wire_generation < wire_generation) )
  {
    struct WireStateHandle *wsh;
    TEH_check_invariants ();
    wsh = build_wire_state ();
    wire_state = wsh;
    if (NULL != old_wsh)
      destroy_wire_state (old_wsh);
    TEH_check_invariants ();
    return wsh;
  }
  return old_wsh;
}
MHD_RESULT
TEH_handler_wire (struct TEH_RequestContext *rc,
                  const char *const args[])
{
  struct WireStateHandle *wsh;
  (void) args;
  wsh = get_wire_state ();
  if (NULL == wsh)
    return TALER_MHD_reply_with_error (rc->connection,
                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
                                       TALER_EC_EXCHANGE_GENERIC_BAD_CONFIGURATION,
                                       NULL);
  {
    const char *etag;
    etag = MHD_lookup_connection_value (rc->connection,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_IF_NONE_MATCH);
    if ( (NULL != etag) &&
         (MHD_HTTP_OK == wsh->http_status) &&
         (NULL != wsh->etag) &&
         (0 == strcmp (etag,
                       wsh->etag)) )
    {
      MHD_RESULT ret;
      struct MHD_Response *resp;
      resp = MHD_create_response_from_buffer (0,
                                              NULL,
                                              MHD_RESPMEM_PERSISTENT);
      TALER_MHD_add_global_headers (resp);
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (resp,
                                             MHD_HTTP_HEADER_EXPIRES,
                                             wsh->dat));
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (resp,
                                             MHD_HTTP_HEADER_ETAG,
                                             wsh->etag));
      ret = MHD_queue_response (rc->connection,
                                MHD_HTTP_NOT_MODIFIED,
                                resp);
      GNUNET_break (MHD_YES == ret);
      MHD_destroy_response (resp);
      return ret;
    }
  }
  return MHD_queue_response (rc->connection,
                             wsh->http_status,
                             wsh->wire_reply);
}
const struct TALER_WireFeeSet *
TEH_wire_fees_by_time (
  struct GNUNET_TIME_Timestamp ts,
  const char *method)
{
  struct WireStateHandle *wsh = get_wire_state ();
  for (struct WireFeeSet *wfs = wsh->wfs_head;
       NULL != wfs;
       wfs = wfs->next)
  {
    if (0 != strcmp (method,
                     wfs->method))
      continue;
    if ( (GNUNET_TIME_timestamp_cmp (wfs->start_date,
                                     >,
                                     ts)) ||
         (GNUNET_TIME_timestamp_cmp (ts,
                                     >=,
                                     wfs->end_date)) )
      continue;
    return &wfs->fees;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "No wire fees for method `%s' at %s configured\n",
              method,
              GNUNET_TIME_timestamp2s (ts));
  return NULL;
}
/* end of taler-exchange-httpd_wire.c */