/*
  This file is part of TALER
  Copyright (C) 2023, 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 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 <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-merchant-exchangekeyupdate.c
 * @brief Process that ensures our /keys data for all exchanges is current
 * @author Christian Grothoff
 */
#include "platform.h"
#include "microhttpd.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#include <pthread.h>
#include <taler/taler_dbevents.h>
#include "taler_merchant_util.h"
#include "taler_merchant_bank_lib.h"
#include "taler_merchantdb_lib.h"
#include "taler_merchantdb_plugin.h"

/**
 * Maximum frequency for the exchange interaction.
 */
#define EXCHANGE_MAXFREQ GNUNET_TIME_relative_multiply ( \
          GNUNET_TIME_UNIT_MINUTES, \
          5)

/**
 * How many inquiries do we process concurrently at most.
 */
#define OPEN_INQUIRY_LIMIT 1024

/**
 * How often do we retry after DB serialization errors (at most)?
 */
#define MAX_RETRIES 3

/**
 * Information about an exchange.
 */
struct Exchange
{
  /**
   * Kept in a DLL.
   */
  struct Exchange *next;

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

  /**
   * Base URL of the exchange are we tracking here.
   */
  char *exchange_url;

  /**
   * Expected currency of the exchange.
   */
  char *currency;

  /**
   * A /keys request to this exchange, NULL if not active.
   */
  struct TALER_EXCHANGE_GetKeysHandle *conn;

  /**
   * The keys of this exchange, NULL if not known.
   */
  struct TALER_EXCHANGE_Keys *keys;

  /**
   * Task where we retry fetching /keys from the exchange.
   */
  struct GNUNET_SCHEDULER_Task *retry_task;

  /**
   * Master public key expected for this exchange.
   */
  struct TALER_MasterPublicKeyP master_pub;

  /**
   * How soon can may we, at the earliest, re-download /keys?
   */
  struct GNUNET_TIME_Absolute first_retry;

  /**
   * How long should we wait between the next retry?
   * Used for exponential back-offs.
   */
  struct GNUNET_TIME_Relative retry_delay;

  /**
   * Are we waiting for /keys downloads due to our
   * hard limit?
   */
  bool limited;

  /**
   * Are we force-retrying a /keys download because some keys
   * were missing (and we thus should not cherry-pick, as
   * a major reason for a force-reload would be an
   * exchange that has lost keys and backfilled them, which
   * breaks keys downloads with cherry-picking).
   */
  bool force_retry;
};


/**
 * Head of known exchanges.
 */
static struct Exchange *e_head;

/**
 * Tail of known exchanges.
 */
static struct Exchange *e_tail;

/**
 * The merchant's configuration.
 */
static const struct GNUNET_CONFIGURATION_Handle *cfg;

/**
 * Our database plugin.
 */
static struct TALER_MERCHANTDB_Plugin *db_plugin;

/**
 * Our event handler listening for /keys forced downloads.
 */
static struct GNUNET_DB_EventHandler *eh;

/**
 * Handle to the context for interacting with the bank.
 */
static struct GNUNET_CURL_Context *ctx;

/**
 * Scheduler context for running the @e ctx.
 */
static struct GNUNET_CURL_RescheduleContext *rc;

/**
 * How many active inquiries do we have right now.
 */
static unsigned int active_inquiries;

/**
 * Value to return from main(). 0 on success, non-zero on errors.
 */
static int global_ret;

/**
 * #GNUNET_YES if we are in test mode and should exit when idle.
 */
static int test_mode;

/**
 * True if the last DB query was limited by the
 * #OPEN_INQUIRY_LIMIT and we thus should check again
 * as soon as we are substantially below that limit,
 * and not only when we get a DB notification.
 */
static bool at_limit;


/**
 * Function that initiates a /keys download.
 *
 * @param cls a `struct Exchange *`
 */
static void
download_keys (void *cls);


/**
 * An inquiry finished, check if we need to start more.
 */
