/*
  This file is part of TALER
  (C) 2014-2024 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 General Public License for more details.

  You should have received a copy of the GNU General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-merchant-httpd_exchanges.c
 * @brief logic this HTTPD keeps for each exchange we interact with
 * @author Marcello Stanisci
 * @author Christian Grothoff
 */
#include "platform.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_dbevents.h>
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd.h"
#include <regex.h>

/**
 * How often do we retry DB transactions with soft errors?
 */
#define MAX_RETRIES 3

/**
 * Threshold after which exponential backoff should not increase.
 */
#define RETRY_BACKOFF_THRESHOLD GNUNET_TIME_relative_multiply ( \
          GNUNET_TIME_UNIT_SECONDS, 60)

/**
 * This is how long /keys long-polls for, so we should
 * allow this time between requests if there is no
 * answer. See exchange_api_handle.c.
 */
#define LONG_POLL_THRESHOLD GNUNET_TIME_relative_multiply ( \
          GNUNET_TIME_UNIT_SECONDS, 120)


/**
 * Information we keep for a pending #MMH_EXCHANGES_keys4exchange() operation.
 */
struct TMH_EXCHANGES_KeysOperation
{

  /**
   * Kept in a DLL.
   */
  struct TMH_EXCHANGES_KeysOperation *next;

  /**
   * Kept in a DLL.
   */
  struct TMH_EXCHANGES_KeysOperation *prev;

  /**
   * Function to call with the result.
   */
  TMH_EXCHANGES_Find2Continuation fc;

  /**
   * Closure for @e fc.
   */
  void *fc_cls;

  /**
   * Exchange we wait for the /keys for.
   */
  struct TMH_Exchange *my_exchange;

  /**
   * Task scheduled to asynchronously return the result to
   * the find continuation.
   */
  struct GNUNET_SCHEDULER_Task *at;

};


/**
 * Information about wire transfer fees of an exchange, by wire method.
 */
struct FeesByWireMethod
{

  /**
   * Kept in a DLL.
   */
  struct FeesByWireMethod *next;

  /**
   * Kept in a DLL.
   */
  struct FeesByWireMethod *prev;

  /**
   * Wire method these fees are for.
   */
  char *wire_method;

  /**
   * Applicable fees, NULL if unknown/error.
   */
  struct TALER_EXCHANGE_WireAggregateFees *af;

};


/**
 * Internal representation for an exchange
 */
struct TMH_Exchange
{

  /**
   * Kept in a DLL.
   */
  struct TMH_Exchange *next;

  /**
   * Kept in a DLL.
   */
  struct TMH_Exchange *prev;

  /**
   * Head of FOs pending for this exchange.
   */
  struct TMH_EXCHANGES_KeysOperation *keys_head;

  /**
   * Tail of FOs pending for this exchange.
   */
  struct TMH_EXCHANGES_KeysOperation *keys_tail;

  /**
   * (base) URL of the exchange.
   */
  char *url;

  /**
   * Currency offered by the exchange according to OUR configuration.
   */
  char *currency;

  /**
   * The keys of this exchange.
   */
  struct TALER_EXCHANGE_Keys *keys;

  /**
   * Head of wire fees from /wire request.
   */
  struct FeesByWireMethod *wire_fees_head;

  /**
   * Tail of wire fees from /wire request.
   */
  struct FeesByWireMethod *wire_fees_tail;

  /**
   * Task to retry downloading /keys again.
   */
  struct GNUNET_SCHEDULER_Task *retry_task;

  /**
   * When are we willing to force downloading again?
   */
  struct GNUNET_TIME_Absolute first_retry;

  /**
   * Current exponential back-off for @e retry_task.
   */
  struct GNUNET_TIME_Relative retry_delay;

  /**
   * Master public key of the exchange.
   */
  struct TALER_MasterPublicKeyP master_pub;

  /**
   * true if this exchange is from our configuration and
   * explicitly trusted, false if we need to check each
   * key to be sure it is trusted.
   */
  bool trusted;

};


/**
 * Head of exchanges we know about.
 */
static struct TMH_Exchange *exchange_head;

