/*
  This file is part of GNU Taler
  (C) 2021-2024 Taler Systems SA

  GNU 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.

  GNU Taler is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public
  License along with TALER; see the file COPYING.  If not,
  see <http://www.gnu.org/licenses/>
*/

/**
 * @file taler-merchant-httpd_private-get-instances-ID-kyc.c
 * @brief implementing GET /instances/$ID/kyc request handling
 * @author Christian Grothoff
 */
#include "platform.h"
#include "taler-merchant-httpd_private-get-instances-ID-kyc.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_exchanges.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_dbevents.h>
#include <regex.h>

/**
 * Information we keep per /kyc request.
 */
struct KycContext;


/**
 * Structure for tracking requests to the exchange's
 * ``/kyc-check`` API.
 */
struct ExchangeKycRequest
{
  /**
   * Kept in a DLL.
   */
  struct ExchangeKycRequest *next;

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

  /**
   * Find operation where we connect to the respective exchange.
   */
  struct TMH_EXCHANGES_KeysOperation *fo;

  /**
   * JSON array of payto-URIs with KYC auth wire transfer
   * instructions.  Provided if @e auth_ok is false and
   * @e kyc_auth_conflict is false.
   */
  json_t *pkaa;

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

  /**
   * KYC request this exchange request is made for.
   */
  struct KycContext *kc;

  /**
   * JSON array of AccountLimits that apply, NULL if
   * unknown (and likely defaults apply).
   */
  json_t *jlimits;

  /**
   * Our account's payto URI.
   */
  struct TALER_FullPayto payto_uri;

  /**
   * Base URL of the exchange.
   */
  char *exchange_url;

  /**
   * Hash of the wire account (with salt) we are checking.
   */
  struct TALER_MerchantWireHashP h_wire;

  /**
   * Current access token for the KYC SPA. Only set
   * if @e auth_ok is true.
   */
  struct TALER_AccountAccessTokenP access_token;

  /**
   * Timestamp when we last got a reply from the exchange.
   */
  struct GNUNET_TIME_Timestamp last_check;

  /**
   * Last HTTP status code obtained via /kyc-check from
   * the exchange.
   */
  unsigned int last_http_status;

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

  /**
   * True if this account
   * cannot work at this exchange because KYC auth is
   * impossible.
   */
  bool kyc_auth_conflict;

  /**
   * We could not get /keys from the exchange.
   */
  bool no_keys;

  /**
   * True if @e access_token is available.
   */
  bool auth_ok;

  /**
   * True if we believe no KYC is currently required
   * for this account at this exchange.
   */
  bool kyc_ok;

  /**
   * True if the exchange exposed to us that the account
   * is currently under AML review.
   */
  bool in_aml_review;


};


/**
 * Information we keep per /kyc request.
 */
struct KycContext
{
  /**
   * Stored in a DLL.
   */
  struct KycContext *next;

  /**
   * Stored in a DLL.
   */
  struct KycContext *prev;

  /**
   * Connection we are handling.
   */
  struct MHD_Connection *connection;

  /**
   * Instance we are serving.
   */
  struct TMH_MerchantInstance *mi;

  /**
   * Our handler context.
   */
  struct TMH_HandlerContext *hc;

  /**
   * Response to return, NULL if we don't have one yet.
   */
  struct MHD_Response *response;

  /**
   * JSON array where we are building up the array with
   * pending KYC operations.
   */
  json_t *kycs_data;

  /**
   * Head of DLL of requests we are making to an
   * exchange to inquire about the latest KYC status.
   */
  struct ExchangeKycRequest *exchange_pending_head;

  /**
   * Tail of DLL of requests we are making to an
   * exchange to inquire about the latest KYC status.
   */
  struct ExchangeKycRequest *exchange_pending_tail;

  /**
   * Set to the exchange URL, or NULL to not filter by
   * exchange.
   */
  const char *exchange_url;

  /**
   * Notification handler from database on changes
   * to the KYC status.
   */
  struct GNUNET_DB_EventHandler *eh;

  /**
   * Set to the h_wire of the merchant account if
   * @a have_h_wire is true, used to filter by account.
   */
  struct TALER_MerchantWireHashP h_wire;

  /**
   * How long are we willing to wait for the exchange(s)?
   */
  struct GNUNET_TIME_Absolute timeout;

