/*
  This file is part of TALER
  Copyright (C) 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-kyccheck.c
 * @brief Process that check the KYC status of our bank accounts at all exchanges
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include "microhttpd.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#include <pthread.h>
#include <regex.h>
#include <taler/taler_dbevents.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_exchange_service.h>
#include "taler/taler_merchant_util.h"
#include "taler/taler_merchant_bank_lib.h"
#include "taler/taler_merchantdb_lib.h"
#include "taler/taler_merchantdb_plugin.h"

/**
 * Timeout for the exchange interaction.  Rather long as we should do
 * long-polling and do not want to wake up too often.
 */
#define EXCHANGE_TIMEOUT GNUNET_TIME_relative_multiply ( \
          GNUNET_TIME_UNIT_MINUTES, \
          30)

/**
 * How long do we wait between requests if all we wait
 * for is a change in the AML investigation status?
 * Default value.
 */
#define AML_FREQ GNUNET_TIME_relative_multiply ( \
          GNUNET_TIME_UNIT_HOURS, \
          6)

/**
 * How long do we wait between requests if all we wait
 * for is a change in the AML investigation status?
 */
static struct GNUNET_TIME_Relative aml_freq;

/**
 * How frequently do we check for updates to our KYC status
 * if there is no actual reason to check? Set to a very low
 * frequency, just to ensure we eventually notice.
 * Default value.
 */
#define AML_LOW_FREQ GNUNET_TIME_relative_multiply ( \
          GNUNET_TIME_UNIT_DAYS, \
          7)

/**
 * How frequently do we check for updates to our KYC status
 * if there is no actual reason to check? Set to a very low
 * frequency, just to ensure we eventually notice.
 */
static struct GNUNET_TIME_Relative aml_low_freq;


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


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

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

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

};


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

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

  /**
   * Head of inquiries for this account.
   */
  struct Inquiry *i_head;

  /**
   * Tail of inquiries for this account.
   */
  struct Inquiry *i_tail;

  /**
   * Merchant instance this account belongs to.
   */
  char *instance_id;

  /**
   * The payto-URI of this account.
   */
  struct TALER_FullPayto merchant_account_uri;

  /**
   * Wire hash of the merchant bank account (with the
   * respective salt).
   */
  struct TALER_MerchantWireHashP h_wire;

  /**
   * Private key of the instance.
   */
  union TALER_AccountPrivateKeyP ap;

  /**
   * Hash of the @e merchant_account_uri.
   */
  struct TALER_NormalizedPaytoHashP h_payto;

  /**
   * Database generation when this account
   * was last active.
   */
  uint64_t account_gen;

};


/**
 * Information about an inquiry job.
 */
struct Inquiry
{
  /**
   * Kept in a DLL.
   */
  struct Inquiry *next;

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

  /**
   * Main task for this inquiry.
   */
  struct GNUNET_SCHEDULER_Task *task;

  /**
   * Which exchange is this inquiry about.
   */
  struct Exchange *e;

  /**
   * Which account is this inquiry about.
   */
  struct Account *a;

  /**
   * AccountLimits that apply to the account, NULL
   * if unknown.
   */
  json_t *jlimits;

  /**
   * Handle for the actual HTTP request to the exchange.
   */
  struct TALER_EXCHANGE_GetKycCheckHandle *kyc;

  /**
   * Access token for the /kyc-info API.
   */
  struct TALER_AccountAccessTokenP access_token;

  /**
   * Last time we called the /kyc-check endpoint.
   */
  struct GNUNET_TIME_Timestamp last_kyc_check;

  /**
   * When is the next KYC check due?
   */
  struct GNUNET_TIME_Absolute due;

  /**
   * When should the current KYC time out?
   */
  struct GNUNET_TIME_Absolute timeout;

  /**
   * Current exponential backoff.
   */
  struct GNUNET_TIME_Relative backoff;

  /**
   * Rule generation known to the client, 0 for none.
   * Corresponds to the decision row in the exchange.
   */
  uint64_t rule_gen;

  /**
   * Last HTTP status returned by the exchange from
   * the /kyc-check endpoint.
   */
  unsigned int last_http_status;

  /**
   * Last Taler error code returned by the exchange from
   * the /kyc-check endpoint.
   */
  enum TALER_ErrorCode last_ec;

  /**
   * True if this is not our first time we make this request.
   */
  bool not_first_time;

  /**
   * Do soft limits on transactions apply to this merchant for operations
   * merchants care about? If so, we should increase our request frequency
   * and ask more often to see if they were lifted.
   */
  bool zero_limited;

  /**
   * Did we not run this inquiry due to limits?
   */
  bool limited;

  /**
   * Do we believe this account's KYC is in good shape?
   */
  bool kyc_ok;

