/*
  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-depositcheck.c
 * @brief Process that inquires with the exchange for deposits that should have been wired
 * @author Christian Grothoff
 */
#include "platform.h"
#include "microhttpd.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#include <pthread.h>
#include "taler_merchant_util.h"
#include "taler_merchantdb_lib.h"
#include "taler_merchantdb_plugin.h"
#include <taler/taler_dbevents.h>

/**
 * How many requests do we make at most in parallel to the same exchange?
 */
#define CONCURRENCY_LIMIT 32

/**
 * How long do we not try a deposit check if the deposit
 * was put on hold due to a KYC/AML block?
 */
#define KYC_RETRY_DELAY GNUNET_TIME_UNIT_HOURS

/**
 * Information we keep per exchange.
 */
struct Child
{

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

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

  /**
   * The child process.
   */
  struct GNUNET_OS_Process *process;

  /**
   * Wait handle.
   */
  struct GNUNET_ChildWaitHandle *cwh;

  /**
   * Which exchange is this state for?
   */
  char *base_url;

  /**
   * Task to restart the child.
   */
  struct GNUNET_SCHEDULER_Task *rt;

  /**
   * When should the child be restarted at the earliest?
   */
  struct GNUNET_TIME_Absolute next_start;

  /**
   * Current minimum delay between restarts, grows
   * exponentially if child exits before this time.
   */
  struct GNUNET_TIME_Relative rd;

};


/**
 * Information we keep per exchange interaction.
 */
struct ExchangeInteraction
{
  /**
   * Kept in a DLL.
   */
  struct ExchangeInteraction *next;

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

  /**
   * Handle for exchange interaction.
   */
  struct TALER_EXCHANGE_DepositGetHandle *dgh;

  /**
   * Wire deadline for the deposit.
   */
  struct GNUNET_TIME_Absolute wire_deadline;

  /**
   * Current value for the retry backoff
   */
  struct GNUNET_TIME_Relative retry_backoff;

  /**
   * Target account hash of the deposit.
   */
  struct TALER_MerchantWireHashP h_wire;

  /**
   * Deposited amount.
   */
  struct TALER_Amount amount_with_fee;

  /**
   * Deposit fee paid.
   */
  struct TALER_Amount deposit_fee;

  /**
   * Public key of the deposited coin.
   */
  struct TALER_CoinSpendPublicKeyP coin_pub;

  /**
   * Hash over the @e contract_terms.
   */
  struct TALER_PrivateContractHashP h_contract_terms;

  /**
   * Merchant instance's private key.
   */
  struct TALER_MerchantPrivateKeyP merchant_priv;

  /**
   * Serial number of the row in the deposits table
   * that we are processing.
   */
  uint64_t deposit_serial;

  /**
   * The instance the deposit belongs to.
   */
  char *instance_id;

};


/**
 * Head of list of children we forked.
 */
static struct Child *c_head;

/**
 * Tail of list of children we forked.
 */
static struct Child *c_tail;

/**
 * Key material of the exchange.
 */
static struct TALER_EXCHANGE_Keys *keys;

/**
 * Head of list of active exchange interactions.
 */
static struct ExchangeInteraction *w_head;

/**
 * Tail of list of active exchange interactions.
 */
static struct ExchangeInteraction *w_tail;

/**
 * Number of active entries in the @e w_head list.
 */
static uint64_t w_count;

/**
 * Notification handler from database on new work.
 */
static struct GNUNET_DB_EventHandler *eh;

/**
 * Notification handler from database on new keys.
 */
static struct GNUNET_DB_EventHandler *keys_eh;

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

/**
 * Name of the configuration file we use.
 */
static char *cfg_filename;

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

/**
 * Next wire deadline that @e task is scheduled for.
 */
static struct GNUNET_TIME_Absolute next_deadline;

/**
 * Next task to run, if any.
 */
static struct GNUNET_SCHEDULER_Task *task;

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

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

/**
 * Which exchange are we monitoring? NULL if we
 * are the parent of the workers.
 */
static char *exchange_url;

/**
 * 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;


/**
 * We're being aborted with CTRL-C (or SIGTERM). Shut down.
 *
 * @param cls closure
 */