static void
end_inquiry (void)
{
  GNUNET_assert (active_inquiries > 0);
  active_inquiries--;
  if ( (active_inquiries < OPEN_INQUIRY_LIMIT / 2) &&
       (at_limit) )
  {
    at_limit = false;
    for (struct Exchange *e = e_head;
         NULL != e;
         e = e->next)
    {
      if (! e->limited)
        continue;
      e->limited = false;
      /* done synchronously so that the active_inquiries
         is updated immediately */
      download_keys (e);
      if (at_limit)
        break;
    }
  }
  if ( (! at_limit) &&
       (0 == active_inquiries) &&
       (test_mode) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "No more open inquiries and in test mode. Existing.\n");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
}


/**
 * Add account restriction @a a to array of @a restrictions.
 *
 * @param[in,out] restrictions JSON array to build
 * @param r restriction to add to @a restrictions
 * @return #GNUNET_SYSERR if @a r is malformed
 */
static enum GNUNET_GenericReturnValue
add_restriction (json_t *restrictions,
                 const struct TALER_EXCHANGE_AccountRestriction *r)
{
  json_t *jr;

  jr = NULL;
  switch (r->type)
  {
  case TALER_EXCHANGE_AR_INVALID:
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  case TALER_EXCHANGE_AR_DENY:
    jr = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_string ("type",
                               "deny")
      );
    break;
  case TALER_EXCHANGE_AR_REGEX:
    jr = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_string (
        "type",
        "regex"),
      GNUNET_JSON_pack_string (
        "regex",
        r->details.regex.posix_egrep),
      GNUNET_JSON_pack_string (
        "human_hint",
        r->details.regex.human_hint),
      GNUNET_JSON_pack_object_incref (
        "human_hint_i18n",
        (json_t *) r->details.regex.human_hint_i18n)
      );
    break;
  }
  if (NULL == jr)
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  GNUNET_assert (0 ==
                 json_array_append_new (restrictions,
                                        jr));
  return GNUNET_OK;

}


/**
 * Update our information in the database about the
 * /keys of an exchange. Run inside of a database
 * transaction scope that will re-try and/or commit
 * depending on the return value.
 *
 * @param keys information to persist
 * @param first_retry earliest we may retry fetching the keys
 * @return transaction status
 */
static enum GNUNET_DB_QueryStatus
insert_keys_data (const struct TALER_EXCHANGE_Keys *keys,
                  struct GNUNET_TIME_Absolute first_retry)
{
  enum GNUNET_DB_QueryStatus qs;

  /* store exchange online signing keys in our DB */
  for (unsigned int i = 0; i<keys->num_sign_keys; i++)
  {
    const struct TALER_EXCHANGE_SigningPublicKey *sign_key
      = &keys->sign_keys[i];

    qs = db_plugin->insert_exchange_signkey (
      db_plugin->cls,
      &keys->master_pub,
      &sign_key->key,
      sign_key->valid_from,
      sign_key->valid_until,
      sign_key->valid_legal,
      &sign_key->master_sig);
    /* 0 is OK, we may already have the key in the DB! */
    if (0 > qs)
    {
      GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
      return qs;
    }
  }