/**
 * Tail of exchanges we know about.
 */
static struct TMH_Exchange *exchange_tail;

/**
 * Our event handler listening for /keys downloads
 * being put into the database.
 */
static struct GNUNET_DB_EventHandler *keys_eh;

/**
 * How many exchanges do we trust (for our configured
 * currency) as per our configuration? Used for a
 * sanity-check on startup.
 */
static int trusted_exchange_count;


const struct TALER_MasterPublicKeyP *
TMH_EXCHANGES_get_master_pub (
  const struct TMH_Exchange *exchange)
{
  GNUNET_break ( (exchange->trusted) ||
                 (NULL != exchange->keys) );
  return &exchange->master_pub;
}


const char *
TMH_EXCHANGES_get_currency (
  const struct TMH_Exchange *exchange)
{
  return exchange->currency;
}


/**
 * Lookup exchange by @a exchange_url. Create one
 * if it does not exist.
 *
 * @param exchange_url base URL to match against
 * @return fresh entry if exchange was not yet known
 */
static struct TMH_Exchange *
lookup_exchange (const char *exchange_url)
{
  for (struct TMH_Exchange *exchange = exchange_head;
       NULL != exchange;
       exchange = exchange->next)
    if (0 == strcmp (exchange->url,
                     exchange_url))
      return exchange;
  return NULL;
}


bool
TMH_EXCHANGES_check_trusted (
  const char *exchange_url)
{
  struct TMH_Exchange *exchange = lookup_exchange (exchange_url);

  if (NULL == exchange)
    return false;
  return exchange->trusted;
}


/**
 * Check if we have any remaining pending requests for the
 * given @a exchange, and if we have the required data, call
 * the callback.
 *
 * @param exchange the exchange to check for pending find operations
 */
static void
process_find_operations (struct TMH_Exchange *exchange)
{
  struct GNUNET_TIME_Timestamp now;

  now = GNUNET_TIME_timestamp_get ();
  for (struct FeesByWireMethod *fbw = exchange->wire_fees_head;
       NULL != fbw;
       fbw = fbw->next)
  {
    while ( (NULL != fbw->af) &&
            (GNUNET_TIME_timestamp_cmp (fbw->af->end_date,
                                        <,
                                        now)) )
    {
      struct TALER_EXCHANGE_WireAggregateFees *af = fbw->af;

      fbw->af = af->next;
      GNUNET_free (af);
    }
    if (NULL == fbw->af)
    {
      /* Disagreement on the current time */
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Exchange has no wire fees configured for `%s' wire method\n",
                  fbw->wire_method);
    }
    else if (GNUNET_TIME_timestamp_cmp (fbw->af->start_date,
                                        >,
                                        now))
    {
      /* Disagreement on the current time */
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Exchange's earliest fee is %s ahead of our time. Clock skew issue?\n",
                  GNUNET_TIME_relative2s (
                    GNUNET_TIME_absolute_get_remaining (
                      fbw->af->start_date.abs_time),
                    true));
    }
  } /* for all wire methods */

  {
    struct TMH_EXCHANGES_KeysOperation *kon;

    kon = NULL;
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Processing find operations for `%s'\n",
                exchange->url);
    for (struct TMH_EXCHANGES_KeysOperation *ko = exchange->keys_head;
         NULL != ko;
         ko = kon)
    {
      kon = ko->next;
      ko->fc (ko->fc_cls,
              exchange->keys,
              exchange);
      TMH_EXCHANGES_keys4exchange_cancel (ko);
    }
  }
}