  /**
   * HTTP status code to use for the reply, i.e 200 for "OK".
   * Special value UINT_MAX is used to indicate hard errors
   * (no reply, return #MHD_NO).
   */
  unsigned int response_code;

  /**
   * #GNUNET_NO if the @e connection was not suspended,
   * #GNUNET_YES if the @e connection was suspended,
   * #GNUNET_SYSERR if @e connection was resumed to as
   * part of #MH_force_pc_resume during shutdown.
   */
  enum GNUNET_GenericReturnValue suspended;

  /**
   * What state are we long-polling for?
   */
  enum TALER_EXCHANGE_KycLongPollTarget lpt;

  /**
   * True if @e h_wire was given.
   */
  bool have_h_wire;

  /**
   * We're still waiting on the exchange to determine
   * the KYC status of our deposit(s).
   */
  bool return_immediately;

};


/**
 * Head of DLL.
 */
static struct KycContext *kc_head;

/**
 * Tail of DLL.
 */
static struct KycContext *kc_tail;


void
TMH_force_kyc_resume ()
{
  for (struct KycContext *kc = kc_head;
       NULL != kc;
       kc = kc->next)
  {
    if (GNUNET_YES == kc->suspended)
    {
      kc->suspended = GNUNET_SYSERR;
      MHD_resume_connection (kc->connection);
    }
  }
}


/**
 * Custom cleanup routine for a `struct KycContext`.
 *
 * @param cls the `struct KycContext` to clean up.
 */
static void
kyc_context_cleanup (void *cls)
{
  struct KycContext *kc = cls;
  struct ExchangeKycRequest *ekr;

  while (NULL != (ekr = kc->exchange_pending_head))
  {
    GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head,
                                 kc->exchange_pending_tail,
                                 ekr);
    if (NULL != ekr->fo)
    {
      TMH_EXCHANGES_keys4exchange_cancel (ekr->fo);
      ekr->fo = NULL;
    }
    json_decref (ekr->pkaa);
    json_decref (ekr->jlimits);
    if (NULL != ekr->keys)
      TALER_EXCHANGE_keys_decref (ekr->keys);
    GNUNET_free (ekr->exchange_url);
    GNUNET_free (ekr->payto_uri.full_payto);
    GNUNET_free (ekr);
  }
  if (NULL != kc->eh)
  {
    TMH_db->event_listen_cancel (kc->eh);
    kc->eh = NULL;
  }
  if (NULL != kc->response)
  {
    MHD_destroy_response (kc->response);
    kc->response = NULL;
  }
  GNUNET_CONTAINER_DLL_remove (kc_head,
                               kc_tail,
                               kc);
  json_decref (kc->kycs_data);
  GNUNET_free (kc);
}


/**
 * Resume the given KYC context and send the final response.  Stores the
 * response in the @a kc and signals MHD to resume the connection.  Also
 * ensures MHD runs immediately.
 *
 * @param kc KYC context
 */
static void
resume_kyc_with_response (struct KycContext *kc)
{
  kc->response_code = MHD_HTTP_OK;
  kc->response = TALER_MHD_MAKE_JSON_PACK (
    GNUNET_JSON_pack_array_incref ("kyc_data",
                                   kc->kycs_data));
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Resuming /kyc handling as exchange interaction is done (%u)\n",
              MHD_HTTP_OK);
  if (GNUNET_YES == kc->suspended)
  {
    kc->suspended = GNUNET_NO;
    MHD_resume_connection (kc->connection);
    TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
  }
}


/**
 * Handle a DB event about an update relevant
 * for the processing of the kyc request.
 *
 * @param cls our `struct KycContext`
 * @param extra additional event data provided
 * @param extra_size number of bytes in @a extra
 */
static void
kyc_change_cb (void *cls,
               const void *extra,
               size_t extra_size)
{
  struct KycContext *kc = cls;

  if (GNUNET_YES == kc->suspended)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Resuming KYC with gateway timeout\n");
    kc->suspended = GNUNET_NO;
    MHD_resume_connection (kc->connection);
    TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
  }
}


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


/**
 * Return JSON array with AccountLimit objects giving
 * the current limits for this exchange.
 *
 * @param[in,out] ekr overall request context
 */
static json_t *
get_exchange_limits (
  struct ExchangeKycRequest *ekr)
{
  const struct TALER_EXCHANGE_Keys *keys = ekr->keys;
  json_t *limits;