  qs = db_plugin->insert_exchange_keys (db_plugin->cls,
                                        keys,
                                        first_retry);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    return qs;
  }

  qs = db_plugin->delete_exchange_accounts (db_plugin->cls,
                                            &keys->master_pub);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    return qs;
  }

  for (unsigned int i = 0; i<keys->accounts_len; i++)
  {
    const struct TALER_EXCHANGE_WireAccount *account
      = &keys->accounts[i];
    json_t *debit_restrictions;
    json_t *credit_restrictions;

    debit_restrictions = json_array ();
    GNUNET_assert (NULL != debit_restrictions);
    credit_restrictions = json_array ();
    GNUNET_assert (NULL != credit_restrictions);
    for (unsigned int j = 0; j<account->debit_restrictions_length; j++)
    {
      if (GNUNET_OK !=
          add_restriction (debit_restrictions,
                           &account->debit_restrictions[j]))
      {
        db_plugin->rollback (db_plugin->cls);
        GNUNET_break (0);
        json_decref (debit_restrictions);
        json_decref (credit_restrictions);
        return GNUNET_DB_STATUS_HARD_ERROR;
      }
    }
    for (unsigned int j = 0; j<account->credit_restrictions_length; j++)
    {
      if (GNUNET_OK !=
          add_restriction (credit_restrictions,
                           &account->credit_restrictions[j]))
      {
        db_plugin->rollback (db_plugin->cls);
        GNUNET_break (0);
        json_decref (debit_restrictions);
        json_decref (credit_restrictions);
        return GNUNET_DB_STATUS_HARD_ERROR;
      }
    }
    qs = db_plugin->insert_exchange_account (
      db_plugin->cls,
      &keys->master_pub,
      account->fpayto_uri,
      account->conversion_url,
      debit_restrictions,
      credit_restrictions,
      &account->master_sig);
    json_decref (debit_restrictions);
    json_decref (credit_restrictions);
    if (qs < 0)
    {
      GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
      return qs;
    }
  } /* end 'for all accounts' */

  for (unsigned int i = 0; i<keys->fees_len; i++)
  {
    const struct TALER_EXCHANGE_WireFeesByMethod *fbm
      = &keys->fees[i];
    const char *wire_method = fbm->method;
    const struct TALER_EXCHANGE_WireAggregateFees *fees
      = fbm->fees_head;

    while (NULL != fees)
    {
      struct GNUNET_HashCode h_wire_method;

      GNUNET_CRYPTO_hash (wire_method,
                          strlen (wire_method) + 1,
                          &h_wire_method);
      qs = db_plugin->store_wire_fee_by_exchange (
        db_plugin->cls,
        &keys->master_pub,
        &h_wire_method,
        &fees->fees,
        fees->start_date,
        fees->end_date,
        &fees->master_sig);
      if (0 > qs)
      {
        GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
        return qs;
      }
      fees = fees->next;
    } /* all fees for this method */
  } /* for all methods (i) */

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Updated keys for %s, inserted %d signing keys, %d denom keys, %d fees-by-wire\n",
              keys->exchange_url,
              keys->num_sign_keys,
              keys->num_denom_keys,
              keys->fees_len);

  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = ntohs (sizeof (es)),
      .type = ntohs (TALER_DBEVENT_MERCHANT_EXCHANGE_KEYS)
    };

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Informing other processes about keys change for %s\n",
                keys->exchange_url);
    db_plugin->event_notify (db_plugin->cls,
                             &es,
                             keys->exchange_url,
                             strlen (keys->exchange_url) + 1);
  }
  return qs;
}


/**
 * Run database transaction to store the @a keys in
 * the merchant database (and notify other processes
 * that may care about them).
 *
 * @param keys the keys to store
 * @param first_retry earliest we may retry fetching the keys
 * @return true on success
 */
static bool
store_keys (struct TALER_EXCHANGE_Keys *keys,
            struct GNUNET_TIME_Absolute first_retry)
{
  enum GNUNET_DB_QueryStatus qs;

  db_plugin->preflight (db_plugin->cls);
  for (unsigned int r = 0; r<MAX_RETRIES; r++)
  {
    if (GNUNET_OK !=
        db_plugin->start (db_plugin->cls,
                          "update exchange key data"))
    {
      db_plugin->rollback (db_plugin->cls);
      GNUNET_break (0);
      return false;
    }

    qs = insert_keys_data (keys,
                           first_retry);
    if (0 > qs)
    {
      db_plugin->rollback (db_plugin->cls);
      if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
        continue;
      GNUNET_break (0);
      return false;
    }

    qs = db_plugin->commit (db_plugin->cls);
    if (0 > qs)
    {
      db_plugin->rollback (db_plugin->cls);
      if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
        continue;
      GNUNET_break (0);
      return false;
    }
    break;
  } /* end of retry loop */
  if (qs < 0)
  {
    GNUNET_break (0);
    return false;
  }
  return true;
}


/**
 * Function called with information about who is auditing
 * a particular exchange and what keys the exchange is using.
 *
 * @param cls closure with a `struct Exchange *`
 * @param kr response data
 * @param[in] keys the keys of the exchange
 */
static void
cert_cb (
  void *cls,
  const struct TALER_EXCHANGE_KeysResponse *kr,
  struct TALER_EXCHANGE_Keys *keys)
{
  struct Exchange *e = cls;
  struct GNUNET_TIME_Absolute n;
  struct GNUNET_TIME_Absolute first_retry;