  /**
   * True if merchant did perform this account's KYC AUTH transfer and @e access_token is set.
   */
  bool auth_ok;

  /**
   * True if the account is known to be currently under
   * investigation by AML staff.
   */
  bool aml_review;

};


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

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

/**
 * Head of accounts.
 */
static struct Account *a_head;

/**
 * Tail of accounts.
 */
static struct Account *a_tail;

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

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

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

/**
 * Event handler to learn that there may be new bank
 * accounts to check.
 */
static struct GNUNET_DB_EventHandler *eh_accounts;

/**
 * Event handler to learn that there may be new exchange
 * keys to check.
 */
static struct GNUNET_DB_EventHandler *eh_keys;

/**
 * Event handler to learn that there was a KYC
 * rule triggered and we need to check the KYC
 * status for an account.
 */
static struct GNUNET_DB_EventHandler *eh_rule;

/**
 * Event handler to learn that higher-frequency KYC
 * checks were forced by an application actively inspecting
 * some KYC status values.
 */
static struct GNUNET_DB_EventHandler *eh_update_forced;

/**
 * Event handler to learn that we got new /keys
 * from an exchange and should reconsider eligibility.
 */
static struct GNUNET_DB_EventHandler *keys_rule;

/**
 * Main task to discover (new) accounts.
 */
static struct GNUNET_SCHEDULER_Task *account_task;

/**
 * Counter determining how often we have called
 * "select_accounts" on the database.
 */
static uint64_t database_gen;

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


/**
 * Check about performing a /kyc-check request with the
 * exchange for the given inquiry.
 *
 * @param cls a `struct Inquiry` to process
 */
static void
inquiry_work (void *cls);


/**
 * An inquiry finished, check if we should resume others.
 */
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 Account *a = a_head;
         NULL != a;
         a = a->next)
    {
      for (struct Inquiry *i = a->i_head;
           NULL != i;
           i = i->next)
      {
        if (! i->limited)
          continue;
        GNUNET_assert (NULL == i->task);
        /* done synchronously so that the active_inquiries
           is updated immediately */
        inquiry_work (i);
        if (at_limit)
          break;
      }
      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;
  }
}


/**
 * Pack the given @a limit into the JSON @a limits array.
 *
 * @param limit account limit to pack
 * @param[in,out] limits JSON array to extend
 */
static void
pack_limit (const struct TALER_EXCHANGE_AccountLimit *limit,
            json_t *limits)
{
  json_t *jl;

  jl = GNUNET_JSON_PACK (
    TALER_JSON_pack_kycte ("operation_type",
                           limit->operation_type),
    GNUNET_JSON_pack_time_rel ("timeframe",
                               limit->timeframe),
    TALER_JSON_pack_amount ("threshold",
                            &limit->threshold),
    GNUNET_JSON_pack_bool ("soft_limit",
                           limit->soft_limit)
    );
  GNUNET_assert (0 ==
                 json_array_append_new (limits,
                                        jl));
}


/**
 * Update KYC status for @a i based on
 * @a account_kyc_status
 *
 * @param[in,out] i inquiry context, jlimits is updated
 * @param account_kyc_status account KYC status details
 */
static void
store_kyc_status (
  struct Inquiry *i,
  const struct TALER_EXCHANGE_AccountKycStatus *account_kyc_status)
{
  json_t *jlimits;

  json_decref (i->jlimits);
  jlimits = json_array ();
  GNUNET_assert (NULL != jlimits);
  i->zero_limited = false;
  for (unsigned int j = 0; j<account_kyc_status->limits_length; j++)
  {
    const struct TALER_EXCHANGE_AccountLimit *limit
      = &account_kyc_status->limits[j];

    pack_limit (limit,
                jlimits);
    if (TALER_amount_is_zero (&limit->threshold) &&
        limit->soft_limit &&
        ( (TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT == limit->operation_type) ||
          (TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE == limit->operation_type) ||
          (TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION == limit->operation_type) ) )
    {
      i->zero_limited = true;
    }
  }
  i->jlimits = jlimits;
  GNUNET_break (! GNUNET_is_zero (&account_kyc_status->access_token));
  i->access_token = account_kyc_status->access_token;
  i->auth_ok = true;
  i->aml_review = account_kyc_status->aml_review;
  i->kyc_ok = (MHD_HTTP_OK == i->last_http_status);
}


/**
 * Function called with the result of a KYC check.
 *
 * @param cls a `struct Inquiry *`
 * @param ks the account's KYC status details
 */