static void
shutdown_task (void *cls)
{
  struct Child *c;
  struct ExchangeInteraction *w;

  (void) cls;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Running shutdown\n");
  if (NULL != eh)
  {
    db_plugin->event_listen_cancel (eh);
    eh = NULL;
  }
  if (NULL != keys_eh)
  {
    db_plugin->event_listen_cancel (keys_eh);
    keys_eh = NULL;
  }
  if (NULL != task)
  {
    GNUNET_SCHEDULER_cancel (task);
    task = NULL;
  }
  while (NULL != (w = w_head))
  {
    GNUNET_CONTAINER_DLL_remove (w_head,
                                 w_tail,
                                 w);
    if (NULL != w->dgh)
    {
      TALER_EXCHANGE_deposits_get_cancel (w->dgh);
      w->dgh = NULL;
    }
    w_count--;
    GNUNET_free (w->instance_id);
    GNUNET_free (w);
  }
  while (NULL != (c = c_head))
  {
    GNUNET_CONTAINER_DLL_remove (c_head,
                                 c_tail,
                                 c);
    if (NULL != c->rt)
    {
      GNUNET_SCHEDULER_cancel (c->rt);
      c->rt = NULL;
    }
    if (NULL != c->cwh)
    {
      GNUNET_wait_child_cancel (c->cwh);
      c->cwh = NULL;
    }
    if (NULL != c->process)
    {
      enum GNUNET_OS_ProcessStatusType type
        = GNUNET_OS_PROCESS_UNKNOWN;
      unsigned long code = 0;

      GNUNET_break (0 ==
                    GNUNET_OS_process_kill (c->process,
                                            SIGTERM));
      GNUNET_break (GNUNET_OK ==
                    GNUNET_OS_process_wait_status (c->process,
                                                   &type,
                                                   &code));
      if ( (GNUNET_OS_PROCESS_EXITED != type) ||
           (0 != code) )
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Process for exchange %s had trouble (%d/%d)\n",
                    c->base_url,
                    (int) type,
                    (int) code);
      GNUNET_OS_process_destroy (c->process);
    }
    GNUNET_free (c->base_url);
    GNUNET_free (c);
  }
  if (NULL != db_plugin)
  {
    db_plugin->rollback (db_plugin->cls); /* just in case */
    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;
  }
}


/**
 * Task to get more deposits to work on from the database.
 *
 * @param cls NULL
 */
static void
select_work (void *cls);


/**
 * Make sure to run the select_work() task at
 * the @a next_deadline.
 *
 * @param deadline time when work becomes ready
 */
static void
run_at (struct GNUNET_TIME_Absolute deadline)
{
  if ( (NULL != task) &&
       (GNUNET_TIME_absolute_cmp (deadline,
                                  >,
                                  next_deadline)) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Not scheduling for %s yet, already have earlier task pending\n",
                GNUNET_TIME_absolute2s (deadline));
    return;
  }
  if (NULL == keys)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Not scheduling for %s yet, no /keys available\n",
                GNUNET_TIME_absolute2s (deadline));
    return; /* too early */
  }
  next_deadline = deadline;
  if (NULL != task)
    GNUNET_SCHEDULER_cancel (task);
  task = GNUNET_SCHEDULER_add_at (deadline,
                                  &select_work,
                                  NULL);
}


/**
 * Function called with detailed wire transfer data.
 *
 * @param cls closure with a `struct ExchangeInteraction *`
 * @param dr HTTP response data
 */