  e->conn = NULL;
  switch (kr->hr.http_status)
  {
  case MHD_HTTP_OK:
    TALER_EXCHANGE_keys_decref (e->keys);
    e->keys = NULL;
    if (0 != strcmp (e->currency,
                     keys->currency))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "/keys response from `%s' is for currency `%s', but we expected `%s'. Ignoring response.\n",
                  e->exchange_url,
                  keys->currency,
                  e->currency);
      TALER_EXCHANGE_keys_decref (keys);
      break;
    }
    if (0 != GNUNET_memcmp (&keys->master_pub,
                            &e->master_pub))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Master public key in %skeys response does not match. Ignoring response.\n",
                  e->exchange_url);
      TALER_EXCHANGE_keys_decref (keys);
      break;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Got new keys for %s, updating database\n",
                e->exchange_url);
    first_retry = GNUNET_TIME_relative_to_absolute (
      EXCHANGE_MAXFREQ);
    if (! store_keys (keys,
                      first_retry))
    {
      GNUNET_break (0);
      TALER_EXCHANGE_keys_decref (keys);
      break;
    }
    e->keys = keys;
    /* Reset back-off */
    e->retry_delay = EXCHANGE_MAXFREQ;
    /* limit retry */
    e->first_retry = first_retry;
    /* Limit by expiration */
    n = GNUNET_TIME_absolute_max (e->first_retry,
                                  keys->key_data_expiration.abs_time);
    if (NULL != e->retry_task)
      GNUNET_SCHEDULER_cancel (e->retry_task);
    e->retry_task = GNUNET_SCHEDULER_add_at (n,
                                             &download_keys,
                                             e);
    end_inquiry ();
    return;
  default:
    GNUNET_break (NULL == keys);
    break;
  }
  /* Try again (soon-ish) */
  e->retry_delay
    = GNUNET_TIME_STD_BACKOFF (e->retry_delay);
  n = GNUNET_TIME_absolute_max (
    e->first_retry,
    GNUNET_TIME_relative_to_absolute (e->retry_delay));
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Will download %skeys in %s\n",
              e->exchange_url,
              GNUNET_TIME_relative2s (
                GNUNET_TIME_absolute_get_remaining (n),
                true));
  if (NULL != e->retry_task)
    GNUNET_SCHEDULER_cancel (e->retry_task);
  e->retry_task
    = GNUNET_SCHEDULER_add_at (n,
                               &download_keys,
                               e);
  end_inquiry ();
}


static void
download_keys (void *cls)
{
  struct Exchange *e = cls;

  e->retry_task = NULL;
  GNUNET_break (OPEN_INQUIRY_LIMIT >= active_inquiries);
  if (OPEN_INQUIRY_LIMIT <= active_inquiries)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Cannot run job: at limit\n");
    e->limited = true;
    at_limit = true;
    return;
  }
  e->retry_delay
    = GNUNET_TIME_STD_BACKOFF (e->retry_delay);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Downloading keys from %s (%s)\n",
              e->exchange_url,
              e->force_retry ? "forced" : "regular");
  e->conn = TALER_EXCHANGE_get_keys (ctx,
                                     e->exchange_url,
                                     e->force_retry
                                     ? NULL
                                     : e->keys,
                                     &cert_cb,
                                     e);
  e->force_retry = false;
  if (NULL != e->conn)
  {
    active_inquiries++;
  }
  else
  {
    struct GNUNET_TIME_Relative n;

    n = GNUNET_TIME_relative_max (e->retry_delay,
                                  EXCHANGE_MAXFREQ);
    e->retry_task
      = GNUNET_SCHEDULER_add_delayed (n,
                                      &download_keys,
                                      e);
  }
}


/**
 * Lookup exchange by @a exchange_url. Create one
 * if it does not exist.
 *
 * @param exchange_url base URL to match against
 * @return NULL if not found
 */