/**
 * Function called with information about the wire fees for each wire method.
 * Stores the wire fees within our internal data structures for later use.
 *
 * @param exchange connection to the exchange
 * @param master_pub public key of the exchange
 * @param num_methods number of wire methods supported
 * @param fbm wire fees by method
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
process_wire_fees (
  struct TMH_Exchange *exchange,
  const struct TALER_MasterPublicKeyP *master_pub,
  unsigned int num_methods,
  const struct TALER_EXCHANGE_WireFeesByMethod fbm[static num_methods])
{
  for (unsigned int i = 0; i<num_methods; i++)
  {
    const char *wire_method = fbm[i].method;
    const struct TALER_EXCHANGE_WireAggregateFees *fees = fbm[i].fees_head;
    struct FeesByWireMethod *f;
    struct TALER_EXCHANGE_WireAggregateFees *endp;

    for (f = exchange->wire_fees_head; NULL != f; f = f->next)
      if (0 == strcasecmp (wire_method,
                           f->wire_method))
        break;
    if (NULL == f)
    {
      f = GNUNET_new (struct FeesByWireMethod);
      f->wire_method = GNUNET_strdup (wire_method);
      GNUNET_CONTAINER_DLL_insert (exchange->wire_fees_head,
                                   exchange->wire_fees_tail,
                                   f);
    }
    endp = f->af;
    while ( (NULL != endp) &&
            (NULL != endp->next) )
      endp = endp->next;
    while ( (NULL != endp) &&
            (NULL != fees) &&
            (GNUNET_TIME_timestamp_cmp (fees->start_date,
                                        <,
                                        endp->end_date)) )
      fees = fees->next;
    if ( (NULL != endp) &&
         (NULL != fees) &&
         (GNUNET_TIME_timestamp_cmp (fees->start_date,
                                     !=,
                                     endp->end_date)) )
    {
      /* Hole or overlap in the fee structure, not allowed! */
      GNUNET_break_op (0);
      return GNUNET_SYSERR;
    }
    while (NULL != fees)
    {
      struct TALER_EXCHANGE_WireAggregateFees *af;

      af = GNUNET_new (struct TALER_EXCHANGE_WireAggregateFees);
      *af = *fees;
      af->next = NULL;
      if (NULL == endp)
        f->af = af;
      else
        endp->next = af;
      endp = af;
      fees = fees->next;
    } /* all fees for this method */
  } /* for all methods (i) */
  return GNUNET_OK;
}


/**
 * Retry getting keys from the given exchange in the closure.
 *
 * @param cls the `struct TMH_Exchange *`
 */
static void
retry_exchange (void *cls)
{
  struct TMH_Exchange *exchange = cls;
  struct GNUNET_DB_EventHeaderP es = {
    .size = ntohs (sizeof (es)),
    .type = ntohs (TALER_DBEVENT_MERCHANT_EXCHANGE_FORCE_KEYS)
  };

  exchange->retry_task = NULL;
  exchange->retry_delay
    = GNUNET_TIME_randomized_backoff (exchange->retry_delay,
                                      RETRY_BACKOFF_THRESHOLD);
  exchange->first_retry
    = GNUNET_TIME_relative_to_absolute (
        exchange->retry_delay);

  TMH_db->event_notify (TMH_db->cls,
                        &es,
                        exchange->url,
                        strlen (exchange->url) + 1);
}


/**
 * Task to asynchronously return keys operation result to caller.
 *
 * @param cls a `struct TMH_EXCHANGES_KeysOperation`
 */
static void
return_keys (void *cls)
{
  struct TMH_EXCHANGES_KeysOperation *fo = cls;
  struct TMH_Exchange *exchange = fo->my_exchange;

  fo->at = NULL;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Returning key data for %s instantly\n",
              exchange->url);
  process_find_operations (exchange);
}