static void
deposit_get_cb (
  void *cls,
  const struct TALER_EXCHANGE_GetDepositResponse *dr)
{
  struct ExchangeInteraction *w = cls;
  struct GNUNET_TIME_Absolute future_retry;

  w->dgh = NULL;
  future_retry
    = GNUNET_TIME_relative_to_absolute (w->retry_backoff);
  switch (dr->hr.http_status)
  {
  case MHD_HTTP_OK:
    {
      enum GNUNET_DB_QueryStatus qs;

      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Exchange returned wire transfer over %s for deposited coin %s\n",
                  TALER_amount2s (&dr->details.ok.coin_contribution),
                  TALER_B2S (&w->coin_pub));
      qs = db_plugin->insert_deposit_to_transfer (
        db_plugin->cls,
        w->deposit_serial,
        &w->h_wire,
        exchange_url,
        &dr->details.ok);
      if (qs <= 0)
      {
        GNUNET_break (0);
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      break;
    }
  case MHD_HTTP_ACCEPTED:
    {
      /* got a 'preliminary' reply from the exchange,
         remember our target UUID */
      enum GNUNET_DB_QueryStatus qs;
      struct GNUNET_TIME_Timestamp now;

      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Exchange returned KYC requirement (%d) for deposited coin %s\n",
                  dr->details.accepted.kyc_ok,
                  TALER_B2S (&w->coin_pub));
      now = GNUNET_TIME_timestamp_get ();
      qs = db_plugin->account_kyc_set_failed (
        db_plugin->cls,
        w->instance_id,
        &w->h_wire,
        exchange_url,
        now,
        MHD_HTTP_ACCEPTED,
        dr->details.accepted.kyc_ok);
      if (qs < 0)
      {
        GNUNET_break (0);
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      if (dr->details.accepted.kyc_ok)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Bumping wire transfer deadline in DB to %s as that is when we will retry\n",
                    GNUNET_TIME_absolute2s (future_retry));
        qs = db_plugin->update_deposit_confirmation_status (
          db_plugin->cls,
          w->deposit_serial,
          true, /* need to try again in the future! */
          GNUNET_TIME_absolute_to_timestamp (future_retry),
          MHD_HTTP_ACCEPTED,
          TALER_EC_NONE,
          "Exchange reported 202 Accepted but no KYC block");
        if (qs < 0)
        {
          GNUNET_break (0);
          GNUNET_SCHEDULER_shutdown ();
          return;
        }
      }
      else
      {
        future_retry
          = GNUNET_TIME_absolute_max (
              future_retry,
              GNUNET_TIME_relative_to_absolute (
                KYC_RETRY_DELAY));
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Bumping wire transfer deadline in DB to %s as that is when we will retry\n",
                    GNUNET_TIME_absolute2s (future_retry));
        qs = db_plugin->update_deposit_confirmation_status (
          db_plugin->cls,
          w->deposit_serial,
          true /* need to try again in the future */,
          GNUNET_TIME_absolute_to_timestamp (future_retry),
          MHD_HTTP_ACCEPTED,
          TALER_EC_NONE,
          "Exchange reported 202 Accepted due to KYC/AML block");
        if (qs < 0)
        {
          GNUNET_break (0);
          GNUNET_SCHEDULER_shutdown ();
          return;
        }
      }
      break;
    }
  default:
    {
      enum GNUNET_DB_QueryStatus qs;
      bool retry_needed = false;

      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Exchange %s returned tracking failure for deposited coin %s: %u\n",
                  exchange_url,
                  TALER_B2S (&w->coin_pub),
                  dr->hr.http_status);
      /* rough classification by HTTP status group */
      switch (dr->hr.http_status / 100)
      {
      case 0:
        /* timeout */
        retry_needed = true;
        break;
      case 1:
      case 2:
      case 3:
        /* very strange */
        retry_needed = false;
        break;
      case 4:
        /* likely fatal */
        retry_needed = false;
        break;
      case 5:
        /* likely transient */
        retry_needed = true;
        break;
      }
      qs = db_plugin->update_deposit_confirmation_status (
        db_plugin->cls,
        w->deposit_serial,
        retry_needed,
        GNUNET_TIME_absolute_to_timestamp (future_retry),
        (uint32_t) dr->hr.http_status,
        dr->hr.ec,
        dr->hr.hint);
      if (qs < 0)
      {
        GNUNET_break (0);
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      break;
    }
  } /* end switch */

  GNUNET_CONTAINER_DLL_remove (w_head,
                               w_tail,
                               w);
  w_count--;
  GNUNET_free (w->instance_id);
  GNUNET_free (w);
  GNUNET_assert (NULL != keys);
  if (0 == w_count)
  {
    /* We only SELECT() again after having finished
       all requests, as otherwise we'll most like
       just SELECT() those again that are already
       being requested; alternatively, we could
       update the retry_time already on SELECT(),
       but this should be easier on the DB. */
    if (NULL != task)
      GNUNET_SCHEDULER_cancel (task);
    task = GNUNET_SCHEDULER_add_now (&select_work,
                                     NULL);
  }
}