static void
exchange_check_cb (
  void *cls,
  const struct TALER_EXCHANGE_GetKycCheckResponse *ks)
{
  struct Inquiry *i = cls;
  bool progress = false;

  i->kyc = NULL;
  if (! i->not_first_time)
    progress = true;
  if (i->last_http_status != ks->hr.http_status)
    progress = true;
  i->last_http_status = ks->hr.http_status;
  i->last_ec = ks->hr.ec;
  i->rule_gen = 0;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "KYC status of `%s' at `%s' is %u\n",
              i->a->merchant_account_uri.full_payto,
              i->e->keys->exchange_url,
              ks->hr.http_status);
  switch (ks->hr.http_status)
  {
  case MHD_HTTP_OK:
    if (! i->kyc_ok)
      progress = true;
    if (i->rule_gen != ks->details.ok.rule_gen)
      progress = true;
    i->rule_gen = ks->details.ok.rule_gen;
    i->last_kyc_check = GNUNET_TIME_timestamp_get ();
    /* exchange says KYC is OK, gives status information */
    store_kyc_status (i,
                      &ks->details.ok);
    i->backoff = GNUNET_TIME_UNIT_MINUTES;
    if (i->aml_review || i->zero_limited)
    {
      if (! progress)
        i->due = GNUNET_TIME_relative_to_absolute (
          GNUNET_TIME_randomize (GNUNET_TIME_relative_max (aml_freq,
                                                           i->backoff)));
    }
    else
    {
      /* KYC is OK, only check again if triggered */
      if (! progress)
        i->due = GNUNET_TIME_relative_to_absolute (
          GNUNET_TIME_randomize (GNUNET_TIME_relative_max (aml_low_freq,
                                                           i->backoff)));
    }
    break;
  case MHD_HTTP_ACCEPTED:
    progress = ! i->auth_ok;
    if (i->rule_gen != ks->details.accepted.rule_gen)
      progress = true;
    i->rule_gen = ks->details.accepted.rule_gen;
    i->last_kyc_check = GNUNET_TIME_timestamp_get ();

    /* exchange says KYC is required */
    store_kyc_status (i,
                      &ks->details.accepted);
    i->backoff = GNUNET_TIME_UNIT_MINUTES;
    /* Start immediately with long-polling */
    if (! progress)
      i->due = GNUNET_TIME_absolute_max (i->last_kyc_check.abs_time,
                                         i->timeout);
    break;
  case MHD_HTTP_NO_CONTENT:
    i->last_kyc_check = GNUNET_TIME_timestamp_get ();
    i->backoff = GNUNET_TIME_UNIT_MINUTES;
    /* exchange claims KYC is off! */
    i->kyc_ok = true;
    i->aml_review = false;
    /* Clear limits, in case exchange had KYC on previously */
    json_decref (i->jlimits);
    i->jlimits = NULL;
    /* KYC is OK, only check again if triggered */
    i->due = GNUNET_TIME_relative_to_absolute (
      GNUNET_TIME_randomize (GNUNET_TIME_relative_max (aml_low_freq,
                                                       i->backoff)));
    break;
  case MHD_HTTP_FORBIDDEN: /* bad signature */
    i->last_kyc_check = GNUNET_TIME_timestamp_get ();
    /* Forbidden => KYC auth must be wrong */
    i->auth_ok = false;
    /* Start with long-polling */
    if (! progress)
      i->due = GNUNET_TIME_absolute_max (i->last_kyc_check.abs_time,
                                         i->timeout);
    i->backoff = GNUNET_TIME_UNIT_MINUTES;
    break;
  case MHD_HTTP_NOT_FOUND: /* account unknown */
    i->last_kyc_check = GNUNET_TIME_timestamp_get ();
    /* Account unknown => no KYC auth yet */
    i->auth_ok = false;
    /* unknown account => wire transfer required! */
    i->kyc_ok = false;
    /* There should not be any limits yet, but clear them
       just in case the exchange has amnesia */
    json_decref (i->jlimits);
    i->jlimits = NULL;
    /* Start immediately with Long-polling */
    if (! progress)
      i->due = GNUNET_TIME_absolute_max (i->last_kyc_check.abs_time,
                                         i->timeout);
    i->backoff = GNUNET_TIME_UNIT_MINUTES;
    break;
  case MHD_HTTP_CONFLICT: /* no account_pub known */
    i->last_kyc_check = GNUNET_TIME_timestamp_get ();
    /* Conflict => KYC auth wire transfer missing! */
    i->auth_ok = false;
    /* Start immediately with Long-polling */
    if (! progress)
      i->due = GNUNET_TIME_absolute_max (i->last_kyc_check.abs_time,
                                         i->timeout);
    i->backoff = GNUNET_TIME_UNIT_MINUTES;
    break;
  default:
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Exchange responded with HTTP status %u (%d) to /kyc-check request!\n",
                ks->hr.http_status,
                ks->hr.ec);
    i->backoff
      = GNUNET_TIME_randomized_backoff (i->backoff,
                                        EXCHANGE_TIMEOUT);
    i->last_kyc_check = GNUNET_TIME_timestamp_get ();
    i->due = GNUNET_TIME_relative_to_absolute (i->backoff);
    i->auth_ok = false;
    break;
  }

  {
    enum GNUNET_DB_QueryStatus qs;

    qs = db_plugin->account_kyc_set_status (
      db_plugin->cls,
      i->a->instance_id,
      &i->a->h_wire,
      i->e->keys->exchange_url,
      i->last_kyc_check,
      i->due,
      i->backoff,
      i->last_http_status,
      i->last_ec,
      i->rule_gen,
      (i->auth_ok)
      ? &i->access_token
      : NULL,
      i->jlimits,
      i->aml_review,
      i->kyc_ok);
    if (qs < 0)
    {
      GNUNET_break (0);
      global_ret = EXIT_FAILURE;
      GNUNET_SCHEDULER_shutdown ();
      return;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "account_kyc_set_status (%s, %u, %s, %s) returned %d\n",
                i->e->keys->exchange_url,
                i->last_http_status,
                i->auth_ok ? "auth OK" : "auth needed",
                NULL == i->jlimits ? "default limits" : "custom limits",
                (int) qs);
    i->not_first_time = true;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Will repeat inquiry in %s\n",
              GNUNET_TIME_relative2s (
                GNUNET_TIME_absolute_get_remaining (i->due),
                true));
  if (! GNUNET_TIME_absolute_is_never (i->due))
    i->task = GNUNET_SCHEDULER_add_at (i->due,
                                       &inquiry_work,
                                       i);
  end_inquiry ();
}