  if (NULL != ekr->jlimits)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Returning custom KYC limits\n");
    return json_incref (ekr->jlimits);
  }
  if (NULL == keys)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "No keys, thus no default KYC limits known\n");
    return NULL;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Returning default KYC limits (%u/%u)\n",
              keys->hard_limits_length,
              keys->zero_limits_length);
  limits = json_array ();
  GNUNET_assert (NULL != limits);
  for (unsigned int i = 0; i<keys->hard_limits_length; i++)
  {
    const struct TALER_EXCHANGE_AccountLimit *limit
      = &keys->hard_limits[i];

    pack_limit (limit,
                limits);
  }
  for (unsigned int i = 0; i<keys->zero_limits_length; i++)
  {
    const struct TALER_EXCHANGE_ZeroLimitedOperation *zlimit
      = &keys->zero_limits[i];
    json_t *jl;
    struct TALER_Amount zero;

    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_set_zero (keys->currency,
                                          &zero));
    jl = GNUNET_JSON_PACK (
      TALER_JSON_pack_kycte ("operation_type",
                             zlimit->operation_type),
      GNUNET_JSON_pack_time_rel ("timeframe",
                                 GNUNET_TIME_UNIT_ZERO),
      TALER_JSON_pack_amount ("threshold",
                              &zero),
      GNUNET_JSON_pack_bool ("soft_limit",
                             true)
      );
    GNUNET_assert (0 ==
                   json_array_append_new (limits,
                                          jl));
  }
  return limits;
}


/**
 * Maps @a ekr to a status code for clients to interpret the
 * overall result.
 *
 * @param ekr request summary
 * @return status of the KYC state as a string
 */
static const char *
map_to_status (const struct ExchangeKycRequest *ekr)
{
  if (ekr->no_keys)
  {
    return "no-exchange-keys";
  }
  if (ekr->kyc_ok)
  {
    if (NULL != ekr->jlimits)
    {
      size_t off;
      json_t *limit;
      json_array_foreach (ekr->jlimits, off, limit)
      {
        struct TALER_Amount threshold;
        enum TALER_KYCLOGIC_KycTriggerEvent operation_type;
        bool soft = false;
        struct GNUNET_JSON_Specification spec[] = {
          TALER_JSON_spec_kycte ("operation_type",
                                 &operation_type),
          TALER_JSON_spec_amount_any ("threshold",
                                      &threshold),
          GNUNET_JSON_spec_mark_optional (
            GNUNET_JSON_spec_bool ("soft_limit",
                                   &soft),
            NULL),
          GNUNET_JSON_spec_end ()
        };

        if (GNUNET_OK !=
            GNUNET_JSON_parse (limit,
                               spec,
                               NULL, NULL))
        {
          GNUNET_break (0);
          return "merchant-internal-error";
        }
        if (! TALER_amount_is_zero (&threshold))
          continue; /* only care about zero-limits */
        if (! soft)
          continue; /* only care about soft limits */
        if ( (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) ||
             (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE) ||
             (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION) )
        {
          if (! ekr->auth_ok)
          {
            if (ekr->kyc_auth_conflict)
              return "kyc-wire-impossible";
            return "kyc-wire-required";
          }
          return "kyc-required";
        }
      }
    }
    if (NULL == ekr->jlimits)
    {
      /* check default limits */
      const struct TALER_EXCHANGE_Keys *keys = ekr->keys;

      for (unsigned int i = 0; i < keys->zero_limits_length; i++)
      {
        enum TALER_KYCLOGIC_KycTriggerEvent operation_type
          = keys->zero_limits[i].operation_type;

        if ( (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT) ||
             (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_AGGREGATE) ||
             (operation_type == TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION) )
        {
          if (! ekr->auth_ok)
          {
            if (ekr->kyc_auth_conflict)
              return "kyc-wire-impossible";
            return "kyc-wire-required";
          }
          return "kyc-required";
        }
      }
    }
    return "ready";
  }
  if (! ekr->auth_ok)
  {
    if (ekr->kyc_auth_conflict)
      return "kyc-wire-impossible";
    return "kyc-wire-required";
  }
  if (ekr->in_aml_review)
    return "awaiting-aml-review";
  switch (ekr->last_http_status)
  {
  case 0:
    return "exchange-unreachable";
  case MHD_HTTP_OK:
    /* then we should have kyc_ok */
    GNUNET_break (0);
    return NULL;
  case MHD_HTTP_ACCEPTED:
    /* Then KYC is really what  is needed */
    return "kyc-required";
  case MHD_HTTP_NO_CONTENT:
    /* then we should have had kyc_ok! */
    GNUNET_break (0);
    return NULL;
  case MHD_HTTP_FORBIDDEN:
    /* then we should have had ! auth_ok */
    GNUNET_break (0);
    return NULL;
  case MHD_HTTP_NOT_FOUND:
    /* then we should have had ! auth_ok */
    GNUNET_break (0);
    return NULL;
  case MHD_HTTP_CONFLICT:
    /* then we should have had ! auth_ok */
    GNUNET_break (0);
    return NULL;
  case MHD_HTTP_INTERNAL_SERVER_ERROR:
    return "exchange-internal-error";
  case MHD_HTTP_GATEWAY_TIMEOUT:
    return "exchange-gateway-timeout";
  default:
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Exchange responded with unexpected HTTP status %u to /kyc-check request!\n",
                ekr->last_http_status);
    break;
  }
  return "exchange-status-invalid";
}