/**
 * Typically called by `select_work`.
 *
 * @param cls NULL
 * @param deposit_serial identifies the deposit operation
 * @param wire_deadline when is the wire due
 * @param retry_time current value for the retry backoff
 * @param h_contract_terms hash of the contract terms
 * @param merchant_priv private key of the merchant
 * @param instance_id row ID of the instance
 * @param h_wire hash of the merchant's wire account into
 * @param amount_with_fee amount the exchange will deposit for this coin
 * @param deposit_fee fee the exchange will charge for this coin which the deposit was made
 * @param coin_pub public key of the deposited coin
 */
static void
pending_deposits_cb (
  void *cls,
  uint64_t deposit_serial,
  struct GNUNET_TIME_Absolute wire_deadline,
  struct GNUNET_TIME_Absolute retry_time,
  const struct TALER_PrivateContractHashP *h_contract_terms,
  const struct TALER_MerchantPrivateKeyP *merchant_priv,
  const char *instance_id,
  const struct TALER_MerchantWireHashP *h_wire,
  const struct TALER_Amount *amount_with_fee,
  const struct TALER_Amount *deposit_fee,
  const struct TALER_CoinSpendPublicKeyP *coin_pub)
{
  struct ExchangeInteraction *w;
  struct GNUNET_TIME_Absolute mx
    = GNUNET_TIME_absolute_max (wire_deadline,
                                retry_time);
  struct GNUNET_TIME_Relative retry_backoff;

  (void) cls;
  if (GNUNET_TIME_absolute_is_future (mx))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Pending deposit should be checked next at %s\n",
                GNUNET_TIME_absolute2s (mx));
    run_at (mx);
    return;
  }
  if (GNUNET_TIME_absolute_is_zero (retry_time))
    retry_backoff = GNUNET_TIME_absolute_get_duration (wire_deadline);
  else
    retry_backoff = GNUNET_TIME_absolute_get_difference (wire_deadline,
                                                         retry_time);
  w = GNUNET_new (struct ExchangeInteraction);
  w->deposit_serial = deposit_serial;
  w->wire_deadline = wire_deadline;
  w->retry_backoff = GNUNET_TIME_randomized_backoff (retry_backoff,
                                                     GNUNET_TIME_UNIT_DAYS);
  w->h_contract_terms = *h_contract_terms;
  w->merchant_priv = *merchant_priv;
  w->h_wire = *h_wire;
  w->amount_with_fee = *amount_with_fee;
  w->deposit_fee = *deposit_fee;
  w->coin_pub = *coin_pub;
  w->instance_id = GNUNET_strdup (instance_id);
  GNUNET_CONTAINER_DLL_insert (w_head,
                               w_tail,
                               w);
  w_count++;
  GNUNET_assert (NULL != keys);
  if (GNUNET_TIME_absolute_is_past (
        keys->key_data_expiration.abs_time))
  {
    /* Parent should re-start us, then we will re-fetch /keys */
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "/keys expired, shutting down\n");
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  GNUNET_assert (NULL == w->dgh);
  w->dgh = TALER_EXCHANGE_deposits_get (
    ctx,
    exchange_url,
    keys,
    &w->merchant_priv,
    &w->h_wire,
    &w->h_contract_terms,
    &w->coin_pub,
    GNUNET_TIME_UNIT_ZERO,
    &deposit_get_cb,
    w);
}


/**
 * Function called on events received from Postgres.
 *
 * @param cls closure, NULL
 * @param extra additional event data provided, timestamp with wire deadline
 * @param extra_size number of bytes in @a extra
 */
static void
db_notify (void *cls,
           const void *extra,
           size_t extra_size)
{
  struct GNUNET_TIME_Absolute deadline;
  struct GNUNET_TIME_AbsoluteNBO nbo_deadline;

  (void) cls;
  if (sizeof (nbo_deadline) != extra_size)
  {
    GNUNET_break (0);
    return;
  }
  if (0 != w_count)
    return; /* already at work! */
  memcpy (&nbo_deadline,
          extra,
          extra_size);
  deadline = GNUNET_TIME_absolute_ntoh (nbo_deadline);
  run_at (deadline);
}