static void
inquiry_work (void *cls)
{
  struct Inquiry *i = cls;
  enum TALER_EXCHANGE_KycLongPollTarget lpt;

  i->task = NULL;
  if (! GNUNET_TIME_absolute_is_past (i->due))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Will start inquiry on %s for %s in %s\n",
                i->a->merchant_account_uri.full_payto,
                i->e->keys->exchange_url,
                GNUNET_TIME_relative2s (
                  GNUNET_TIME_absolute_get_remaining (i->due),
                  true));
    i->task
      = GNUNET_SCHEDULER_add_at (i->due,
                                 &inquiry_work,
                                 i);
    goto finish;
  }

  GNUNET_assert (OPEN_INQUIRY_LIMIT >= active_inquiries);
  if (OPEN_INQUIRY_LIMIT <= active_inquiries)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Not looking for work: at limit\n");
    i->limited = true;
    at_limit = true;
    return;
  }
  at_limit = false;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Starting KYC status of `%s' at `%s'\n",
              i->a->merchant_account_uri.full_payto,
              i->e->keys->exchange_url);
  i->timeout
    = GNUNET_TIME_relative_to_absolute (EXCHANGE_TIMEOUT);
  lpt = TALER_EXCHANGE_KLPT_NONE;
  if (! i->auth_ok)
    lpt = TALER_EXCHANGE_KLPT_KYC_AUTH_TRANSFER;
  else if (! i->kyc_ok)
    lpt = TALER_EXCHANGE_KLPT_KYC_OK;
  else if (i->aml_review)
    lpt = TALER_EXCHANGE_KLPT_INVESTIGATION_DONE;
  if (! i->not_first_time)
    lpt = TALER_EXCHANGE_KLPT_NONE;
  i->kyc = TALER_EXCHANGE_get_kyc_check_create (
    ctx,
    i->e->keys->exchange_url,
    &i->a->h_payto,
    &i->a->ap);
  if (NULL == i->kyc)
  {
    GNUNET_break (0);
    i->due = i->timeout;
    i->task
      = GNUNET_SCHEDULER_add_at (i->due,
                                 &inquiry_work,
                                 i);
    goto finish;
  }
  GNUNET_assert (GNUNET_OK ==
                 TALER_EXCHANGE_get_kyc_check_set_options (
                   i->kyc,
                   TALER_EXCHANGE_get_kyc_check_option_known_rule_gen (
                     i->rule_gen),
                   TALER_EXCHANGE_get_kyc_check_option_lpt (lpt),
                   TALER_EXCHANGE_get_kyc_check_option_timeout (
                     i->not_first_time && (! test_mode)
                     ? EXCHANGE_TIMEOUT
                     : GNUNET_TIME_UNIT_ZERO)));
  GNUNET_assert (TALER_EC_NONE ==
                 TALER_EXCHANGE_get_kyc_check_start (i->kyc,
                                                     &exchange_check_cb,
                                                     i));
  active_inquiries++;
finish:
  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;
  }
}


/**
 * Check if the account @a could work with exchange that
 * has keys @a keys.
 *
 * @param keys the keys of an exchange
 * @param a an account
 */