/**
 * Take data from @a ekr to expand our response.
 *
 * @param ekr exchange we are done inspecting
 */
static void
ekr_expand_response (struct ExchangeKycRequest *ekr)
{
  const char *status;

  status = map_to_status (ekr);
  if (NULL == status)
  {
    GNUNET_break (0);
    status = "logic-bug";
  }
  GNUNET_assert (
    0 ==
    json_array_append_new (
      ekr->kc->kycs_data,
      GNUNET_JSON_PACK (
        TALER_JSON_pack_full_payto (
          "payto_uri",
          ekr->payto_uri),
        GNUNET_JSON_pack_data_auto (
          "h_wire",
          &ekr->h_wire),
        GNUNET_JSON_pack_string (
          "status",
          status),
        GNUNET_JSON_pack_string (
          "exchange_url",
          ekr->exchange_url),
        GNUNET_JSON_pack_bool ("no_keys",
                               ekr->no_keys),
        GNUNET_JSON_pack_bool ("auth_conflict",
                               ekr->kyc_auth_conflict),
        GNUNET_JSON_pack_uint64 ("exchange_http_status",
                                 ekr->last_http_status),
        (TALER_EC_NONE == ekr->last_ec)
        ? GNUNET_JSON_pack_allow_null (
          GNUNET_JSON_pack_string (
            "dummy",
            NULL))
        : GNUNET_JSON_pack_uint64 ("exchange_code",
                                   ekr->last_ec),
        ekr->auth_ok
        ? GNUNET_JSON_pack_data_auto (
          "access_token",
          &ekr->access_token)
        : GNUNET_JSON_pack_allow_null (
          GNUNET_JSON_pack_string (
            "dummy",
            NULL)),
        GNUNET_JSON_pack_allow_null (
          GNUNET_JSON_pack_array_steal (
            "limits",
            get_exchange_limits (ekr))),
        GNUNET_JSON_pack_allow_null (
          GNUNET_JSON_pack_array_incref ("payto_kycauths",
                                         ekr->pkaa))
        )));
}


/**
 * We are done with asynchronous processing, generate the
 * response for the @e kc.
 *
 * @param[in,out] kc KYC context to respond for
 */
static void
kc_respond (struct KycContext *kc)
{
  if ( (! kc->return_immediately) &&
       (! GNUNET_TIME_absolute_is_past (kc->timeout)) )
  {
    if (GNUNET_NO == kc->suspended)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Suspending: long poll target %d not reached\n",
                  kc->lpt);
      MHD_suspend_connection (kc->connection);
      kc->suspended = GNUNET_YES;
    }
    else
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Remaining suspended: long poll target %d not reached\n",
                  kc->lpt);
    }
    return;
  }
  /* All exchange requests done, create final
     big response from cumulated replies */
  resume_kyc_with_response (kc);
}


/**
 * We are done with the KYC request @a ekr.  Remove it from the work list and
 * check if we are done overall.
 *
 * @param[in] ekr key request that is done (and will be freed)
 */
