/*
  This file is part of TALER
  Copyright (C) 2024, 2025 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-donaukeyupdate.c
 * @brief Process that ensures our /keys data for all Donau instances is current
 * @author Bohdan Potuzhnyi
 * @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 "donau/donau_service.h"
#include "taler_merchant_util.h"
#include "taler_merchantdb_lib.h"
#include "taler_merchantdb_plugin.h"
#include "taler_merchant_bank_lib.h"

/**
 * Maximum frequency for the Donau interaction.
 */
#define DONAU_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 a Donau instance.
 */
struct Donau
{
  /**
   * Pointer to the next Donau instance in the doubly linked list.
   */
  struct Donau *next;

  /**
   * Pointer to the previous Donau instance in the doubly linked list.
   */
  struct Donau *prev;

  /**
   * Base URL of the Donau instance being tracked.
   * This URL is used to query the Donau service for keys and other resources.
   */
  char *donau_url;

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

  /**
   * Pointer to the keys obtained from the Donau instance.
   * This structure holds the cryptographic keys for the Donau instance.
   */
  struct DONAU_Keys *keys;

  /**
   * A handle for an ongoing /keys request to the Donau instance.
   * This is NULL when there is no active request.
   */
  struct DONAU_GetKeysHandle *conn;

  /**
   * Scheduler task for retrying a failed /keys request.
   * This task will trigger the next attempt to download the Donau keys if the previous request failed or needs to be retried.
   */
  struct GNUNET_SCHEDULER_Task *retry_task;

  /**
   * The earliest time at which the Donau instance can attempt another /keys request.
   * This is used to manage the timing between requests and ensure compliance with rate-limiting rules.
   */
  struct GNUNET_TIME_Absolute first_retry;

  /**
   * The delay between the next retry for fetching /keys.
   * Used to implement exponential backoff strategies for retries in case of failures.
   */
  struct GNUNET_TIME_Relative retry_delay;

  /**
   * A flag indicating whether this Donau instance is currently rate-limited.
   * If true, the instance is temporarily paused from making further requests due to reaching a limit.
   */
  bool limited;

  /**
   * Are we force-retrying a /keys download because some keys
   * were missing?
   */
  bool force_retry;
};


/**
 * Head of known Donau instances.
 */
static struct Donau *d_head;

/**
 * Tail of known Donau instances.
 */
static struct Donau *d_tail;

/**
 * Context for the charity force download.
 */
struct ForceCharityCtx
{
  /**
   * Pointer to the next ForceCharityCtx in the doubly linked list.
   */
  struct ForceCharityCtx *next;

  /**
   * Pointer to the previous ForceCharityCtx in the doubly linked list.
   */
  struct ForceCharityCtx *prev;

  /**
   * Serial of the Donau instance in our DB for which we running the force update.
   */
  uint64_t di_serial;

  /**
   * Base URL of the Donau instance for which we are running the force update.
   */
  char *donau_url;

  /**
   * ID of the charity for which we are running the force update.
   */
  uint64_t charity_id;

  /**
   * Handle to the charity update request.
   */
  struct DONAU_CharityGetHandle *h;
};

/**
 * Head of the list of charity force updates.
 */
static struct ForceCharityCtx *fcc_head;

/**
 * Tail of the list of charity force updates.
 */
static struct ForceCharityCtx *fcc_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;

/**
 * Our event handler listening for /charity_id forced downloads.
 */
static struct GNUNET_DB_EventHandler *eh_charity;

/**
 * Handle to the context for interacting with the Donau services.
 */
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 for a Donau instance.
 *
 * @param cls closure with a `struct Donau *`
 */
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 Donau *d = d_head;
         NULL != d;
         d = d->next)
    {
      if (! d->limited)
        continue;
      d->limited = false;
      /* done synchronously so that the active_inquiries
         is updated immediately */
      download_keys (d);
      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. Exiting.\n");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
}


/**
 * Update Donau keys in the database.
 *
 * @param keys Donau keys to persist
 * @param first_retry earliest we may retry fetching the keys
 * @return transaction status
 */