struct TMH_EXCHANGES_KeysOperation *
TMH_EXCHANGES_keys4exchange (
  const char *chosen_exchange,
  bool force_download,
  TMH_EXCHANGES_Find2Continuation fc,
  void *fc_cls)
{
  struct TMH_Exchange *exchange;
  struct TMH_EXCHANGES_KeysOperation *fo;

  if (NULL == TMH_curl_ctx)
  {
    GNUNET_break (0);
    return NULL;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Trying to find chosen exchange `%s'\n",
              chosen_exchange);
  exchange = lookup_exchange (chosen_exchange);
  if (NULL == exchange)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Exchange `%s' not configured\n",
                chosen_exchange);
    return NULL;
  }
  if (! exchange->trusted)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Exchange `%s' not trusted\n",
                chosen_exchange);
    return NULL;
  }
  fo = GNUNET_new (struct TMH_EXCHANGES_KeysOperation);
  fo->fc = fc;
  fo->fc_cls = fc_cls;
  fo->my_exchange = exchange;
  GNUNET_CONTAINER_DLL_insert (exchange->keys_head,
                               exchange->keys_tail,
                               fo);
  if ( (NULL == exchange->keys) &&
       (! force_download) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Waiting for `%skeys' already, failing query instantly\n",
                exchange->url);
    GNUNET_assert (NULL == fo->at);
    fo->at = GNUNET_SCHEDULER_add_now (&return_keys,
                                       fo);
    return fo;
  }
  if ( (NULL != exchange->keys) &&
       (! force_download) &&
       (GNUNET_TIME_absolute_is_future (
          exchange->keys->key_data_expiration.abs_time)) )
  {
    /* We have a valid reply, immediately return result */
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "The exchange `%s' is ready\n",
                exchange->url);
    GNUNET_assert (NULL == fo->at);
    fo->at = GNUNET_SCHEDULER_add_now (&return_keys,
                                       fo);
    return fo;
  }
  if ( (force_download) &&
       (GNUNET_TIME_absolute_is_future (exchange->first_retry)) &&
       (NULL != exchange->keys) )
  {
    /* Return results immediately. */
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Earliest retry is in the future, returning keys now\n");
    fo->at = GNUNET_SCHEDULER_add_now (&return_keys,
                                       fo);
    /* *no* return here, we MAY schedule a 'retry_task' in the
       next block if there isn't one yet */
  }
  if (NULL == exchange->retry_task)
    exchange->retry_task
      = GNUNET_SCHEDULER_add_at (exchange->first_retry,
                                 &retry_exchange,
                                 exchange);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Next %skeys request scheduled for %s\n",
              exchange->url,
              GNUNET_TIME_absolute2s (
                exchange->first_retry));
  /* No activity to launch, we are already doing so. */
  return fo;
}


void
TMH_EXCHANGES_keys4exchange_cancel (
  struct TMH_EXCHANGES_KeysOperation *fo)
{
  struct TMH_Exchange *exchange = fo->my_exchange;

  if (NULL != fo->at)
  {
    GNUNET_SCHEDULER_cancel (fo->at);
    fo->at = NULL;
  }
  GNUNET_CONTAINER_DLL_remove (exchange->keys_head,
                               exchange->keys_tail,
                               fo);
  GNUNET_free (fo);
}


/**
 * Obtain applicable fees for @a exchange and @a wire_method.
 *
 * @param exchange the exchange to query
 * @param now current time
 * @param wire_method the wire method we want the fees for
 * @return NULL if we do not have fees for this method yet
 */