static void
ekr_finished (struct ExchangeKycRequest *ekr)
{
  struct KycContext *kc = ekr->kc;

  ekr_expand_response (ekr);
  GNUNET_CONTAINER_DLL_remove (kc->exchange_pending_head,
                               kc->exchange_pending_tail,
                               ekr);
  json_decref (ekr->jlimits);
  json_decref (ekr->pkaa);
  if (NULL != ekr->keys)
    TALER_EXCHANGE_keys_decref (ekr->keys);
  GNUNET_free (ekr->exchange_url);
  GNUNET_free (ekr->payto_uri.full_payto);
  GNUNET_free (ekr);

  if (NULL != kc->exchange_pending_head)
    return; /* wait for more */
  kc_respond (kc);
}


/**
 * Figure out which exchange accounts from @a keys could
 * be used for a KYC auth wire transfer from the account
 * that @a ekr is checking. Will set the "pkaa" array
 * in @a ekr.
 *
 * @param[in,out] ekr request we are processing
 */
static void
determine_eligible_accounts (
  struct ExchangeKycRequest *ekr)
{
  struct KycContext *kc = ekr->kc;
  const struct TALER_EXCHANGE_Keys *keys = ekr->keys;
  struct TALER_Amount kyc_amount;
  char *merchant_pub_str;
  struct TALER_NormalizedPayto np;

  ekr->pkaa = json_array ();
  GNUNET_assert (NULL != ekr->pkaa);
  {
    const struct TALER_EXCHANGE_GlobalFee *gf;

    gf = TALER_EXCHANGE_get_global_fee (keys,
                                        GNUNET_TIME_timestamp_get ());
    if (NULL == gf)
    {
      GNUNET_assert (GNUNET_OK ==
                     TALER_amount_set_zero (keys->currency,
                                            &kyc_amount));
    }
    else
    {
      /* FIXME-#9427: history fee should be globally renamed to KYC fee... */
      kyc_amount = gf->fees.history;
    }
  }

  merchant_pub_str
    = GNUNET_STRINGS_data_to_string_alloc (
        &kc->mi->merchant_pub,
        sizeof (kc->mi->merchant_pub));
  /* For all accounts of the exchange */
  np = TALER_payto_normalize (ekr->payto_uri);
  for (unsigned int i = 0; i<keys->accounts_len; i++)
  {
    const struct TALER_EXCHANGE_WireAccount *account
      = &keys->accounts[i];

    /* KYC auth transfers are never supported with conversion */
    if (NULL != account->conversion_url)
      continue;
    /* filter by source account by credit_restrictions */
    if (GNUNET_YES !=
        TALER_EXCHANGE_test_account_allowed (account,
                                             true, /* credit */
                                             np))
      continue;
    /* exchange account is allowed, add it */
    {
      const char *exchange_account_payto
        = account->fpayto_uri.full_payto;
      char *payto_kycauth;

      if (TALER_amount_is_zero (&kyc_amount))
        GNUNET_asprintf (&payto_kycauth,
                         "%s%cmessage=KYC:%s",
                         exchange_account_payto,
                         (NULL == strchr (exchange_account_payto,
                                          '?'))
                         ? '?'
                         : '&',
                         merchant_pub_str);
      else
        GNUNET_asprintf (&payto_kycauth,
                         "%s%camount=%s&message=KYC:%s",
                         exchange_account_payto,
                         (NULL == strchr (exchange_account_payto,
                                          '?'))
                         ? '?'
                         : '&',
                         TALER_amount2s (&kyc_amount),
                         merchant_pub_str);
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Found account %s where KYC auth is possible\n",
                  payto_kycauth);
      GNUNET_assert (0 ==
                     json_array_append_new (ekr->pkaa,
                                            json_string (payto_kycauth)));
      GNUNET_free (payto_kycauth);
    }
  }
  GNUNET_free (np.normalized_payto);
  GNUNET_free (merchant_pub_str);
}


/**
 * Function called with the result of a #TMH_EXCHANGES_keys4exchange()
 * operation.  Runs the KYC check against the exchange.
 *
 * @param cls closure with our `struct ExchangeKycRequest *`
 * @param keys keys of the exchange context
 * @param exchange representation of the exchange
 */
static void
kyc_with_exchange (void *cls,
                   struct TALER_EXCHANGE_Keys *keys,
                   struct TMH_Exchange *exchange)
{
  struct ExchangeKycRequest *ekr = cls;