static enum GNUNET_DB_QueryStatus
insert_donau_keys_data (const struct DONAU_Keys *keys,
                        struct GNUNET_TIME_Absolute first_retry)
{
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Inserting Donau keys into the database %s\n",
              keys->donau_url);
  return db_plugin->upsert_donau_keys (db_plugin->cls,
                                       keys,
                                       first_retry);
}


/**
 * Store Donau keys in the database and handle retries.
 *
 * @param keys the keys to store
 * @param first_retry earliest time we may retry fetching the keys
 * @return true on success
 */
static bool
store_donau_keys (struct DONAU_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 donau key data"))
    {
      db_plugin->rollback (db_plugin->cls);
      GNUNET_break (0);
      return false;
    }

    qs = insert_donau_keys_data (keys,
                                 first_retry);
    if (0 > qs)
    {
      db_plugin->rollback (db_plugin->cls);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Error while inserting Donau keys into the database: status %d",
                  qs);
      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);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to commit Donau keys to the database: status %d",
                  qs);
      if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
        continue;
      GNUNET_break (0);
      return false;
    }
    break;
  }
  if (qs < 0)
  {
    GNUNET_break (0);
    return false;
  }
  return true;
}


/**
 * Store Donau charity in the database and handle retries.
 *
 * @param charity_id the charity ID to store
 * @param donau_url the base URL of the Donau instance
 * @param charity the charity structure to store
 */
static bool
store_donau_charity (uint64_t charity_id,
                     const char *donau_url,
                     const struct DONAU_Charity *charity)
{
  enum GNUNET_DB_QueryStatus qs;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Inserting/updating charity %llu for Donau `%s'\n",
              (unsigned long long) charity_id,
              donau_url);

  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 donau charity data"))
    {
      db_plugin->rollback (db_plugin->cls);
      GNUNET_break (0);
      return false;
    }

    qs = db_plugin->update_donau_instance (db_plugin->cls,
                                           donau_url,
                                           charity,
                                           charity_id);
    if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    {
      db_plugin->rollback (db_plugin->cls);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Error while updating charity into the database: status %d",
                  qs);
      continue;
    }
    if (0 >= qs)
    {
      db_plugin->rollback (db_plugin->cls);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Error while updating charity into the database: status %d",
                  qs);
      return false;
    }

    qs = db_plugin->commit (db_plugin->cls);
    if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    {
      db_plugin->rollback (db_plugin->cls);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to commit charity data to the database: status %d",
                  qs);
      continue;
    }
    if (0 > qs)
    {
      db_plugin->rollback (db_plugin->cls);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to commit charity data to the database: status %d",
                  qs);
      return false;
    }
    break;
  }
  if (0 >= qs)
  {
    GNUNET_break (0);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Retries exhausted while inserting charity %llu for Donau `%s': last status %d",
                (unsigned long long) charity_id,
                donau_url,
                qs);
    return false;
  }
  return true;
}


/**
 * Callback after Donau keys are fetched.
 *
 * @param cls closure with a `struct Donau *`
 * @param kr response data
 * @param keys the keys of the Donau instance
 */
static void
donau_cert_cb (
  void *cls,
  const struct DONAU_KeysResponse *kr,
  struct DONAU_Keys *keys)
{
  struct Donau *d = cls;
  struct GNUNET_TIME_Absolute n;
  struct GNUNET_TIME_Absolute first_retry;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Starting donau cert with object \n");

  d->conn = NULL;
  switch (kr->hr.http_status)
  {
  case MHD_HTTP_OK:
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Got new keys for %s, updating database\n",
                d->donau_url);
    first_retry = GNUNET_TIME_relative_to_absolute (DONAU_MAXFREQ);
    if (! store_donau_keys (keys,
                            first_retry))
    {
      GNUNET_break (0);
      DONAU_keys_decref (keys);
      break;
    }

    d->keys = keys;
    /* Reset back-off */
    d->retry_delay = DONAU_MAXFREQ;
    /* limit retry */
    d->first_retry = first_retry;