static bool
is_eligible (const struct TALER_EXCHANGE_Keys *keys,
             const struct Account *a)
{
  struct TALER_NormalizedPayto np;
  bool ret;

  np = TALER_payto_normalize (a->merchant_account_uri);
  ret = TALER_EXCHANGE_keys_test_account_allowed (keys,
                                                  true,
                                                  np);
  GNUNET_free (np.normalized_payto);
  return ret;
}


/**
 * Start the KYC checking for account @a at exchange @a e.
 *
 * @param e an exchange
 * @param a an account
 */
static void
start_inquiry (struct Exchange *e,
               struct Account *a)
{
  struct Inquiry *i;
  enum GNUNET_DB_QueryStatus qs;

  i = GNUNET_new (struct Inquiry);
  i->e = e;
  i->a = a;
  GNUNET_CONTAINER_DLL_insert (a->i_head,
                               a->i_tail,
                               i);
  qs = db_plugin->get_kyc_status (db_plugin->cls,
                                  a->merchant_account_uri,
                                  a->instance_id,
                                  e->keys->exchange_url,
                                  &i->auth_ok,
                                  &i->access_token,
                                  &i->kyc_ok,
                                  &i->last_http_status,
                                  &i->last_ec,
                                  &i->rule_gen,
                                  &i->last_kyc_check,
                                  &i->due,
                                  &i->backoff,
                                  &i->aml_review,
                                  &i->jlimits);
  if (qs < 0)
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (qs > 0)
    i->not_first_time = true;
  if (GNUNET_YES == test_mode)
    i->due = GNUNET_TIME_UNIT_ZERO_ABS; /* immediately */
  inquiry_work (i);
}


/**
 * Stop KYC inquiry @a i.
 *
 * @param[in] i the inquiry to stop
 */
static void
stop_inquiry (struct Inquiry *i)
{
  struct Account *a = i->a;

  GNUNET_CONTAINER_DLL_remove (a->i_head,
                               a->i_tail,
                               i);
  if (NULL != i->task)
  {
    GNUNET_SCHEDULER_cancel (i->task);
    i->task = NULL;
  }
  if (NULL != i->kyc)
  {
    TALER_EXCHANGE_get_kyc_check_cancel (i->kyc);
    i->kyc = NULL;
  }
  if (NULL != i->jlimits)
  {
    json_decref (i->jlimits);
    i->jlimits = NULL;
  }
  GNUNET_free (i);
}


/**
 * Stop KYC inquiry for account @a at exchange @a e.
 *
 * @param e an exchange
 * @param a an account
 */
static void
stop_inquiry_at (struct Exchange *e,
                 struct Account *a)
{
  for (struct Inquiry *i = a->i_head;
       NULL != i;
       i = i->next)
  {
    if (e == i->e)
    {
      stop_inquiry (i);
      return;
    }
  }
  /* strange, there should have been a match! */
  GNUNET_break (0);
}


/**
 * Set the account @a h_wire of @a instance_id to be ineligible
 * for the exchange at @a exchange_url and thus no need to do KYC checks.
 *
 * @param instance_id instance that has the account
 * @param exchange_url base URL of the exchange
 * @param h_wire hash of the merchant bank account that is ineligible
 */
static void
flag_ineligible (const char *instance_id,
                 const char *exchange_url,
                 const struct TALER_MerchantWireHashP *h_wire)
{
  enum GNUNET_DB_QueryStatus qs;

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Account %s not eligible at exchange %s\n",
              TALER_B2S (h_wire),
              exchange_url);
  qs = db_plugin->account_kyc_set_status (
    db_plugin->cls,
    instance_id,
    h_wire,
    exchange_url,
    GNUNET_TIME_timestamp_get (),
    GNUNET_TIME_UNIT_FOREVER_ABS,
    GNUNET_TIME_UNIT_FOREVER_REL,
    0,
    TALER_EC_MERCHANT_PRIVATE_ACCOUNT_NOT_ELIGIBLE_FOR_EXCHANGE,
    0,
    NULL,
    NULL,
    false,
    false);
  if (qs < 0)
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "account_kyc_set_status (%s) returned %d\n",
              exchange_url,
              (int) qs);
}


/**
 * Start inquries for all exchanges on account @a a.
 *
 * @param a an account
 */
static void
start_inquiries (struct Account *a)
{
  for (struct Exchange *e = e_head;
       NULL != e;
       e = e->next)
  {
    if (is_eligible (e->keys,
                     a))
    {
      start_inquiry (e,
                     a);
    }
    else
    {
      flag_ineligible (a->instance_id,
                       e->keys->exchange_url,
                       &a->h_wire);
    }
  }
}


/**
 * Stop all inquries involving account @a a.
 *
 * @param a an account
 */