  (void) exchange;
  ekr->fo = NULL;
  if (NULL == keys)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Failed to download `%skeys`\n",
                ekr->exchange_url);
    ekr->no_keys = true;
    ekr_finished (ekr);
    return;
  }
  ekr->keys = TALER_EXCHANGE_keys_incref (keys);
  if (! ekr->auth_ok)
  {
    determine_eligible_accounts (ekr);
    if (0 == json_array_size (ekr->pkaa))
    {
      /* No KYC auth wire transfers are possible to this exchange from
         our merchant bank account, so we cannot use this account with
         this exchange if it has any KYC requirements! */
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "KYC auth to `%s' impossible for merchant account `%s'\n",
                  ekr->exchange_url,
                  ekr->payto_uri.full_payto);
      ekr->kyc_auth_conflict = true;
    }
  }
  ekr_finished (ekr);
}


/**
 * Closure for add_unreachable_status().
 */
struct UnreachableContext
{
  /**
   * Where we are building the response.
   */
  struct KycContext *kc;

  /**
   * Pointer to our account hash.
   */
  const struct TALER_MerchantWireHashP *h_wire;

  /**
   * Bank account for which we have no status from any exchange.
   */
  struct TALER_FullPayto payto_uri;

};

/**
 * Add all trusted exchanges with "unknown" status for the
 * bank account given in the context.
 *
 * @param cls a `struct UnreachableContext`
 * @param url base URL of the exchange
 * @param exchange internal handle for the exchange
 */
static void
add_unreachable_status (void *cls,
                        const char *url,
                        const struct TMH_Exchange *exchange)
{
  struct UnreachableContext *uc = cls;
  struct KycContext *kc = uc->kc;

  GNUNET_assert (
    0 ==
    json_array_append_new (
      kc->kycs_data,
      GNUNET_JSON_PACK (
        TALER_JSON_pack_full_payto (
          "payto_uri",
          uc->payto_uri),
        GNUNET_JSON_pack_data_auto (
          "h_wire",
          uc->h_wire),
        GNUNET_JSON_pack_string (
          "status",
          "exchange-unreachable"),
        GNUNET_JSON_pack_string (
          "exchange_url",
          url),
        GNUNET_JSON_pack_bool ("no_keys",
                               true),
        GNUNET_JSON_pack_bool ("auth_conflict",
                               false),
        GNUNET_JSON_pack_uint64 ("exchange_http_status",
                                 0)
        )));

}


/**
 * Function called from account_kyc_get_status() with KYC status information
 * for this merchant.
 *
 * @param cls our `struct KycContext *`
 * @param h_wire hash of the wire account
 * @param payto_uri payto:// URI of the merchant's bank account
 * @param exchange_url base URL of the exchange for which this is a status
 * @param last_check when did we last get an update on our KYC status from the exchange
 * @param kyc_ok true if we satisfied the KYC requirements
 * @param access_token access token for the KYC SPA, NULL if we cannot access it yet (need KYC auth wire transfer)
 * @param last_http_status last HTTP status from /kyc-check
 * @param last_ec last Taler error code from /kyc-check
 * @param in_aml_review true if the account is pending review
 * @param jlimits JSON array of applicable AccountLimits, or NULL if unknown (like defaults apply)
 */