    /* FIXME: Might be good to reference some key_data_expiration and not first sign_key*/
    n = GNUNET_TIME_absolute_max (d->first_retry,
                                  keys->sign_keys[0].expire_sign.abs_time);
    if (NULL != d->retry_task)
      GNUNET_SCHEDULER_cancel (d->retry_task);
    d->retry_task = GNUNET_SCHEDULER_add_at (n,
                                             &download_keys,
                                             d);
    end_inquiry ();
    return;
  default:
    GNUNET_break (NULL == keys);
    break;
  }

  d->retry_delay
    = GNUNET_TIME_STD_BACKOFF (d->retry_delay);
  n = GNUNET_TIME_absolute_max (
    d->first_retry,
    GNUNET_TIME_relative_to_absolute (d->retry_delay));

  if (NULL != d->retry_task)
    GNUNET_SCHEDULER_cancel (d->retry_task);
  d->retry_task
    = GNUNET_SCHEDULER_add_at (n,
                               &download_keys,
                               d);
  end_inquiry ();
}


/**
 * Initiate the download of Donau keys.
 *
 * @param cls closure with a `struct Donau *`
 */
static void
download_keys (void *cls)
{
  struct Donau *d = cls;

  d->retry_task = NULL;
  GNUNET_break (OPEN_INQUIRY_LIMIT >= active_inquiries);
  if (OPEN_INQUIRY_LIMIT <= active_inquiries)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Cannot start more donaukeys inquiries, already at limit\n");
    d->limited = true;
    at_limit = true;
    return;
  }
  d->retry_delay
    = GNUNET_TIME_STD_BACKOFF (d->retry_delay);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Downloading keys from %s (%s)\n",
              d->donau_url,
              d->force_retry ? "forced" : "regular");
  d->conn = DONAU_get_keys (ctx,
                            d->donau_url,
                            &donau_cert_cb,
                            d);
  d->force_retry = false;
  if (NULL != d->conn)
  {
    active_inquiries++;
  }
  else
  {
    struct GNUNET_TIME_Relative n;

    n = GNUNET_TIME_relative_max (d->retry_delay,
                                  DONAU_MAXFREQ);

    d->retry_task
      = GNUNET_SCHEDULER_add_delayed (n,
                                      &download_keys,
                                      d);
  }
}


/**
 * Callback for DONAU_charity_get() that stores the charity
 * information in the DB and finishes the inquiry.
 *
 * @param cls closure with `struct ForceCharityCtx *`
 * @param gcr response from DONAU
 */
static void
donau_charity_cb (void *cls,
                  const struct DONAU_GetCharityResponse *gcr)
{
  struct ForceCharityCtx *fcc = cls;
  fcc->h = NULL;

  switch (gcr->hr.http_status)
  {
  case MHD_HTTP_OK:
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Got charity_id `%llu' details for donau `%s', updating DB\n",
                (unsigned long long) fcc->charity_id,
                fcc->donau_url);

    if (! store_donau_charity (fcc->charity_id,
                               fcc->donau_url,
                               &gcr->details.ok.charity))
    {
      GNUNET_break (0);
    }
    break;

  default:
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "DONAU charity_get for `%s' failed with HTTP %u / ec %u\n",
                fcc->donau_url,
                gcr->hr.http_status,
                gcr->hr.ec);
    break;
  }

  end_inquiry ();
}


/**
 * Download the charity_id for a Donau instance.
 *
 * @param cls closure with a `struct Donau *`
 */