static void
stop_inquiries (struct Account *a)
{
  struct Inquiry *i;

  while (NULL != (i = a->i_head))
    stop_inquiry (i);
}


/**
 * Callback invoked with information about a bank account.
 *
 * @param cls closure
 * @param merchant_priv private key of the merchant instance
 * @param ad details about the account
 */
static void
account_cb (
  void *cls,
  const struct TALER_MerchantPrivateKeyP *merchant_priv,
  const struct TALER_MERCHANTDB_AccountDetails *ad)
{
  struct TALER_FullPayto payto_uri = ad->payto_uri;

  if (! ad->active)
    return;
  if (NULL == merchant_priv)
    return; /* instance was deleted */
  for (struct Account *a = a_head;
       NULL != a;
       a = a->next)
  {
    if (0 ==
        TALER_full_payto_cmp (payto_uri,
                              a->merchant_account_uri))
    {
      a->account_gen = database_gen;
      return;
    }
  }
  {
    struct Account *a = GNUNET_new (struct Account);

    a->account_gen = database_gen;
    a->merchant_account_uri.full_payto
      = GNUNET_strdup (ad->payto_uri.full_payto);
    a->instance_id
      = GNUNET_strdup (ad->instance_id);
    a->h_wire
      = ad->h_wire;
    a->ap.merchant_priv
      = *merchant_priv;
    TALER_full_payto_normalize_and_hash (a->merchant_account_uri,
                                         &a->h_payto);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Found account %s of instance %s with H_PAYTO %s\n",
                ad->payto_uri.full_payto,
                ad->instance_id,
                GNUNET_sh2s (&a->h_payto.hash));
    GNUNET_CONTAINER_DLL_insert (a_head,
                                 a_tail,
                                 a);
    start_inquiries (a);
  }
}


/**
 * The set of bank accounts has changed, update our
 * list of active inquiries.
 *
 * @param cls unused
 */
static void
find_accounts (void *cls)
{
  enum GNUNET_DB_QueryStatus qs;

  (void) cls;
  account_task = NULL;
  database_gen++;
  qs = db_plugin->select_accounts (db_plugin->cls,
                                   NULL, /* all instances */
                                   &account_cb,
                                   NULL);
  if (qs < 0)
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  for (struct Account *a = a_head;
       NULL != a;
       a = a->next)
  {
    if (a->account_gen < database_gen)
      stop_inquiries (a);
  }
  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;
  }
}


/**
 * Function called when transfers are added to the merchant database.  We look
 * for more work.
 *
 * @param cls closure (NULL)
 * @param extra additional event data provided
 * @param extra_size number of bytes in @a extra
 */
static void
account_changed (void *cls,
                 const void *extra,
                 size_t extra_size)
{
  (void) cls;
  (void) extra;
  (void) extra_size;
  if (NULL != account_task)
    return;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Received account change notification: reloading accounts\n");
  account_task
    = GNUNET_SCHEDULER_add_now (&find_accounts,
                                NULL);
}


/**
 * Interact with the database to get the current set
 * of exchange keys known to us.
 *
 * @param exchange_url the exchange URL to check
 */
static void
find_keys (const char *exchange_url)
{
  enum GNUNET_DB_QueryStatus qs;
  struct TALER_EXCHANGE_Keys *keys;
  struct Exchange *e;
  struct GNUNET_TIME_Absolute first_retry;

  qs = db_plugin->select_exchange_keys (db_plugin->cls,
                                        exchange_url,
                                        &first_retry,
                                        &keys);
  if (qs < 0)
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if ( (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ||
       (NULL == keys) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "No %s/keys yet!\n",
                exchange_url);
    return;
  }
  for (e = e_head; NULL != e; e = e->next)
  {
    if (0 == strcmp (e->keys->exchange_url,
                     keys->exchange_url))
    {
      struct TALER_EXCHANGE_Keys *old_keys = e->keys;

      e->keys = keys;
      for (struct Account *a = a_head;
           NULL != a;
           a = a->next)
      {
        bool was_eligible = is_eligible (old_keys,
                                         a);
        bool now_eligible = is_eligible (keys,
                                         a);

        if (was_eligible == now_eligible)
          continue; /* no change, do nothing */
        if (was_eligible)
          stop_inquiry_at (e,
                           a);
        else /* is_eligible */
          start_inquiry (e,
                         a);
      }
      TALER_EXCHANGE_keys_decref (old_keys);
      return;
    }
  }
  e = GNUNET_new (struct Exchange);
  e->keys = keys;
  GNUNET_CONTAINER_DLL_insert (e_head,
                               e_tail,
                               e);
  for (struct Account *a = a_head;
       NULL != a;
       a = a->next)
  {
    if ( (a->account_gen == database_gen) &&
         (is_eligible (e->keys,
                       a)) )
      start_inquiry (e,
                     a);
  }
}