static void
kyc_status_cb (
  void *cls,
  const struct TALER_MerchantWireHashP *h_wire,
  struct TALER_FullPayto payto_uri,
  const char *exchange_url,
  struct GNUNET_TIME_Timestamp last_check,
  bool kyc_ok,
  const struct TALER_AccountAccessTokenP *access_token,
  unsigned int last_http_status,
  enum TALER_ErrorCode last_ec,
  bool in_aml_review,
  const json_t *jlimits)
{
  struct KycContext *kc = cls;
  struct ExchangeKycRequest *ekr;

  if (NULL == exchange_url)
  {
    struct UnreachableContext uc = {
      .kc = kc,
      .h_wire = h_wire,
      .payto_uri = payto_uri
    };

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Account has unknown KYC status for all exchanges.\n");
    TMH_exchange_get_trusted (&add_unreachable_status,
                              &uc);
    kc_respond (kc);
    return;
  }
  if (! TMH_EXCHANGES_check_trusted (exchange_url))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Skipping exchange `%s': not trusted\n",
                exchange_url);
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "KYC status for `%s' at `%s' is %u/%s/%s/%s\n",
              payto_uri.full_payto,
              exchange_url,
              last_http_status,
              kyc_ok ? "KYC OK" : "KYC NEEDED",
              in_aml_review ? "IN AML REVIEW" : "NO AML REVIEW",
              NULL == jlimits ? "DEFAULT LIMITS" : "CUSTOM LIMITS");
  switch (kc->lpt)
  {
  case TALER_EXCHANGE_KLPT_NONE:
    break;
  case TALER_EXCHANGE_KLPT_KYC_AUTH_TRANSFER:
    if (NULL != access_token)
      kc->return_immediately = true;
    break;
  case TALER_EXCHANGE_KLPT_INVESTIGATION_DONE:
    if (! in_aml_review)
      kc->return_immediately = true;
    break;
  case TALER_EXCHANGE_KLPT_KYC_OK:
    if (kyc_ok)
      kc->return_immediately = true;
    break;
  }
  ekr = GNUNET_new (struct ExchangeKycRequest);
  GNUNET_CONTAINER_DLL_insert (kc->exchange_pending_head,
                               kc->exchange_pending_tail,
                               ekr);
  ekr->last_http_status = last_http_status;
  ekr->last_ec = last_ec;
  if (NULL != jlimits)
    ekr->jlimits = json_incref ((json_t *) jlimits);
  ekr->h_wire = *h_wire;
  ekr->exchange_url = GNUNET_strdup (exchange_url);
  ekr->payto_uri.full_payto
    = GNUNET_strdup (payto_uri.full_payto);
  ekr->last_check = last_check;
  ekr->kyc_ok = kyc_ok;
  ekr->kc = kc;
  ekr->in_aml_review = in_aml_review;
  ekr->auth_ok = (NULL != access_token);
  if ( (! ekr->auth_ok) ||
       (NULL == ekr->jlimits) )
  {
    /* Figure out wire transfer instructions */
    if (GNUNET_NO == kc->suspended)
    {
      MHD_suspend_connection (kc->connection);
      kc->suspended = GNUNET_YES;
    }
    ekr->fo = TMH_EXCHANGES_keys4exchange (
      exchange_url,
      false,
      &kyc_with_exchange,
      ekr);
    if (NULL == ekr->fo)
    {
      GNUNET_break (0);
      ekr_finished (ekr);
      return;
    }
    return;
  }
  ekr->access_token = *access_token;
  ekr_finished (ekr);
}


/**
 * Check the KYC status of an instance.
 *
 * @param mi instance to check KYC status of
 * @param connection the MHD connection to handle
 * @param[in,out] hc context with further information about the request
 * @return MHD result code
 */
static MHD_RESULT
get_instances_ID_kyc (
  struct TMH_MerchantInstance *mi,
  struct MHD_Connection *connection,
  struct TMH_HandlerContext *hc)
{
  struct KycContext *kc = hc->ctx;