static void
download_charity_id (void *cls)
{
  struct ForceCharityCtx *fcc = cls;

  /* nothing to do if a request is already outstanding */
  if (NULL != fcc->h)
    return;

  /* respect global inquiry limit */
  GNUNET_break (OPEN_INQUIRY_LIMIT >= active_inquiries);
  if (OPEN_INQUIRY_LIMIT <= active_inquiries)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Cannot start more charity inquiries, already at limit\n");
    return;
  }

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Downloading charity `%llu' from `%s'\n",
              (unsigned long long) fcc->charity_id,
              fcc->donau_url);

  fcc->h = DONAU_charity_get (ctx,
                              fcc->donau_url,
                              fcc->charity_id,
                              NULL,          /* bearer token -- not needed */
                              &donau_charity_cb,
                              fcc);

  if (NULL != fcc->h)
  {
    active_inquiries++;
  }
  else
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Failed to initiate DONAU_charity_get() for `%s'\n",
                fcc->donau_url);
    /* we do NOT retry here – simply finish the (failed) inquiry */
    end_inquiry ();
  }
}


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


/**
 * Lookup a ForceCharityCtx by donau-instance serial.
 *
 * @param di_serial serial to search for
 * @return matching context or NULL
 */
static struct ForceCharityCtx *
lookup_donau_charity (uint64_t di_serial)
{
  for (struct ForceCharityCtx *fcc = fcc_head;
       NULL != fcc;
       fcc = fcc->next)
    if (fcc->di_serial == di_serial)
      return fcc;
  return NULL;
}


/**
 * Force immediate (re)loading of /charity_id for an donau.
 *
 * @param cls NULL
 * @param extra base URL of the donau that changed
 * @param extra_len number of bytes in @a extra
 */
static void
force_donau_charity_id (void *cls,
                        const void *extra,
                        size_t extra_len)
{
  uint64_t di_serial;
  char *donau_url = NULL;
  uint64_t charity_id = -1;
  enum GNUNET_DB_QueryStatus qs;
  struct ForceCharityCtx *fcc;

  if ( (sizeof(uint64_t) != extra_len) ||
       (NULL == extra) )
  {
    GNUNET_break (0);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Incorrect extra for the force_donau_charity_id");
    return;
  }
  GNUNET_memcpy (&di_serial,
                 extra,
                 sizeof(uint64_t));
  di_serial = GNUNET_ntohll (di_serial);
  qs = db_plugin->select_donau_instance_by_serial (db_plugin->cls,
                                                   di_serial,
                                                   &donau_url,
                                                   &charity_id);

  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
  {
    GNUNET_break (0);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "force_donau_charity_id: instance serial %llu not found (status %d)\n",
                (unsigned long long) di_serial,
                qs);
    return;
  }

  fcc = lookup_donau_charity (di_serial);
  if (NULL == fcc)
  {
    fcc = GNUNET_new (struct ForceCharityCtx);
    fcc->di_serial   = di_serial;
    fcc->donau_url   = donau_url;      /* take ownership */
    fcc->charity_id  = charity_id;
    GNUNET_CONTAINER_DLL_insert (fcc_head, fcc_tail, fcc);

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Created new ForceCharityCtx for donau `%s' "
                "(serial %llu, charity %llu)\n",
                donau_url,
                (unsigned long long) di_serial,
                (unsigned long long) charity_id);
  }
  else
  {
    GNUNET_free (donau_url);
    if (NULL != fcc->h)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Already downloading charity_id for donau `%s'\n",
                  fcc->donau_url);
      return;
    }
  }
  download_charity_id (fcc);
}


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

  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 update notification: reload `%s'\n",
              url);

  d = lookup_donau (url);
  if (NULL == d)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Donau instance `%s' not found. Creating new instance.\n",
                url);

    d = GNUNET_new (struct Donau);
    d->donau_url = GNUNET_strdup (url);
    d->retry_delay = DONAU_MAXFREQ;
    d->first_retry = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_ZERO);

    GNUNET_CONTAINER_DLL_insert (d_head,
                                 d_tail,
                                 d);
    download_keys (d);
  }

  if (NULL != d->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 (
                  d->first_retry),
                true));
  if (NULL != d->retry_task)
    GNUNET_SCHEDULER_cancel (d->retry_task);
  d->force_retry = true;
  d->retry_task
    = GNUNET_SCHEDULER_add_at (d->first_retry,
                               &download_keys,
                               d);
}


/**
 * 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 != d_head)
  {
    struct Donau *d = d_head;

    GNUNET_free (d->donau_url);
    GNUNET_free (d->currency);
    if (NULL != d->conn)
    {
      DONAU_get_keys_cancel (d->conn);
      d->conn = NULL;
    }
    if (NULL != d->keys)
    {
      DONAU_keys_decref (d->keys);
      d->keys = NULL;
    }
    if (NULL != d->retry_task)
    {
      GNUNET_SCHEDULER_cancel (d->retry_task);
      d->retry_task = NULL;
    }
    GNUNET_CONTAINER_DLL_remove (d_head,
                                 d_tail,
                                 d);
    GNUNET_free (d);
  }
  if (NULL != eh)
  {
    db_plugin->event_listen_cancel (eh);
    eh = NULL;
  }
  if (NULL != eh_charity)
  {
    db_plugin->event_listen_cancel (eh_charity);
    eh_charity = 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;
  }
}


/**
 * Callback function typically used by `select_donau_instances` to handle
 * the details of each Donau instance retrieved from the database.
 *
 * @param cls Closure to pass additional context or data to the callback function.
 * @param donau_instance_serial Serial number of the Donau instance in the merchant database.
 * @param donau_url The URL of the Donau instance.
 * @param charity_name The name of the charity associated with the Donau instance.
 * @param charity_pub_key Pointer to the charity's public key used for cryptographic operations.
 * @param charity_id The unique identifier for the charity within the Donau instance.
 * @param charity_max_per_year Maximum allowed donations to the charity for the current year.
 * @param charity_receipts_to_date Total donations received by the charity so far in the current year.
 * @param current_year The year for which the donation data is being tracked.
 * @param donau_keys_json JSON object containing additional key-related information for the Donau instance.
 */
static void
accept_donau (
  void *cls,
  uint64_t donau_instance_serial,
  const char *donau_url,
  const char *charity_name,
  const struct DONAU_CharityPublicKeyP *charity_pub_key,
  uint64_t charity_id,
  const struct TALER_Amount *charity_max_per_year,
  const struct TALER_Amount *charity_receipts_to_date,
  int64_t current_year,
  const json_t *donau_keys_json
  )
{
  struct Donau *d;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Donau instance `%s' not found. Creating new instance.\n",
              donau_url);
  d = GNUNET_new (struct Donau);
  d->donau_url = GNUNET_strdup (donau_url);
  d->retry_delay = DONAU_MAXFREQ;
  d->first_retry = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_ZERO);
  GNUNET_CONTAINER_DLL_insert (d_head,
                               d_tail,
                               d);
  if (NULL == donau_keys_json)
  {
    download_keys (d);
    return;
  }
  d->keys = DONAU_keys_from_json (donau_keys_json);
  if (NULL == d->keys)
  {
    GNUNET_break (0);
    download_keys (d);
    return;
  }
  d->retry_delay = DONAU_MAXFREQ;
  d->first_retry = GNUNET_TIME_relative_to_absolute (DONAU_MAXFREQ);

  {
    struct GNUNET_TIME_Absolute n;

    n = GNUNET_TIME_absolute_min (
      d->first_retry,
      d->keys->sign_keys[0].expire_sign.abs_time);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Will download %skeys in %s\n",
                donau_url,
                GNUNET_TIME_relative2s (
                  GNUNET_TIME_absolute_get_remaining (n),
                  true));
    d->retry_task = GNUNET_SCHEDULER_add_at (n,
                                             &download_keys,
                                             d);
  }
}