/**
 * Function called when keys were changed in the
 * merchant database. Updates ours.
 *
 * @param cls closure (NULL)
 * @param extra additional event data provided
 * @param extra_size number of bytes in @a extra
 */
static void
keys_changed (void *cls,
              const void *extra,
              size_t extra_size)
{
  const char *url = extra;

  (void) cls;
  if ( (NULL == extra) ||
       (0 == extra_size) )
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if ('\0' != url[extra_size - 1])
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Received keys change notification: reload `%s'\n",
              url);
  find_keys (url);
}


/**
 * Function called when a KYC rule was triggered by
 * a transaction and we need to get the latest KYC
 * status immediately.
 *
 * @param cls closure (NULL)
 * @param extra additional event data provided
 * @param extra_size number of bytes in @a extra
 */
static void
rule_triggered (void *cls,
                const void *extra,
                size_t extra_size)
{
  const char *text = extra;
  const char *space;
  struct TALER_MerchantWireHashP h_wire;
  const char *exchange_url;

  (void) cls;
  if ( (NULL == extra) ||
       (0 == extra_size) )
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if ('\0' != text[extra_size - 1])
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  space = memchr (extra,
                  ' ',
                  extra_size);
  if (NULL == space)
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  if (GNUNET_OK !=
      GNUNET_STRINGS_string_to_data (extra,
                                     space - text,
                                     &h_wire,
                                     sizeof (h_wire)))
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
  exchange_url = &space[1];
  if (! TALER_is_web_url (exchange_url))
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  for (struct Account *a = a_head;
       NULL != a;
       a = a->next)
  {
    if (0 !=
        GNUNET_memcmp (&h_wire,
                       &a->h_wire))
      continue;
    for (struct Inquiry *i = a->i_head;
         NULL != i;
         i = i->next)
    {
      if (0 != strcmp (exchange_url,
                       i->e->keys->exchange_url))
        continue;
      i->kyc_ok = false;
      i->due = GNUNET_TIME_UNIT_ZERO_ABS;
      if (NULL != i->task)
      {
        GNUNET_SCHEDULER_cancel (i->task);
        i->task = NULL;
      }
      if (NULL != i->kyc)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "/kyc-check already running for %s\n",
                    text);
        return;
      }
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Starting %skyc-check for `%s' due to KYC rule trigger\n",
                  exchange_url,
                  i->a->merchant_account_uri.full_payto);
      i->task = GNUNET_SCHEDULER_add_at (i->due,
                                         &inquiry_work,
                                         i);
      return;
    }
  }
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "KYC rule trigger notification `%s' matches none of our accounts\n",
              text);
}


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

  (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;
  }
  find_keys (url);
  GNUNET_free (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;

    if (NULL != e->keys)
    {
      TALER_EXCHANGE_keys_decref (e->keys);
      e->keys = NULL;
    }
    GNUNET_CONTAINER_DLL_remove (e_head,
                                 e_tail,
                                 e);
    GNUNET_free (e);
  }
  while (NULL != a_head)
  {
    struct Account *a = a_head;

    stop_inquiries (a);
    GNUNET_CONTAINER_DLL_remove (a_head,
                                 a_tail,
                                 a);
    GNUNET_free (a->merchant_account_uri.full_payto);
    GNUNET_free (a->instance_id);
    GNUNET_free (a);
  }
  if (NULL != eh_accounts)
  {
    db_plugin->event_listen_cancel (eh_accounts);
    eh_accounts = NULL;
  }
  if (NULL != account_task)
  {
    GNUNET_SCHEDULER_cancel (account_task);
    account_task = NULL;
  }
  if (NULL != eh_keys)
  {
    db_plugin->event_listen_cancel (eh_keys);
    eh_keys = NULL;
  }
  if (NULL != eh_rule)
  {
    db_plugin->event_listen_cancel (eh_rule);
    eh_rule = NULL;
  }
  if (NULL != eh_update_forced)
  {
    db_plugin->event_listen_cancel (eh_update_forced);
    eh_update_forced = NULL;
  }
  if (NULL != keys_rule)
  {
    db_plugin->event_listen_cancel (keys_rule);
    keys_rule = 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;
  }
}


/**
 * Function called when we urgently need to re-check the KYC status
 * of some account. Finds the respective inquiry and re-launches
 * the check, unless we are already doing it.
 *
 * @param cls NULL
 * @param instance_id instance for which to force the check
 * @param exchange_url base URL of the exchange to check
 * @param h_wire hash of the wire account to check KYC status for
 */