static struct Exchange *
lookup_exchange (const char *exchange_url)
{
  for (struct Exchange *e = e_head;
       NULL != e;
       e = e->next)
    if (0 == strcmp (e->exchange_url,
                     exchange_url))
      return e;
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Got notification about unknown exchange `%s'\n",
              exchange_url);
  return NULL;
}


/**
 * Force immediate (re)loading of /keys for an exchange.
 *
 * @param cls NULL
 * @param extra base URL of the exchange that changed
 * @param extra_len number of bytes in @a extra
 */
static void
force_exchange_keys (void *cls,
                     const void *extra,
                     size_t extra_len)
{
  const char *url = extra;
  struct Exchange *e;

  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);
  e = lookup_exchange (url);
  if (NULL == e)
  {
    GNUNET_break (0);
    return;
  }
  if (NULL != e->conn)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Already downloading %skeys\n",
                url);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Will download %skeys in %s\n",
              url,
              GNUNET_TIME_relative2s (
                GNUNET_TIME_absolute_get_remaining (
                  e->first_retry),
                true));
  if (NULL != e->retry_task)
    GNUNET_SCHEDULER_cancel (e->retry_task);
  e->force_retry = true;
  e->retry_task
    = GNUNET_SCHEDULER_add_at (e->first_retry,
                               &download_keys,
                               e);
}


/**
 * Function called on each configuration section. Finds sections
 * about exchanges, parses the entries.
 *
 * @param cls NULL
 * @param section name of the section
 */
static void
accept_exchanges (void *cls,
                  const char *section)
{
  char *url;
  char *mks;
  char *currency;

  (void) cls;
  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");
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  for (struct Exchange *e = e_head;
       NULL != e;
       e = e->next)
  {
    if (0 == strcmp (url,
                     e->exchange_url))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Exchange `%s' configured in multiple sections, maybe set DISABLED=YES in section `%s'?\n",
                  url,
                  section);
      GNUNET_free (url);
      global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      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);
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             section,
                                             "MASTER_KEY",
                                             &mks))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               section,
                               "MASTER_KEY");
    global_ret = EXIT_NOTCONFIGURED;
    GNUNET_SCHEDULER_shutdown ();
    GNUNET_free (currency);
    GNUNET_free (url);
    return;
  }

  {
    struct Exchange *e;

    e = GNUNET_new (struct Exchange);
    e->exchange_url = url;
    e->currency = currency;
    GNUNET_CONTAINER_DLL_insert (e_head,
                                 e_tail,
                                 e);
    if (GNUNET_OK !=
        GNUNET_CRYPTO_eddsa_public_key_from_string (
          mks,
          strlen (mks),
          &e->master_pub.eddsa_pub))
    {
      GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
                                 section,
                                 "MASTER_KEY",
                                 "malformed EdDSA key");
      global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      GNUNET_free (mks);
      return;
    }
    GNUNET_free (mks);

    {
      enum GNUNET_DB_QueryStatus qs;
      struct TALER_EXCHANGE_Keys *keys = NULL;

      qs = db_plugin->select_exchange_keys (db_plugin->cls,
                                            url,
                                            &e->first_retry,
                                            &keys);
      if (qs < 0)
      {
        GNUNET_break (0);
        global_ret = EXIT_FAILURE;
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      if ( (NULL != keys) &&
           (0 != strcmp (keys->currency,
                         e->currency)) )
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "/keys cached in our database were for currency `%s', but we expected `%s'. Fetching /keys again.\n",
                    keys->currency,
                    e->currency);
        TALER_EXCHANGE_keys_decref (keys);
        keys = NULL;
      }
      if ( (NULL != keys) &&
           (0 != GNUNET_memcmp (&e->master_pub,
                                &keys->master_pub)) )
      {
        /* master pub differs => fetch keys again */
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Master public key of exchange `%s' differs from our configuration. Fetching /keys again.\n",
                    e->exchange_url);
        TALER_EXCHANGE_keys_decref (keys);
        keys = NULL;
      }
      e->keys = keys;
      if (NULL == keys)
      {
        /* done synchronously so that the active_inquiries
           is updated immediately */

        download_keys (e);
      }
      else
      {
        e->retry_task
          = GNUNET_SCHEDULER_add_at (keys->key_data_expiration.abs_time,
                                     &download_keys,
                                     e);
      }
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Exchange `%s' setup\n",
                e->exchange_url);
  }
}