static const struct FeesByWireMethod *
get_wire_fees (const struct TMH_Exchange *exchange,
               struct GNUNET_TIME_Timestamp now,
               const char *wire_method)
{
  for (struct FeesByWireMethod *fbw = exchange->wire_fees_head;
       NULL != fbw;
       fbw = fbw->next)
  {
    if (0 == strcasecmp (fbw->wire_method,
                         wire_method) )
    {
      struct TALER_EXCHANGE_WireAggregateFees *af;

      /* Advance through list up to current time */
      while ( (NULL != (af = fbw->af)) &&
              (GNUNET_TIME_timestamp_cmp (now,
                                          >=,
                                          af->end_date)) )
      {
        fbw->af = af->next;
        GNUNET_free (af);
      }
      return fbw;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Exchange supports `%s' as a wire method (but we do not use that one)\n",
                fbw->wire_method);
  }
  return NULL;
}


/**
 * Free @a exchange.
 *
 * @param[in] exchange entry to free
 */
static void
free_exchange_entry (struct TMH_Exchange *exchange)
{
  struct FeesByWireMethod *f;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Releasing %s exchange %s\n",
              exchange->trusted ? "trusted" : "untrusted",
              exchange->url);
  GNUNET_CONTAINER_DLL_remove (exchange_head,
                               exchange_tail,
                               exchange);
  while (NULL != (f = exchange->wire_fees_head))
  {
    struct TALER_EXCHANGE_WireAggregateFees *af;

    GNUNET_CONTAINER_DLL_remove (exchange->wire_fees_head,
                                 exchange->wire_fees_tail,
                                 f);
    while (NULL != (af = f->af))
    {
      f->af = af->next;
      GNUNET_free (af);
    }
    GNUNET_free (f->wire_method);
    GNUNET_free (f);
  }
  TALER_EXCHANGE_keys_decref (exchange->keys);
  exchange->keys = NULL;
  if (NULL != exchange->retry_task)
  {
    GNUNET_SCHEDULER_cancel (exchange->retry_task);
    exchange->retry_task = NULL;
  }
  GNUNET_assert (NULL == exchange->keys_head);
  GNUNET_assert (NULL == exchange->keys_tail);
  GNUNET_free (exchange->currency);
  GNUNET_free (exchange->url);
  GNUNET_free (exchange);
}


enum GNUNET_GenericReturnValue
TMH_EXCHANGES_lookup_wire_fee (
  const struct TMH_Exchange *exchange,
  const char *wire_method,
  struct TALER_Amount *wire_fee)
{
  const struct FeesByWireMethod *fbm;
  const struct TALER_EXCHANGE_WireAggregateFees *af;

  fbm = get_wire_fees (exchange,
                       GNUNET_TIME_timestamp_get (),
                       wire_method);
  if (NULL == fbm)
    return GNUNET_NO;
  af = fbm->af;
  *wire_fee = af->fees.wire;
  return GNUNET_OK;
}


enum TMH_ExchangeStatus
TMH_exchange_check_debit (
  const char *instance_id,
  const struct TMH_Exchange *exchange,
  const struct TMH_WireMethod *wm,
  struct TALER_Amount *max_amount)
{
  const struct TALER_EXCHANGE_Keys *keys = exchange->keys;
  bool have_kyc = false;
  bool no_access_token = true;
  enum TMH_ExchangeStatus retry_ok = 0;

  if (GNUNET_TIME_absolute_is_past (exchange->first_retry))
    retry_ok = TMH_ES_RETRY_OK;

  if (NULL == keys)
    return TMH_ES_NO_KEYS | retry_ok;
  if (0 != strcasecmp (keys->currency,
                       max_amount->currency))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Currency mismatch: exchange %s uses %s, we need %s\n",
                exchange->url,
                keys->currency,
                max_amount->currency);
    return TMH_ES_NO_CURR | retry_ok;
  }
  {
    struct TALER_NormalizedPayto np;
    bool account_ok;

    np = TALER_payto_normalize (wm->payto_uri);
    account_ok = TALER_EXCHANGE_keys_test_account_allowed (keys,
                                                           false,
                                                           np);
    GNUNET_free (np.normalized_payto);
    if (! account_ok)
      return TMH_ES_NO_ACC | retry_ok;
  }
  if (! keys->kyc_enabled)
    return TMH_ES_OK | retry_ok;

  {
    json_t *jlimits = NULL;
    enum GNUNET_DB_QueryStatus qs;

    qs = TMH_db->get_kyc_limits (TMH_db->cls,
                                 wm->payto_uri,
                                 instance_id,
                                 exchange->url,
                                 &have_kyc,
                                 &no_access_token,
                                 &jlimits);
    GNUNET_break (qs >= 0);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "get_kyc_limits for %s at %s returned %s/%s\n",
                wm->payto_uri.full_payto,
                exchange->url,
                have_kyc ? "KYC OK" : "KYC missing",
                NULL == jlimits ? "default limits" : "custom limits");
    if ( (qs > 0) &&
         (NULL != jlimits) )
    {
      json_t *jlimit;
      size_t idx;
      struct TALER_Amount kyc_limit;
      bool unlimited = true;

      json_array_foreach (jlimits, idx, jlimit)
      {
        enum TALER_KYCLOGIC_KycTriggerEvent ot;
        struct TALER_Amount threshold;
        bool soft_limit = false;
        struct GNUNET_JSON_Specification spec[] = {
          TALER_JSON_spec_kycte ("operation_type",
                                 &ot),
          TALER_JSON_spec_amount_any ("threshold",
                                      &threshold),
          GNUNET_JSON_spec_mark_optional (
            GNUNET_JSON_spec_bool ("soft_limit",
                                   &soft_limit),
            NULL),
          GNUNET_JSON_spec_end ()
        };

        if (GNUNET_OK !=
            GNUNET_JSON_parse (jlimit,
                               spec,
                               NULL, NULL))
        {
          GNUNET_break (0);
          continue;
        }
        if (soft_limit)
          continue;
        if ( (TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT != ot) &&
             (TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION != ot) )
          continue;
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "KYC rule %u with limit %s applies\n",
                    (unsigned int) idx,
                    TALER_amount2s (&threshold));
        if (unlimited)
        {
          unlimited = false;
          kyc_limit = threshold;
        }
        else
        {
          TALER_amount_min (&kyc_limit,
                            &kyc_limit,
                            &threshold);
        }
      }
      json_decref (jlimits);
      /* We had custom rules, do not evaluate default rules */
      if (! unlimited)
        TALER_amount_min (max_amount,
                          max_amount,
                          &kyc_limit);
      return TMH_ES_OK | retry_ok;
    } /* END of if qs > 0, NULL != jlimits */
  }

  /* Check zero limits *only* if we did no KYC process at all yet.
     Because if we did, there is at least a chance that those have
     been lifted. */
  if ( (no_access_token) ||
       ( (! have_kyc) &&
         (TALER_EXCHANGE_keys_evaluate_zero_limits (
            keys,
            TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) ||
          TALER_EXCHANGE_keys_evaluate_zero_limits (
            keys,
            TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION)) ) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "KYC requirements of %s not satisfied\n",
                exchange->url);
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_set_zero (
                     max_amount->currency,
                     max_amount));
    return TMH_ES_OK | retry_ok;
  }
  /* In any case, abide by hard limits (unless we have custom rules). */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Evaluating default hard limits of %s\n",
              exchange->url);
  TALER_EXCHANGE_keys_evaluate_hard_limits (
    keys,
    TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT,
    max_amount);
  TALER_EXCHANGE_keys_evaluate_hard_limits (
    keys,
    TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION,
    max_amount);
  if (TALER_EXCHANGE_keys_evaluate_zero_limits (
        keys,
        TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) ||
      TALER_EXCHANGE_keys_evaluate_zero_limits (
        keys,
        TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Operation is zero-limited by default\n");
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_set_zero (max_amount->currency,
                                          max_amount));
  }
  return TMH_ES_OK | retry_ok;
}


void
TMH_exchange_get_trusted (TMH_ExchangeCallback cb,
                          void *cb_cls)
{
  for (const struct TMH_Exchange *exchange = exchange_head;
       NULL != exchange;
       exchange = exchange->next)
  {
    if (! exchange->trusted)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Exchange %s not trusted, skipping!\n",
                  exchange->url);
      continue;
    }
    cb (cb_cls,
        exchange->url,
        exchange);
  }
}


bool
TMH_test_exchange_configured_for_currency (
  const char *currency)
{
  for (const struct TMH_Exchange *exchange = exchange_head;
       NULL != exchange;
       exchange = exchange->next)
  {
    if (! exchange->trusted)
      continue;
    if (NULL == exchange->currency)
      continue;
    if (0 == strcmp (currency,
                     exchange->currency))
      return true;
  }
  return false;
}


/**
 * (Re)load of keys from DB.
 *
 * @param exchange exchange to reload keys of
 */
static void
reload_exchange_keys (struct TMH_Exchange *exchange)
{
  enum GNUNET_DB_QueryStatus qs;
  struct TALER_EXCHANGE_Keys *keys;
  struct GNUNET_TIME_Absolute first_retry;

  qs = TMH_db->select_exchange_keys (TMH_db->cls,
                                     exchange->url,
                                     &first_retry,
                                     &keys);
  if (qs < 0)
  {
    GNUNET_break (0);
    return;
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "No keys yet for `%s'\n",
                exchange->url);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Loading latest keys of `%s' from database\n",
              exchange->url);
  exchange->retry_delay = GNUNET_TIME_UNIT_ZERO;
  exchange->first_retry = first_retry;
  if (NULL == exchange->currency)
    exchange->currency = GNUNET_strdup (keys->currency);
  if (0 != strcmp (keys->currency,
                   exchange->currency))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "/keys cached in our database are for currency `%s', but we expected `%s'\n",
                keys->currency,
                exchange->currency);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Loaded /keys from database with %u accounts, %u fees\n",
              keys->accounts_len,
              keys->fees_len);
  if (GNUNET_OK !=
      process_wire_fees (exchange,
                         &keys->master_pub,
                         keys->fees_len,
                         keys->fees))
  {
    /* invalid wire fee specification given */
    GNUNET_break_op (0);
    /* but: we can continue anyway, things may just not
       work, but probably better than to not keep going. */
    return;
  }

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Reloaded /keys of %s from database\n",
              exchange->url);
  TALER_EXCHANGE_keys_decref (exchange->keys);
  exchange->keys = keys;
  if ( (exchange->trusted) &&
       (0 != GNUNET_memcmp (&exchange->master_pub,
                            &keys->master_pub)) )
  {
    /* master pub differs => do not trust the exchange (without auditor) */
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Master public key of exchange `%s' differs from our configuration. Not trusting exchange.\n",
                exchange->url);
    exchange->trusted = false;
  }
  if (! exchange->trusted)
  {
    exchange->master_pub = keys->master_pub;
    for (struct TMH_Exchange *e = exchange_head;
         NULL != e;
         e = e->next)
    {
      if (e == exchange)
        continue;
      if (! e->trusted)
        continue;
      if (0 ==
          GNUNET_memcmp (&e->master_pub,
                         &exchange->master_pub))
        exchange->trusted = true; /* same exchange, different URL => trust applies */
    }
  }

  process_find_operations (exchange);
}


/**
 * Function called on each configuration section. Finds sections
 * about exchanges, parses the entries.
 *
 * @param cls closure, with a `const struct GNUNET_CONFIGURATION_Handle *`
 * @param section name of the section
 */
static void
accept_exchanges (void *cls,
                  const char *section)
{
  const struct GNUNET_CONFIGURATION_Handle *cfg = cls;
  char *url;
  char *mks;
  struct TMH_Exchange *exchange;
  char *currency;

  if (GNUNET_SYSERR == trusted_exchange_count)
    return;
  if (0 != strncasecmp (section,
                        "merchant-exchange-",
                        strlen ("merchant-exchange-")))
    return;
  if (GNUNET_YES ==
      GNUNET_CONFIGURATION_get_value_yesno (cfg,
                                            section,
                                            "DISABLED"))
    return;
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             section,
                                             "EXCHANGE_BASE_URL",
                                             &url))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               section,
                               "EXCHANGE_BASE_URL");
    return;
  }
  exchange = lookup_exchange (url);
  if (NULL != exchange)
  {
    GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                               section,
                               "EXCHANGE_BASE_URL",
                               "same base URL specified again");
    GNUNET_free (url);
    return;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             section,
                                             "CURRENCY",
                                             &currency))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               section,
                               "CURRENCY");
    GNUNET_free (url);
    return;
  }
  exchange = GNUNET_new (struct TMH_Exchange);
  exchange->url = url;
  exchange->currency = currency;
  GNUNET_CONTAINER_DLL_insert (exchange_head,
                               exchange_tail,
                               exchange);
  if (GNUNET_OK ==
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             section,
                                             "MASTER_KEY",
                                             &mks))
  {
    if (GNUNET_OK ==
        GNUNET_CRYPTO_eddsa_public_key_from_string (
          mks,
          strlen (mks),
          &exchange->master_pub.eddsa_pub))
    {
      exchange->trusted = true;
      trusted_exchange_count++;
    }
    else
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 section,
                                 "MASTER_KEY",
                                 "malformed EdDSA key");
    }
    GNUNET_free (mks);
  }
  else
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "MASTER_KEY missing in section '%s', not trusting exchange\n",
                section);
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Setup exchange %s as %s\n",
              exchange->url,
              exchange->trusted ? "trusted" : "untrusted");
  reload_exchange_keys (exchange);
  if (NULL != exchange->retry_task)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Exchange at `%s' configured in multiple configuration sections (see `%s')!\n",
                exchange->url,
                section);
    trusted_exchange_count = GNUNET_SYSERR;
    return;
  }
  exchange->retry_task
    = GNUNET_SCHEDULER_add_now (&retry_exchange,
                                exchange);
}