static void
force_check_now (void *cls,
                 const char *instance_id,
                 const char *exchange_url,
                 const struct TALER_MerchantWireHashP *h_wire)
{
  for (struct Account *a = a_head;
       NULL != a;
       a = a->next)
  {
    if (0 !=
        strcmp (instance_id,
                a->instance_id))
      continue;
    if (0 !=
        GNUNET_memcmp (h_wire,
                       &a->h_wire))
      continue;
    for (struct Inquiry *i = a->i_head;
         NULL != i;
         i = i->next)
    {
      if (0 !=
          strcmp (i->e->keys->exchange_url,
                  exchange_url))
        continue;
      /* If we are not actively checking with the exchange, do start
         to check immediately */
      if (NULL != i->kyc)
      {
        i->due = GNUNET_TIME_absolute_get (); /* now! */
        if (NULL != i->task)
          GNUNET_SCHEDULER_cancel (i->task);
        i->task = GNUNET_SCHEDULER_add_at (i->due,
                                           &inquiry_work,
                                           i);
      }
      return;
    }
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "No inquiry at `%s' for exchange `%s' and h_wire `%s'. Likely the account is not eligible.\n",
              instance_id,
              exchange_url,
              TALER_B2S (h_wire));
  /* In this case, set the due date back to FOREVER */
  flag_ineligible (instance_id,
                   exchange_url,
                   h_wire);
}


/**
 * Function called when a KYC status update was forced by an
 * application checking the KYC status of an account.
 *
 * @param cls closure (NULL)
 * @param extra additional event data provided
 * @param extra_size number of bytes in @a extra
 */
static void
update_forced (void *cls,
               const void *extra,
               size_t extra_size)
{
  enum GNUNET_DB_QueryStatus qs;

  (void) cls;
  (void) extra;
  (void) extra_size;
  qs = db_plugin->account_kyc_get_outdated (
    db_plugin->cls,
    &force_check_now,
    NULL);
  if (qs < 0)
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return;
  }
}


/**
 * 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;
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "merchant-kyccheck",
                                           "AML_FREQ",
                                           &aml_freq))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
                               "merchant-kyccheck",
                               "AML_FREQ");
    /* use default */
    aml_freq = AML_FREQ;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "merchant-kyccheck",
                                           "AML_LOW_FREQ",
                                           &aml_low_freq))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
                               "merchant-kyccheck",
                               "AML_LOW_FREQ");
    /* use default */
    aml_low_freq = AML_LOW_FREQ;
  }
  if (GNUNET_TIME_relative_cmp (aml_low_freq,
                                <,
                                aml_freq))
  {
    aml_low_freq = GNUNET_TIME_relative_multiply (aml_freq,
                                                  10);
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "AML_LOW_FREQ was set to less than AML_FREQ. Using %s instead\n",
                GNUNET_TIME_relative2s (aml_low_freq,
                                        true));
  }
  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 = htons (sizeof (es)),
      .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KEYS)
    };

    eh_keys
      = db_plugin->event_listen (db_plugin->cls,
                                 &es,
                                 GNUNET_TIME_UNIT_FOREVER_REL,
                                 &keys_changed,
                                 NULL);
  }
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = htons (sizeof (es)),
      .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_UPDATE_FORCED)
    };

    eh_update_forced
      = db_plugin->event_listen (db_plugin->cls,
                                 &es,
                                 GNUNET_TIME_UNIT_FOREVER_REL,
                                 &update_forced,
                                 NULL);
  }
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = htons (sizeof (es)),
      .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED)
    };

    eh_rule
      = db_plugin->event_listen (db_plugin->cls,
                                 &es,
                                 GNUNET_TIME_UNIT_FOREVER_REL,
                                 &rule_triggered,
                                 NULL);
  }
  GNUNET_CONFIGURATION_iterate_sections (cfg,
                                         &accept_exchanges,
                                         NULL);
  fprintf (stderr, "NOW\n");
  {
    struct GNUNET_DB_EventHeaderP es = {
      .size = htons (sizeof (es)),
      .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
    };

    eh_accounts
      = db_plugin->event_listen (db_plugin->cls,
                                 &es,
                                 GNUNET_TIME_UNIT_FOREVER_REL,
                                 &account_changed,
                                 NULL);
  }
  GNUNET_assert (NULL == account_task);
  account_task
    = GNUNET_SCHEDULER_add_now (&find_accounts,
                                NULL);
}


/**
 * The main function of taler-merchant-kyccheck
 *
 * @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-kyccheck",
    gettext_noop (
      "background process that checks the KYC state of our bank accounts at various exchanges"),
    options,
    &run, NULL);
  if (GNUNET_SYSERR == ret)
    return EXIT_INVALIDARGUMENT;
  if (GNUNET_NO == ret)
    return EXIT_SUCCESS;
  return global_ret;
}


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