/**
 * We're being aborted with CTRL-C (or SIGTERM). Shut down.
 *
 * @param cls closure (NULL)
 */
static void
shutdown_task (void *cls)
{
  (void) cls;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Running shutdown\n");
  while (NULL != e_head)
  {
    struct Exchange *e = e_head;

    GNUNET_free (e->exchange_url);
    GNUNET_free (e->currency);
    if (NULL != e->conn)
    {
      TALER_EXCHANGE_get_keys_cancel (e->conn);
      e->conn = NULL;
    }
    if (NULL != e->keys)
    {
      TALER_EXCHANGE_keys_decref (e->keys);
      e->keys = NULL;
    }
    if (NULL != e->retry_task)
    {
      GNUNET_SCHEDULER_cancel (e->retry_task);
      e->retry_task = NULL;
    }
    GNUNET_CONTAINER_DLL_remove (e_head,
                                 e_tail,
                                 e);
    GNUNET_free (e);
  }
  if (NULL != eh)
  {
    db_plugin->event_listen_cancel (eh);
    eh = NULL;
  }
  TALER_MERCHANTDB_plugin_unload (db_plugin);
  db_plugin = NULL;
  cfg = NULL;
  if (NULL != ctx)
  {
    GNUNET_CURL_fini (ctx);
    ctx = NULL;
  }
  if (NULL != rc)
  {
    GNUNET_CURL_gnunet_rc_destroy (rc);
    rc = NULL;
  }
}


/**
 * First task.
 *
 * @param cls closure, NULL
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be NULL!)
 * @param c configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *c)
{
  (void) args;
  (void) cfgfile;

  cfg = c;
  GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
                                 NULL);
  ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
                          &rc);
  rc = GNUNET_CURL_gnunet_rc_create (ctx);
  if (NULL == ctx)
  {
    GNUNET_break (0);
    GNUNET_SCHEDULER_shutdown ();
    global_ret = EXIT_FAILURE;
    return;
  }
  if (NULL ==
      (db_plugin = TALER_MERCHANTDB_plugin_load (cfg)))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to initialize DB subsystem\n");
    GNUNET_SCHEDULER_shutdown ();
    global_ret = EXIT_NOTCONFIGURED;
    return;
  }
  if (GNUNET_OK !=
      db_plugin->connect (db_plugin->cls))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to connect to database. Consider running taler-merchant-dbinit!\n");
    GNUNET_SCHEDULER_shutdown ();
    global_ret = EXIT_FAILURE;
    return;
  }
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = ntohs (sizeof (es)),
      .type = ntohs (TALER_DBEVENT_MERCHANT_EXCHANGE_FORCE_KEYS)
    };

    eh = db_plugin->event_listen (db_plugin->cls,
                                  &es,
                                  GNUNET_TIME_UNIT_FOREVER_REL,
                                  &force_exchange_keys,
                                  NULL);
  }
  GNUNET_CONFIGURATION_iterate_sections (cfg,
                                         &accept_exchanges,
                                         NULL);
  if ( (0 == active_inquiries) &&
       (test_mode) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "No more open inquiries and in test mode. Existing.\n");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
}


/**
 * The main function of taler-merchant-exchangekeyupdate
 *
 * @param argc number of arguments from the command line
 * @param argv command line arguments
 * @return 0 ok, 1 on error
 */
int
main (int argc,
      char *const *argv)
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_timetravel ('T',
                                     "timetravel"),
    GNUNET_GETOPT_option_flag ('t',
                               "test",
                               "run in test mode and exit when idle",
                               &test_mode),
    GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION),
    GNUNET_GETOPT_OPTION_END
  };
  enum GNUNET_GenericReturnValue ret;

  ret = GNUNET_PROGRAM_run (
    TALER_MERCHANT_project_data (),
    argc, argv,
    "taler-merchant-exchangekeyupdate",
    gettext_noop (
      "background process that ensures our key and configuration data on exchanges is up-to-date"),
    options,
    &run, NULL);
  if (GNUNET_SYSERR == ret)
    return EXIT_INVALIDARGUMENT;
  if (GNUNET_NO == ret)
    return EXIT_SUCCESS;
  return global_ret;
}


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