/**
 * 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 == 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\n");
    GNUNET_SCHEDULER_shutdown ();
    global_ret = EXIT_FAILURE;
    return;
  }
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = ntohs (sizeof(es)),
      .type = ntohs (TALER_DBEVENT_MERCHANT_DONAU_KEYS)
    };

    eh = db_plugin->event_listen (db_plugin->cls,
                                  &es,
                                  GNUNET_TIME_UNIT_FOREVER_REL,
                                  &force_donau_keys,
                                  NULL);
  }
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = ntohs (sizeof(es)),
      .type = ntohs (TALER_DBEVENT_MERCHANT_DONAU_CHARITY_ID)
    };

    eh_charity = db_plugin->event_listen (
      db_plugin->cls,
      &es,
      GNUNET_TIME_UNIT_FOREVER_REL,
      &force_donau_charity_id,
      NULL);
  }

  {
    enum GNUNET_DB_QueryStatus qs;

    qs = db_plugin->select_all_donau_instances (db_plugin->cls,
                                                &accept_donau,
                                                NULL);
    if (qs < 0)
    {
      GNUNET_break (0);
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
  }
  if ( (0 == active_inquiries) &&
       (test_mode) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "No donau keys inquiries to start, exiting.\n");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
}


/**
 * The main function of taler-merchant-donaukeyupdate
 *
 * @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-donaukeyupdate",
    gettext_noop (
      "background process that ensures our key and configuration data on Donau 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-donaukeyupdate.c */