static void
select_work (void *cls)
{
  bool retry = false;
  uint64_t limit = CONCURRENCY_LIMIT - w_count;

  (void) cls;
  task = NULL;
  GNUNET_assert (w_count <= CONCURRENCY_LIMIT);
  GNUNET_assert (NULL != keys);
  if (0 == limit)
  {
    GNUNET_break (0);
    return;
  }
  if (GNUNET_TIME_absolute_is_past (
        keys->key_data_expiration.abs_time))
  {
    /* Parent should re-start us, then we will re-fetch /keys */
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  while (1)
  {
    enum GNUNET_DB_QueryStatus qs;

    db_plugin->preflight (db_plugin->cls);
    if (retry)
      limit = 1;
    qs = db_plugin->lookup_pending_deposits (
      db_plugin->cls,
      exchange_url,
      limit,
      retry,
      &pending_deposits_cb,
      NULL);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Looking up pending deposits query status was %d\n",
                (int) qs);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
    case GNUNET_DB_STATUS_SOFT_ERROR:
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Transaction failed!\n");
      global_ret = EXIT_FAILURE;
      GNUNET_SCHEDULER_shutdown ();
      return;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      if (test_mode)
      {
        GNUNET_SCHEDULER_shutdown ();
        return;
      }
      if (retry)
        return; /* nothing left */
      retry = true;
      continue;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    default:
      /* wait for async completion, then select more work. */
      return;
    }
  }
}


/**
 * Start a copy of this process with the exchange URL
 * set to the given @a base_url
 *
 * @param base_url base URL to run with
 */