  if (NULL == kc)
  {
    kc = GNUNET_new (struct KycContext);
    kc->mi = mi;
    hc->ctx = kc;
    hc->cc = &kyc_context_cleanup;
    GNUNET_CONTAINER_DLL_insert (kc_head,
                                 kc_tail,
                                 kc);
    kc->connection = connection;
    kc->hc = hc;
    kc->kycs_data = json_array ();
    GNUNET_assert (NULL != kc->kycs_data);
    TALER_MHD_parse_request_timeout (connection,
                                     &kc->timeout);
    {
      uint64_t num = 0;
      int val;

      TALER_MHD_parse_request_number (connection,
                                      "lpt",
                                      &num);
      val = (int) num;
      if ( (val < 0) ||
           (val > TALER_EXCHANGE_KLPT_MAX) )
      {
        /* Protocol violation, but we can be graceful and
           just ignore the long polling! */
        GNUNET_break_op (0);
        val = TALER_EXCHANGE_KLPT_NONE;
      }
      kc->lpt = (enum TALER_EXCHANGE_KycLongPollTarget) val;
    }
    kc->return_immediately
      = (TALER_EXCHANGE_KLPT_NONE == kc->lpt);
    /* process 'exchange_url' argument */
    kc->exchange_url = MHD_lookup_connection_value (
      connection,
      MHD_GET_ARGUMENT_KIND,
      "exchange_url");
    if ( (NULL != kc->exchange_url) &&
         ( (! TALER_url_valid_charset (kc->exchange_url)) ||
           (! TALER_is_web_url (kc->exchange_url)) ) )
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_BAD_REQUEST,
        TALER_EC_GENERIC_PARAMETER_MALFORMED,
        "exchange_url must be a valid HTTP(s) URL");
    }

    TALER_MHD_parse_request_arg_auto (connection,
                                      "h_wire",
                                      &kc->h_wire,
                                      kc->have_h_wire);

    if ( (TALER_EXCHANGE_KLPT_NONE != kc->lpt) &&
         (! GNUNET_TIME_absolute_is_past (kc->timeout)) )
    {
      if (kc->have_h_wire)
      {
        struct TALER_MERCHANTDB_MerchantKycStatusChangeEventP ev = {
          .header.size = htons (sizeof (ev)),
          .header.type = htons (
            TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_STATUS_CHANGED
            ),
          .h_wire = kc->h_wire
        };

        kc->eh = TMH_db->event_listen (
          TMH_db->cls,
          &ev.header,
          GNUNET_TIME_absolute_get_remaining (kc->timeout),
          &kyc_change_cb,
          kc);
      }
      else
      {
        struct GNUNET_DB_EventHeaderP hdr = {
          .size = htons (sizeof (hdr)),
          .type = htons (TALER_DBEVENT_MERCHANT_KYC_STATUS_CHANGED)
        };

        kc->eh = TMH_db->event_listen (
          TMH_db->cls,
          &hdr,
          GNUNET_TIME_absolute_get_remaining (kc->timeout),
          &kyc_change_cb,
          kc);
      }
    } /* end register LISTEN hooks */
  } /* end 1st time initialization */

  if (GNUNET_SYSERR == kc->suspended)
    return MHD_NO; /* during shutdown, we don't generate any more replies */
  GNUNET_assert (GNUNET_NO == kc->suspended);

  if (NULL != kc->response)
    return MHD_queue_response (connection,
                               kc->response_code,
                               kc->response);

  /* Check our database */
  {
    enum GNUNET_DB_QueryStatus qs;

    GNUNET_break (0 ==
                  json_array_clear (kc->kycs_data));
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Checking KYC status for %s (%d/%s)\n",
                mi->settings.id,
                kc->have_h_wire,
                kc->exchange_url);
    qs = TMH_db->account_kyc_get_status (
      TMH_db->cls,
      mi->settings.id,
      kc->have_h_wire
      ? &kc->h_wire
      : NULL,
      kc->exchange_url,
      &kyc_status_cb,
      kc);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "account_kyc_get_status returned %d records\n",
                (int) qs);
    if (qs < 0)
    {
      /* Database error */
      GNUNET_break (0);
      if (GNUNET_YES == kc->suspended)
      {
        /* must have suspended before DB error, resume! */
        MHD_resume_connection (connection);
        kc->suspended = GNUNET_NO;
      }
      return TALER_MHD_reply_with_ec (
        connection,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "account_kyc_get_status");
    }
    if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    {
      /* no matching accounts, could not have suspended */
      GNUNET_assert (GNUNET_NO == kc->suspended);
      return TALER_MHD_reply_static (connection,
                                     MHD_HTTP_NO_CONTENT,
                                     NULL,
                                     NULL,
                                     0);
    }
  }
  if (GNUNET_YES == kc->suspended)
    return MHD_YES;
  /* Should have generated a response */
  GNUNET_break (NULL != kc->response);
  return MHD_queue_response (connection,
                             kc->response_code,
                             kc->response);
}


MHD_RESULT
TMH_private_get_instances_ID_kyc (
  const struct TMH_RequestHandler *rh,
  struct MHD_Connection *connection,
  struct TMH_HandlerContext *hc)
{
  struct TMH_MerchantInstance *mi = hc->instance;

  (void) rh;
  return get_instances_ID_kyc (mi,
                               connection,
                               hc);
}


MHD_RESULT
TMH_private_get_instances_default_ID_kyc (
  const struct TMH_RequestHandler *rh,
  struct MHD_Connection *connection,
  struct TMH_HandlerContext *hc)
{
  struct TMH_MerchantInstance *mi;

  (void) rh;
  mi = TMH_lookup_instance (hc->infix);
  if (NULL == mi)
  {
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_NOT_FOUND,
      TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN,
      hc->infix);
  }
  return get_instances_ID_kyc (mi,
                               connection,
                               hc);
}


/* end of taler-merchant-httpd_private-get-instances-ID-kyc.c */