/**
 * Trigger (re)loading of keys from DB.
 *
 * @param cls NULL
 * @param extra base URL of the exchange that changed
 * @param extra_len number of bytes in @a extra
 */
static void
update_exchange_keys (void *cls,
                      const void *extra,
                      size_t extra_len)
{
  const char *url = extra;
  struct TMH_Exchange *exchange;

  if ( (NULL == extra) ||
       (0 == extra_len) )
  {
    GNUNET_break (0);
    return;
  }
  if ('\0' != url[extra_len - 1])
  {
    GNUNET_break (0);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Received keys change notification: reload `%s'\n",
              url);
  exchange = lookup_exchange (url);
  GNUNET_break (NULL != exchange);
  if (NULL != exchange)
    reload_exchange_keys (exchange);
}


bool
TMH_EXCHANGES_is_below_limit (
  const struct TALER_EXCHANGE_Keys *keys,
  enum TALER_KYCLOGIC_KycTriggerEvent operation_type,
  const struct TALER_Amount *amount)
{
  if (NULL == keys)
  {
    /* should only be called after we have keys! */
    GNUNET_break (0);
    return false;
  }
  for (unsigned int i = 0; i<keys->hard_limits_length; i++)
  {
    const struct TALER_EXCHANGE_AccountLimit *al
      = &keys->hard_limits[i];

    if (operation_type != al->operation_type)
      continue;
    if (-1 ==
        TALER_amount_cmp (&al->threshold,
                          amount))
      /* -1: threshold < amount */
      return false;
  }
  return true;
}


void
TMH_EXCHANGES_get_limit (
  const char *exchange_url,
  enum TALER_KYCLOGIC_KycTriggerEvent operation_type,
  struct TALER_Amount *amount)
{
  struct TMH_Exchange *exchange;
  const struct TALER_EXCHANGE_Keys *keys;

  exchange = lookup_exchange (exchange_url);
  if ( (NULL == exchange) ||
       (NULL == (keys = exchange->keys)) )
  {
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_set_zero (
                     amount->currency,
                     amount));
    return;
  }
  for (unsigned int i = 0; i<keys->hard_limits_length; i++)
  {
    const struct TALER_EXCHANGE_AccountLimit *al
      = &keys->hard_limits[i];

    if (operation_type != al->operation_type)
      continue;
    TALER_amount_min (amount,
                      amount,
                      &al->threshold);
  }
}


enum GNUNET_GenericReturnValue
TMH_EXCHANGES_init (const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  /* get exchanges from the merchant configuration and try to connect to them */
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = ntohs (sizeof (es)),
      .type = ntohs (TALER_DBEVENT_MERCHANT_EXCHANGE_KEYS)
    };

    GNUNET_assert (NULL == keys_eh);
    keys_eh = TMH_db->event_listen (TMH_db->cls,
                                    &es,
                                    GNUNET_TIME_UNIT_FOREVER_REL,
                                    &update_exchange_keys,
                                    NULL);
  }
  GNUNET_CONFIGURATION_iterate_sections (cfg,
                                         &accept_exchanges,
                                         (void *) cfg);
  /* build JSON with list of trusted exchanges (will be included in contracts) */
  return trusted_exchange_count;
}


void
TMH_EXCHANGES_done ()
{
  if (NULL != keys_eh)
  {
    TMH_db->event_listen_cancel (keys_eh);
    keys_eh = NULL;
  }
  while (NULL != exchange_head)
    free_exchange_entry (exchange_head);
}


/* end of taler-merchant-httpd_exchanges.c */