static struct GNUNET_OS_Process *
start_worker (const char *base_url)
{
  char toff[30];
  long long zo;

  zo = GNUNET_TIME_get_offset ();
  GNUNET_snprintf (toff,
                   sizeof (toff),
                   "%lld",
                   zo);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Launching worker for exchange `%s' using `%s`\n",
              base_url,
              NULL == cfg_filename
              ? "<default>"
              : cfg_filename);
  if (NULL == cfg_filename)
    return GNUNET_OS_start_process (
      GNUNET_OS_INHERIT_STD_ALL,
      NULL,
      NULL,
      NULL,
      "taler-merchant-depositcheck",
      "taler-merchant-depositcheck",
      "-e", base_url,
      "-L", "INFO",
      "-T", toff,
      test_mode ? "-t" : NULL,
      NULL);
  return GNUNET_OS_start_process (
    GNUNET_OS_INHERIT_STD_ALL,
    NULL,
    NULL,
    NULL,
    "taler-merchant-depositcheck",
    "taler-merchant-depositcheck",
    "-c", cfg_filename,
    "-e", base_url,
    "-L", "INFO",
    "-T", toff,
    test_mode ? "-t" : NULL,
    NULL);
}


/**
 * Restart worker process for the given child.
 *
 * @param cls a `struct Child *` that needs a worker.
 */
static void
restart_child (void *cls);


/**
 * Function called upon death or completion of a child process.
 *
 * @param cls a `struct Child *`
 * @param type type of the process
 * @param exit_code status code of the process
 */
static void
child_done_cb (void *cls,
               enum GNUNET_OS_ProcessStatusType type,
               long unsigned int exit_code)
{
  struct Child *c = cls;

  c->cwh = NULL;
  GNUNET_OS_process_destroy (c->process);
  c->process = NULL;
  if ( (GNUNET_OS_PROCESS_EXITED != type) ||
       (0 != exit_code) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Process for exchange %s had trouble (%d/%d)\n",
                c->base_url,
                (int) type,
                (int) exit_code);
    GNUNET_SCHEDULER_shutdown ();
    global_ret = EXIT_NOTINSTALLED;
    return;
  }
  if (test_mode &&
      (! GNUNET_TIME_relative_is_zero (c->rd)) )
  {
    return;
  }
  if (GNUNET_TIME_absolute_is_future (c->next_start))
    c->rd = GNUNET_TIME_STD_BACKOFF (c->rd);
  else
    c->rd = GNUNET_TIME_UNIT_SECONDS;
  c->rt = GNUNET_SCHEDULER_add_at (c->next_start,
                                   &restart_child,
                                   c);
}


static void
restart_child (void *cls)
{
  struct Child *c = cls;

  c->rt = NULL;
  c->next_start = GNUNET_TIME_relative_to_absolute (c->rd);
  c->process = start_worker (c->base_url);
  if (NULL == c->process)
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                         "exec");
    global_ret = EXIT_NO_RESTART;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  c->cwh = GNUNET_wait_child (c->process,
                              &child_done_cb,
                              c);
}


/**
 * Function to iterate over section.
 *
 * @param cls closure
 * @param section name of the section
 */
static void
cfg_iter_cb (void *cls,
             const char *section)
{
  char *base_url;
  struct Child *c;

  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",
                                             &base_url))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
                               section,
                               "EXCHANGE_BASE_URL");
    return;
  }
  c = GNUNET_new (struct Child);
  c->rd = GNUNET_TIME_UNIT_SECONDS;
  c->base_url = base_url;
  GNUNET_CONTAINER_DLL_insert (c_head,
                               c_tail,
                               c);
  c->rt = GNUNET_SCHEDULER_add_now (&restart_child,
                                    c);
}


/**
 * 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;

  if ( (NULL == extra) ||
       (0 == extra_len) )
  {
    GNUNET_break (0);
    return;
  }
  if ('\0' != url[extra_len - 1])
  {
    GNUNET_break (0);
    return;
  }
  if (0 != strcmp (url,
                   exchange_url))
    return; /* not relevant for us */

  {
    enum GNUNET_DB_QueryStatus qs;
    struct GNUNET_TIME_Absolute earliest_retry;

    if (NULL != keys)
    {
      TALER_EXCHANGE_keys_decref (keys);
      keys = NULL;
    }
    qs = db_plugin->select_exchange_keys (db_plugin->cls,
                                          exchange_url,
                                          &earliest_retry,
                                          &keys);
    if (qs < 0)
    {
      GNUNET_break (0);
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "No keys yet for `%s'\n",
                  exchange_url);
    }
  }
  if (NULL == keys)
  {
    if (NULL != task)
    {
      GNUNET_SCHEDULER_cancel (task);
      task = NULL;
    }
  }
  else
  {
    if (NULL == task)
      task = GNUNET_SCHEDULER_add_now (&select_work,
                                       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;

  cfg = c;
  if (NULL != cfgfile)
    cfg_filename = GNUNET_strdup (cfgfile);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Running with configuration %s\n",
              cfgfile);
  GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
                                 NULL);
  if (NULL == exchange_url)
  {
    GNUNET_CONFIGURATION_iterate_sections (c,
                                           &cfg_iter_cb,
                                           NULL);
    if (NULL == c_head)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "No exchanges found in configuration\n");
      global_ret = EXIT_NOTCONFIGURED;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    return;
  }

  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_NO_RESTART;
    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_NO_RESTART;
    return;
  }
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = htons (sizeof (es)),
      .type = htons (TALER_DBEVENT_MERCHANT_NEW_WIRE_DEADLINE)
    };

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

    keys_eh = db_plugin->event_listen (db_plugin->cls,
                                       &es,
                                       GNUNET_TIME_UNIT_FOREVER_REL,
                                       &update_exchange_keys,
                                       NULL);
  }

  update_exchange_keys (NULL,
                        exchange_url,
                        strlen (exchange_url) + 1);
}


/**
 * The main function of the taler-merchant-depositcheck
 *
 * @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_string ('e',
                                 "exchange",
                                 "BASE_URL",
                                 "limit us to checking deposits of this exchange",
                                 &exchange_url),
    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-depositcheck",
    gettext_noop (
      "background process that checks with the exchange on deposits that are past the wire deadline"),
    options,
    &run, NULL);
  if (GNUNET_SYSERR == ret)
    return EXIT_INVALIDARGUMENT;
  if (GNUNET_NO == ret)
    return EXIT_SUCCESS;
  return global_ret;
}


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