/*
  This file is part of Anastasis
  Copyright (C) 2020, 2021, 2022 Anastasis SARL

  Anastasis is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  Anastasis 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
  Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file reducer/anastasis_api_redux.c
 * @brief anastasis reducer api
 * @author Christian Grothoff
 * @author Dominik Meister
 * @author Dennis Neufeld
 */
#include <platform.h>
#include <jansson.h>
#include "anastasis_redux.h"
#include "anastasis_error_codes.h"
#include <taler/taler_json_lib.h>
#include "anastasis_api_redux.h"
#include "anastasis_api_redux_state.h"
#include "validation.h"


/**
 * How long do we wait at most for a /config reply from an Anastasis provider.
 * 60s is very generous, given the tiny bandwidth required, even for the most
 * remote locations.
 */
#define CONFIG_GENERIC_TIMEOUT GNUNET_TIME_UNIT_MINUTES

/**
 * How long do we wait in a more "synchronous"
 * scenaro for a /config reply from an Anastasis provider.
 */
#define CONFIG_FAST_TIMEOUT GNUNET_TIME_UNIT_SECONDS


#define GENERATE_STRING(STRING) #STRING,
static const char *generic_strings[] = {
  ANASTASIS_GENERIC_STATES (GENERATE_STRING)
};
#undef GENERATE_STRING


/**
 * #ANASTASIS_REDUX_add_provider_to_state_ waiting for the
 * configuration request to complete or fail.
 */
struct ConfigReduxWaiting
{
  /**
   * Kept in a DLL.
   */
  struct ConfigReduxWaiting *prev;

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

  /**
   * Associated redux action.
   */
  struct ANASTASIS_ReduxAction ra;

  /**
   * Config request we are waiting for.
   */
  struct ConfigRequest *cr;

  /**
   * State we are processing.  Borrowed, not owned: the operation that
   * started us keeps the state alive until it has cancelled us.  See
   * #ANASTASIS_REDUX_add_provider_to_state_.
   */
  struct ANASTASIS_ReduxState *rs;

  /**
   * Function to call with updated @e rs.
   */
  ANASTASIS_REDUX_StateCallback cb;

  /**
   * Closure for @e cb.
   */
  void *cb_cls;

};


/**
 * Anastasis authorization method configuration
 */
struct AuthorizationMethodConfig
{
  /**
   * Type of the method, i.e. "question".
   */
  char *type;

  /**
   * Fee charged for accessing key share using this method, in the
   * provider's primary currency.
   */
  struct TALER_Amount usage_fee;

  /**
   * Fee charged for accessing key share using this method, per currency.
   */
  struct TALER_AmountList usage_fees;
};


/**
 * State for a "get config" operation.
 */
struct ConfigRequest
{

  /**
   * Kept in a DLL, given that we may have multiple backends.
   */
  struct ConfigRequest *next;

  /**
   * Kept in a DLL, given that we may have multiple backends.
   */
  struct ConfigRequest *prev;

  /**
   * Head of DLL of REDUX operations waiting for an answer.
   */
  struct ConfigReduxWaiting *w_head;

  /**
   * Tail of DLL of REDUX operations waiting for an answer.
   */
  struct ConfigReduxWaiting *w_tail;

  /**
   * When did we start?
   */
  struct GNUNET_TIME_Absolute start_time;

  /**
   * When do we time out?
   */
  struct GNUNET_TIME_Absolute timeout_at;

  /**
   * How long do we wait before trying again?
   */
  struct GNUNET_TIME_Relative backoff;

  /**
   * Obtained status code.
   */
  unsigned int http_status;

  /**
   * The /config GET operation handle.
   */
  struct ANASTASIS_ConfigOperation *co;

  /**
   * URL of the anastasis backend.
   */
  char *url;

  /**
   * Business name of the anastasis backend.
   */
  char *business_name;

  /**
   * Array of authorization methods supported by the server.
   */
  struct AuthorizationMethodConfig *methods;

  /**
   * Length of the @e methods array.
   */
  unsigned int methods_length;

  /**
   * Maximum size of an upload in megabytes.
   */
  uint32_t storage_limit_in_megabytes;

  /**
   * Annual fee for an account / policy upload, primary currency.
   */
  struct TALER_Amount annual_fee;

  /**
   * Annual fee for an account / policy upload, per currency.
   */
  struct TALER_AmountList annual_fees;

  /**
   * Fee for a truth upload, primary currency.
   */
  struct TALER_Amount truth_upload_fee;

  /**
   * Fee for a truth upload, per currency.
   */
  struct TALER_AmountList truth_upload_fees;

  /**
   * Maximum legal liability for data loss covered by the
   * provider, primary currency.
   */
  struct TALER_Amount liability_limit;

  /**
   * Maximum legal liability for data loss covered by the
   * provider, per currency.
   */
  struct TALER_AmountList liability_limits;

  /**
   * Currencies the provider accepts, primary currency first.
   */
  char **currencies;

  /**
   * Length of the @e currencies array.
   */
  unsigned int currencies_len;

  /**
   * Provider salt.
   */
  struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;

  /**
   * Task to timeout /config requests.
   */
  struct GNUNET_SCHEDULER_Task *tt;

  /**
   * Status of the /config request.
   */
  enum TALER_ErrorCode ec;
};


/**
 * Reducer API's CURL context handle.
 */
struct GNUNET_CURL_Context *ANASTASIS_REDUX_ctx_;

/**
 * JSON containing country specific identity attributes to ask the user for.
 */
static json_t *redux_id_attr;

/**
 * Head of DLL of Anastasis backend configuration requests.
 */
static struct ConfigRequest *cr_head;

/**
 * Tail of DLL of Anastasis backend configuration requests.
 */
static struct ConfigRequest *cr_tail;

/**
 * JSON containing country specific information.
 */
static json_t *redux_countries;

/**
 * List of Anastasis providers.
 */
static json_t *provider_list;

/**
 * External reducer binary or NULL
 * to use internal reducer.
 */
static char *external_reducer_binary;


const char *
ANASTASIS_REDUX_probe_external_reducer (void)
{
  if (NULL != external_reducer_binary)
    return external_reducer_binary;
  external_reducer_binary = getenv ("ANASTASIS_EXTERNAL_REDUCER");
  if (NULL != external_reducer_binary)
    unsetenv ("ANASTASIS_EXTERNAL_REDUCER");

  return external_reducer_binary;

}


/**
 * Extract the generic part of the step @a rs is at.
 *
 * The generic states are the common prefix of the backup and the
 * recovery state enumerations (see #ANASTASIS_GENERIC_STATES), so a
 * value that is in range for #ANASTASIS_GenericState means the same
 * thing in either variant.
 *
 * @param rs the state to inspect
 * @return #ANASTASIS_GENERIC_STATE_INVALID if @a rs is past the generic
 *         steps, or is an `error` state
 */
static enum ANASTASIS_GenericState
get_generic_state (const struct ANASTASIS_ReduxState *rs)
{
  unsigned int s = UINT_MAX;

  switch (rs->type)
  {
  case ANASTASIS_RT_BACKUP:
    s = (unsigned int) rs->details.backup.state;
    break;
  case ANASTASIS_RT_RECOVERY:
    s = (unsigned int) rs->details.recovery.state;
    break;
  case ANASTASIS_RT_ERROR:
    return ANASTASIS_GENERIC_STATE_INVALID;
  }
  if (s >= sizeof (generic_strings) / sizeof(*generic_strings))
    return ANASTASIS_GENERIC_STATE_INVALID;
  return (enum ANASTASIS_GenericState) s;
}


enum ANASTASIS_GenericState
ANASTASIS_generic_state_from_string_ (const char *state_string)
{
  for (enum ANASTASIS_GenericState i = 0;
       i < sizeof (generic_strings) / sizeof(*generic_strings);
       i++)
    if (0 == strcmp (state_string,
                     generic_strings[i]))
      return i;
  return ANASTASIS_GENERIC_STATE_INVALID;
}


const char *
ANASTASIS_generic_state_to_string_ (enum ANASTASIS_GenericState gs)
{
  if ( (gs < 0) ||
       (gs >= sizeof (generic_strings) / sizeof(*generic_strings)) )
  {
    GNUNET_break_op (0);
    return NULL;
  }
  return generic_strings[gs];
}


void
ANASTASIS_redux_fail_ (ANASTASIS_ActionCallback cb,
                       void *cb_cls,
                       enum TALER_ErrorCode ec,
                       const char *detail)
{
  json_t *estate;

  estate = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("detail",
                               detail)),
    GNUNET_JSON_pack_string ("reducer_type",
                             "error"),
    GNUNET_JSON_pack_uint64 ("code",
                             ec),
    GNUNET_JSON_pack_string ("hint",
                             TALER_ErrorCode_get_hint (ec)));
  cb (cb_cls,
      ec,
      estate);
  json_decref (estate);
}


/**
 * Transition @a rs to the generic state @a gs.
 *
 * @param[in,out] rs state to transition
 * @param gs state to transition to
 */
static void
redux_transition (struct ANASTASIS_ReduxState *rs,
                  enum ANASTASIS_GenericState gs)
{
  switch (rs->type)
  {
  case ANASTASIS_RT_BACKUP:
    rs->details.backup.state = (enum ANASTASIS_BackupState) gs;
    return;
  case ANASTASIS_RT_RECOVERY:
    rs->details.recovery.state = (enum ANASTASIS_RecoveryState) gs;
    return;
  case ANASTASIS_RT_ERROR:
    break;
  }
  GNUNET_assert (0);
}


void
ANASTASIS_redux_init (struct GNUNET_CURL_Context *ctx)
{
  ANASTASIS_REDUX_ctx_ = ctx;
}


/**
 * Function to free a `struct ConfigRequest`, an async operation.
 *
 * @param cr state for a "get config" operation
 */
static void
free_config_request (struct ConfigRequest *cr)
{
  GNUNET_assert (NULL == cr->w_head);
  if (NULL != cr->co)
    ANASTASIS_config_cancel (cr->co);
  if (NULL != cr->tt)
    GNUNET_SCHEDULER_cancel (cr->tt);
  GNUNET_free (cr->url);
  GNUNET_free (cr->business_name);
  for (unsigned int i = 0; i<cr->methods_length; i++)
  {
    GNUNET_free (cr->methods[i].type);
    TALER_amount_list_free (&cr->methods[i].usage_fees);
  }
  GNUNET_free (cr->methods);
  TALER_amount_list_free (&cr->annual_fees);
  TALER_amount_list_free (&cr->truth_upload_fees);
  TALER_amount_list_free (&cr->liability_limits);
  for (unsigned int i = 0; i<cr->currencies_len; i++)
    GNUNET_free (cr->currencies[i]);
  GNUNET_array_grow (cr->currencies,
                     cr->currencies_len,
                     0);
  GNUNET_free (cr);
}


void
ANASTASIS_redux_done ()
{
  struct ConfigRequest *cr;

  while (NULL != (cr = cr_head))
  {
    GNUNET_CONTAINER_DLL_remove (cr_head,
                                 cr_tail,
                                 cr);
    free_config_request (cr);
  }
  ANASTASIS_REDUX_ctx_ = NULL;
  if (NULL != redux_countries)
  {
    json_decref (redux_countries);
    redux_countries = NULL;
  }
  if (NULL != redux_id_attr)
  {
    json_decref (redux_id_attr);
    redux_id_attr = NULL;
  }
  if (NULL != provider_list)
  {
    json_decref (provider_list);
    provider_list = NULL;
  }
}


const json_t *
ANASTASIS_redux_countries_init_ (void)
{
  char *dn;
  json_error_t error;

  if (NULL != redux_countries)
    return redux_countries;

  {
    char *path;

    path = GNUNET_OS_installation_get_path (ANASTASIS_project_data (),
                                            GNUNET_OS_IPK_DATADIR);
    if (NULL == path)
    {
      GNUNET_break (0);
      return NULL;
    }
    GNUNET_asprintf (&dn,
                     "%s/redux.countries.json",
                     path);
    GNUNET_free (path);
  }
  redux_countries = json_load_file (dn,
                                    JSON_COMPACT,
                                    &error);
  if (NULL == redux_countries)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to parse `%s': %s at %d:%d (%d)\n",
                dn,
                error.text,
                error.line,
                error.column,
                error.position);
    GNUNET_free (dn);
    return NULL;
  }
  GNUNET_free (dn);
  return redux_countries;
}


/**
 * Abort waiting for /config reply.
 *
 * @param cls a `struct ConfigReduxWaiting` handle.
 */
static void
abort_provider_config_cb (void *cls)
{
  struct ConfigReduxWaiting *w = cls;
  struct ConfigRequest *cr = w->cr;

  GNUNET_CONTAINER_DLL_remove (cr->w_head,
                               cr->w_tail,
                               w);
  /* w->rs is borrowed; freeing it is the caller's business. */
  GNUNET_free (w);
}


/**
 * Notify anyone waiting on @a cr that the request is done
 * (successful or failed).
 *
 * @param[in,out] cr request that completed
 */
static void
notify_waiting (struct ConfigRequest *cr)
{
  struct ConfigReduxWaiting *w;

  while (NULL != (w = cr->w_head))
  {
    struct ANASTASIS_ReduxProvider *p;

    p = ANASTASIS_REDUX_provider_get_ (&w->rs->common,
                                       cr->url);
    if (p->have_config)
      ANASTASIS_REDUX_provider_config_clear_ (&p->config);
    p->have_config = false;
    memset (&p->error,
            0,
            sizeof (p->error));
    if (TALER_EC_NONE != cr->ec)
    {
      p->status = ANASTASIS_RPS_ERROR;
      p->error.ec = cr->ec;
      p->error.http_status = cr->http_status;
    }
    else
    {
      struct ANASTASIS_ReduxProviderConfig *cfg = &p->config;

      p->status = ANASTASIS_RPS_OK;
      p->have_config = true;
      cfg->methods_len = cr->methods_length;
      cfg->methods = GNUNET_new_array (cfg->methods_len,
                                       struct ANASTASIS_ReduxMethodSpec);
      for (unsigned int i = 0; i<cr->methods_length; i++)
      {
        cfg->methods[i].type = GNUNET_strdup (cr->methods[i].type);
        cfg->methods[i].usage_fee = cr->methods[i].usage_fee;
        TALER_amount_list_copy (&cfg->methods[i].usage_fees,
                                &cr->methods[i].usage_fees);
      }
      cfg->annual_fee = cr->annual_fee;
      TALER_amount_list_copy (&cfg->annual_fees,
                              &cr->annual_fees);
      cfg->truth_upload_fee = cr->truth_upload_fee;
      TALER_amount_list_copy (&cfg->truth_upload_fees,
                              &cr->truth_upload_fees);
      cfg->liability_limit = cr->liability_limit;
      TALER_amount_list_copy (&cfg->liability_limits,
                              &cr->liability_limits);
      cfg->currencies_len = cr->currencies_len;
      cfg->currencies = GNUNET_new_array (cfg->currencies_len,
                                          char *);
      for (unsigned int i = 0; i<cfg->currencies_len; i++)
        cfg->currencies[i] = GNUNET_strdup (cr->currencies[i]);
      if ( (NULL == cfg->currency) &&
           (0 != cfg->currencies_len) )
        cfg->currency = GNUNET_strdup (cfg->currencies[0]);
      cfg->provider_salt = cr->provider_salt;
      cfg->business_name = GNUNET_strdup (cr->business_name);
      cfg->storage_limit_in_megabytes = cr->storage_limit_in_megabytes;
      cfg->http_status = cr->http_status;
    }
    w->cb (w->cb_cls,
           cr->ec,
           w->rs);
    abort_provider_config_cb (w);
  }
}


/**
 * Notify anyone waiting on @a cr that the request is done
 * (successful or failed).
 *
 * @param[in,out] cls request that completed
 */
static void
notify_waiting_cb (void *cls)
{
  struct ConfigRequest *cr = cls;

  cr->tt = NULL;
  notify_waiting (cr);
}


/**
 * Function called when it is time to retry a
 * failed /config request.
 *
 * @param cls the `struct ConfigRequest *` to retry.
 */
static void
retry_config (void *cls);


/**
 * Function called with the results of a #ANASTASIS_get_config().
 *
 * @param cls closure
 * @param acfg anastasis configuration
 */
static void
config_cb (void *cls,
           const struct ANASTASIS_Config *acfg)
{
  struct ConfigRequest *cr = cls;

  cr->co = NULL;
  if (NULL != cr->tt)
  {
    GNUNET_SCHEDULER_cancel (cr->tt);
    cr->tt = NULL;
  }
  cr->http_status = acfg->http_status;
  if (MHD_HTTP_OK != acfg->http_status)
  {
    if (0 == acfg->http_status)
      cr->ec = TALER_EC_ANASTASIS_GENERIC_PROVIDER_UNREACHABLE;
    else
      cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED;
  }
  if (0 == acfg->details.ok.storage_limit_in_megabytes)
  {
    cr->http_status = 0;
    cr->ec = TALER_EC_ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG;
  }
  else
  {
    cr->ec = TALER_EC_NONE;
    GNUNET_free (cr->business_name);
    cr->business_name = GNUNET_strdup (acfg->details.ok.business_name);
    for (unsigned int i = 0; i<cr->methods_length; i++)
    {
      GNUNET_free (cr->methods[i].type);
      TALER_amount_list_free (&cr->methods[i].usage_fees);
    }
    GNUNET_free (cr->methods);
    cr->methods = GNUNET_new_array (acfg->details.ok.methods_length,
                                    struct AuthorizationMethodConfig);
    for (unsigned int i = 0; i<acfg->details.ok.methods_length; i++)
    {
      cr->methods[i].type = GNUNET_strdup (acfg->details.ok.methods[i].type);
      cr->methods[i].usage_fee = acfg->details.ok.methods[i].usage_fee;
      TALER_amount_list_copy (&cr->methods[i].usage_fees,
                              &acfg->details.ok.methods[i].usage_fees);
    }
    cr->methods_length = acfg->details.ok.methods_length;
    cr->storage_limit_in_megabytes =
      acfg->details.ok.storage_limit_in_megabytes;
    cr->annual_fee = acfg->details.ok.annual_fee;
    cr->truth_upload_fee = acfg->details.ok.truth_upload_fee;
    cr->liability_limit = acfg->details.ok.liability_limit;
    TALER_amount_list_free (&cr->annual_fees);
    TALER_amount_list_copy (&cr->annual_fees,
                            &acfg->details.ok.annual_fees);
    TALER_amount_list_free (&cr->truth_upload_fees);
    TALER_amount_list_copy (&cr->truth_upload_fees,
                            &acfg->details.ok.truth_upload_fees);
    TALER_amount_list_free (&cr->liability_limits);
    TALER_amount_list_copy (&cr->liability_limits,
                            &acfg->details.ok.liability_limits);
    for (unsigned int i = 0; i<cr->currencies_len; i++)
      GNUNET_free (cr->currencies[i]);
    GNUNET_array_grow (cr->currencies,
                       cr->currencies_len,
                       0);
    for (unsigned int i = 0; i<acfg->details.ok.currencies_len; i++)
      GNUNET_array_append (cr->currencies,
                           cr->currencies_len,
                           GNUNET_strdup (acfg->details.ok.currencies[i]));
    cr->provider_salt = acfg->details.ok.provider_salt;
  }
  notify_waiting (cr);
  if (MHD_HTTP_OK != acfg->http_status)
  {
    cr->backoff = GNUNET_TIME_STD_BACKOFF (cr->backoff);
    GNUNET_assert (NULL == cr->tt);
    GNUNET_assert (NULL != cr->url);
    cr->tt = GNUNET_SCHEDULER_add_delayed (cr->backoff,
                                           &retry_config,
                                           cr);
  }
}


/**
 * Aborts a "get config" after timeout.
 *
 * @param cls closure for a "get config" request
 */
static void
config_request_timeout (void *cls)
{
  struct ConfigRequest *cr = cls;

  cr->tt = NULL;
  if (NULL != cr->co)
  {
    ANASTASIS_config_cancel (cr->co);
    cr->co = NULL;
  }
  cr->http_status = 0;
  cr->ec = TALER_EC_GENERIC_TIMEOUT;
  notify_waiting (cr);
  cr->backoff = GNUNET_TIME_STD_BACKOFF (cr->backoff);
  GNUNET_assert (NULL == cr->tt);
  GNUNET_assert (NULL != cr->url);
  cr->tt = GNUNET_SCHEDULER_add_delayed (cr->backoff,
                                         &retry_config,
                                         cr);
}


static void
retry_config (void *cls)
{
  struct ConfigRequest *cr = cls;

  cr->tt = NULL;
  if (NULL != cr->co)
  {
    ANASTASIS_config_cancel (cr->co);
    cr->co = NULL;
  }
  cr->timeout_at = GNUNET_TIME_relative_to_absolute (CONFIG_GENERIC_TIMEOUT);
  GNUNET_assert (NULL == cr->tt);
  cr->tt = GNUNET_SCHEDULER_add_at (cr->timeout_at,
                                    &config_request_timeout,
                                    cr);
  cr->co = ANASTASIS_get_config (ANASTASIS_REDUX_ctx_,
                                 cr->url,
                                 &config_cb,
                                 cr);
  GNUNET_break (NULL != cr->co);
}


/**
 * Schedule job to obtain Anastasis provider configuration at @a url.
 *
 * @param timeout how long to wait for a reply
 * @param url base URL of Anastasis provider
 * @return check config handle
 */
static struct ConfigRequest *
check_config (struct GNUNET_TIME_Relative timeout,
              const char *url)
{
  struct ConfigRequest *cr;

  for (cr = cr_head; NULL != cr; cr = cr->next)
  {
    if (0 != strcmp (url,
                     cr->url))
      continue;
    if (NULL != cr->co)
    {
      struct GNUNET_TIME_Relative duration;
      struct GNUNET_TIME_Relative left;
      struct GNUNET_TIME_Relative xleft;

      duration = GNUNET_TIME_absolute_get_duration (cr->start_time);
      left = GNUNET_TIME_relative_subtract (timeout,
                                            duration);
      xleft = GNUNET_TIME_absolute_get_remaining (cr->timeout_at);
      if (GNUNET_TIME_relative_cmp (left,
                                    <,
                                    xleft))
      {
        /* new timeout is shorter! */
        cr->timeout_at = GNUNET_TIME_relative_to_absolute (left);
        GNUNET_SCHEDULER_cancel (cr->tt);
        cr->tt = GNUNET_SCHEDULER_add_at (cr->timeout_at,
                                          &config_request_timeout,
                                          cr);
      }
      return cr; /* already on it */
    }
    break;
  }
  if (NULL == cr)
  {
    cr = GNUNET_new (struct ConfigRequest);
    cr->start_time = GNUNET_TIME_absolute_get ();
    cr->url = GNUNET_strdup (url);
    GNUNET_CONTAINER_DLL_insert (cr_head,
                                 cr_tail,
                                 cr);
  }
  if (MHD_HTTP_OK == cr->http_status)
    return cr;
  cr->timeout_at = GNUNET_TIME_relative_to_absolute (timeout);
  if (NULL != cr->tt)
    GNUNET_SCHEDULER_cancel (cr->tt);
  cr->tt = GNUNET_SCHEDULER_add_at (cr->timeout_at,
                                    &config_request_timeout,
                                    cr);
  cr->co = ANASTASIS_get_config (ANASTASIS_REDUX_ctx_,
                                 cr->url,
                                 &config_cb,
                                 cr);
  if (NULL == cr->co)
  {
    GNUNET_break (0);
    return NULL;
  }
  return cr;
}


/**
 * Begin asynchronous check for provider configurations.
 *
 * @param cc country code that was selected
 * @param[in,out] common state to set the provider list for
 * @return #TALER_EC_NONE on success
 */
static enum TALER_ErrorCode
begin_provider_config_check (const char *cc,
                             struct ANASTASIS_ReduxCommon *common)
{
  if (NULL == provider_list)
  {
    json_error_t error;
    char *dn;
    char *path;

    path = GNUNET_OS_installation_get_path (ANASTASIS_project_data (),
                                            GNUNET_OS_IPK_DATADIR);
    if (NULL == path)
    {
      GNUNET_break (0);
      return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    }
    GNUNET_asprintf (&dn,
                     "%s/provider-list.json",
                     path);
    GNUNET_free (path);
    provider_list = json_load_file (dn,
                                    JSON_COMPACT,
                                    &error);
    if (NULL == provider_list)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Failed to parse `%s': %s at %d:%d (%d)\n",
                  dn,
                  error.text,
                  error.line,
                  error.column,
                  error.position);
      GNUNET_free (dn);
      return TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED;
    }
    GNUNET_free (dn);
  }

  {
    size_t index;
    json_t *provider;
    const json_t *provider_arr = json_object_get (provider_list,
                                                  "anastasis_provider");

    /* The applicable providers depend on the country, so start over
       rather than merge into whatever an earlier selection left. */
    ANASTASIS_REDUX_providers_clear_ (common);
    common->have_providers = true;
    json_array_foreach (provider_arr, index, provider)
    {
      const char *url;
      const char *restricted = NULL;
      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_string ("url",
                                 &url),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_string ("restricted",
                                   &restricted),
          NULL),
        GNUNET_JSON_spec_end ()
      };

      if (GNUNET_OK !=
          GNUNET_JSON_parse (provider,
                             spec,
                             NULL, NULL))
      {
        GNUNET_break (0);
        ANASTASIS_REDUX_providers_clear_ (common);
        return TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED;
      }
      if ( (NULL != restricted) &&
           (0 != strcmp (restricted,
                         cc)) )
      {
        /* skip */
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Skipping provider restricted to country `%s'\n",
                    restricted);
        continue;
      }
      if ( (NULL == restricted) &&
           (0 == strcmp (cc,
                         "xx")) )
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Running in demo mode, skipping unrestricted providers\n");
        /* demo mode, skipping regular providers */
        continue;
      }
      (void) ANASTASIS_REDUX_provider_get_ (common,
                                            url);
      check_config (CONFIG_GENERIC_TIMEOUT,
                    url);
    }
  }
  return TALER_EC_NONE;
}


/**
 * Function to validate an input by regular expression ("validation-regex").
 *
 * @param input text to validate
 * @param regexp regular expression to validate
 * @return true if validation passed, else false
 */
static bool
validate_regex (const char *input,
                const char *regexp)
{
  regex_t regex;

  if (0 != regcomp (&regex,
                    regexp,
                    REG_EXTENDED))
  {
    /* The expression comes from the (untrusted) state, so failing to compile
       it must not mean "input accepted": that would let a client disable the
       check for an attribute simply by supplying a broken regex. */
    GNUNET_break_op (0);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to compile regular expression `%s'.",
                regexp);
    return false;
  }
  /* check if input has correct form */
  if (0 != regexec (&regex,
                    input,
                    0,
                    NULL,
                    0))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Input `%s' does not match regex `%s'\n",
                input,
                regexp);
    regfree (&regex);
    return false;
  }
  regfree (&regex);
  return true;
}


/**
 * Function to load json containing country specific identity
 * attributes.  Uses a single-slot cache to avoid loading
 * exactly the same attributes twice.
 *
 * @param country_code country code (e.g. "de")
 * @return NULL on error
 */
static const json_t *
redux_id_attr_init (const char *country_code)
{
  static char redux_id_cc[3];
  char *dn;
  json_error_t error;

  /* `country_code` originates from untrusted state; it is interpolated into a
     file path below, so reject anything that is not exactly two ASCII letters
     to prevent path traversal (e.g. "../../..") into arbitrary JSON files. */
  {
    char c0 = country_code[0];
    char c1 = (c0 == '\0') ? '\0' : country_code[1];

    if ( (2 != strlen (country_code)) ||
         (! ( ((c0 >= 'A') && (c0 <= 'Z')) || ((c0 >= 'a') && (c0 <= 'z')) )) ||
         (! ( ((c1 >= 'A') && (c1 <= 'Z')) || ((c1 >= 'a') && (c1 <= 'z')) )) )
    {
      GNUNET_break (0);
      return NULL;
    }
  }

  if (0 == strcmp (country_code,
                   redux_id_cc))
    return redux_id_attr;

  if (NULL != redux_id_attr)
  {
    json_decref (redux_id_attr);
    redux_id_attr = NULL;
  }
  {
    char *path;

    path = GNUNET_OS_installation_get_path (ANASTASIS_project_data (),
                                            GNUNET_OS_IPK_DATADIR);
    if (NULL == path)
    {
      GNUNET_break (0);
      return NULL;
    }
    GNUNET_asprintf (&dn,
                     "%s/redux.%s.json",
                     path,
                     country_code);
    GNUNET_free (path);
  }
  redux_id_attr = json_load_file (dn,
                                  JSON_COMPACT,
                                  &error);
  if (NULL == redux_id_attr)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to parse `%s': %s at %d:%d (%d)\n",
                dn,
                error.text,
                error.line,
                error.column,
                error.position);
    GNUNET_free (dn);
    return NULL;
  }
  GNUNET_free (dn);
  strncpy (redux_id_cc,
           country_code,
           sizeof (redux_id_cc));
  redux_id_cc[2] = '\0';
  return redux_id_attr;
}


/**
 * DispatchHandler/Callback function which is called for a
 * "select_continent" action.
 *
 * @param[in] rs state to operate on
 * @param arguments arguments to use for operation on state
 * @param cb callback to call during/after operation
 * @param cb_cls callback closure
 * @return NULL
 */
static struct ANASTASIS_ReduxAction *
select_continent (struct ANASTASIS_ReduxState *rs,
                  const json_t *arguments,
                  ANASTASIS_ActionCallback cb,
                  void *cb_cls)
{
  const json_t *rc = ANASTASIS_redux_countries_init_ ();
  const json_t *root = json_object_get (rc,
                                        "countries");
  const json_t *continent;
  const char *cname;
  json_t *countries;

  if (NULL == root)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED,
                           "'countries' missing");
    return NULL;
  }
  if (NULL == arguments)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return NULL;
  }
  continent = json_object_get (arguments,
                               "continent");
  if (NULL == continent)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'continent' missing");
    return NULL;
  }
  cname = json_string_value (continent);
  countries = json_array ();
  GNUNET_assert (NULL != countries);
  {
    size_t index;
    const json_t *country;

    json_array_foreach (root, index, country)
    {
      const char *cc = json_string_value (json_object_get (country,
                                                           "continent"));

      if ( (NULL != cname) &&
           (NULL != cc) &&
           (0 == strcmp (cname,
                         cc)) )
        GNUNET_assert (0 ==
                       json_array_append (countries,
                                          (json_t *) country));
    }
    if (0 == json_array_size (countries))
    {
      json_decref (countries);
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                             "'continent' unknown");
      return NULL;
    }
  }
  {
    const char *detail;

    if (GNUNET_OK !=
        ANASTASIS_REDUX_countries_set_ (&rs->common,
                                        countries,
                                        &detail))
    {
      GNUNET_break (0);
      json_decref (countries);
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED,
                             detail);
      return NULL;
    }
  }
  json_decref (countries);
  redux_transition (rs,
                    ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING);
  GNUNET_free (rs->common.selected_continent);
  rs->common.selected_continent = GNUNET_strdup (cname);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
  return NULL;
}


/**
 * DispatchHandler/Callback function which is called for a
 * "select_country" action.
 *
 * @param state state to operate on
 * @param arguments arguments to use for operation on state
 * @param cb callback to call during/after operation
 * @param cb_cls callback closure
 * @return #ANASTASIS_ReduxAction
 */
static struct ANASTASIS_ReduxAction *
select_country (struct ANASTASIS_ReduxState *rs,
                const json_t *arguments,
                ANASTASIS_ActionCallback cb,
                void *cb_cls)
{
  const json_t *required_attrs;
  const char *country_code;

  if (NULL == arguments)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return NULL;
  }
  if (NULL == json_object_get (arguments,
                               "country_code"))
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'country_code' missing");
    return NULL;
  }
  country_code = json_string_value (json_object_get (arguments,
                                                     "country_code"));

  {
    bool found = false;

    for (unsigned int i = 0; i < rs->common.countries_len; i++)
      if ( (NULL != country_code) &&
           (0 == strcmp (rs->common.countries[i].code,
                         country_code)) )
      {
        found = true;
        break;
      }
    if (! found)
    {
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                             "specified country not on selected continent");
      return NULL;
    }
  }

  /* Begin fetching provider /configs (we likely need them later) */
  {
    enum TALER_ErrorCode ec;

    ec = begin_provider_config_check (country_code,
                                      &rs->common);
    if (TALER_EC_NONE != ec)
    {
      GNUNET_break (0);
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             ec,
                             NULL);
      return NULL;
    }
  }

  {
    const json_t *ria;

    ria = redux_id_attr_init (country_code);
    if (NULL == ria)
    {
      GNUNET_break (0);
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_RESOURCE_MISSING,
                             country_code);
      return NULL;
    }
    required_attrs = json_object_get (ria,
                                      "required_attributes");
  }
  if (NULL == required_attrs)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED,
                           "'required_attributes' missing");
    return NULL;
  }
  {
    const char *detail;

    if (GNUNET_OK !=
        ANASTASIS_REDUX_required_attributes_set_ (&rs->common,
                                                  required_attrs,
                                                  &detail))
    {
      GNUNET_break (0);
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_RESOURCE_MALFORMED,
                             detail);
      return NULL;
    }
  }
  redux_transition (rs,
                    ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING);
  GNUNET_free (rs->common.selected_country);
  rs->common.selected_country = GNUNET_strdup (country_code);
  /* The country's currency is the default the user is offered; an
     explicit "select_currency" overrides it. */
  if (NULL == rs->common.preferred_currency)
    for (unsigned int i = 0; i < rs->common.countries_len; i++)
      if ( (0 == strcmp (rs->common.countries[i].code,
                         country_code)) &&
           (NULL != rs->common.countries[i].currency) )
      {
        rs->common.preferred_currency
          = GNUNET_strdup (rs->common.countries[i].currency);
        break;
      }
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
  return NULL;
}


/**
 * DispatchHandler/Callback function which is called for a
 * "select_currency" action.
 *
 * Which currency the user would rather pay in is advisory: every order
 * this provider creates carries all of its currencies as choices, so the
 * wallet may still settle in another one.  What it decides is which total
 * the reducer quotes and which currency a UI puts in front of the user.
 *
 * @param state state to operate on
 * @param arguments arguments to use for operation on state
 * @param cb callback to call during/after operation
 * @param cb_cls callback closure
 * @return NULL
 */
static struct ANASTASIS_ReduxAction *
select_currency (struct ANASTASIS_ReduxState *rs,
                 const json_t *arguments,
                 ANASTASIS_ActionCallback cb,
                 void *cb_cls)
{
  const char *currency;

  if (NULL == arguments)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return NULL;
  }
  currency = json_string_value (json_object_get (arguments,
                                                 "currency"));
  if (NULL == currency)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'currency' missing");
    return NULL;
  }
  /* Not checked against the providers: at this point their /config
     requests are still in flight, and a currency none of them offers is
     not an error either --- it just means no single-currency total can
     be quoted. */
  if (GNUNET_OK !=
      TALER_check_currency (currency))
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'currency' malformed");
    return NULL;
  }
  GNUNET_free (rs->common.preferred_currency);
  rs->common.preferred_currency = GNUNET_strdup (currency);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
  return NULL;
}


/**
 * DispatchHandler/Callback function which is called for a
 * "unselect_continent" action.
 *
 * @param state state to operate on
 * @param arguments arguments to use for operation on state
 * @param cb callback to call during/after operation
 * @param cb_cls callback closure
 * @return NULL
 */
static struct ANASTASIS_ReduxAction *
unselect_continent (struct ANASTASIS_ReduxState *rs,
                    const json_t *arguments,
                    ANASTASIS_ActionCallback cb,
                    void *cb_cls)
{
  redux_transition (rs,
                    ANASTASIS_GENERIC_STATE_CONTINENT_SELECTING);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
  return NULL;
}


struct ANASTASIS_ReduxAction *
ANASTASIS_REDUX_add_provider_to_state_ (const char *url,
                                        struct ANASTASIS_ReduxState *rs,
                                        ANASTASIS_REDUX_StateCallback cb,
                                        void *cb_cls)
{
  struct ConfigRequest *cr;
  struct ConfigReduxWaiting *w;

  cr = check_config (CONFIG_FAST_TIMEOUT,
                     url);
  w = GNUNET_new (struct ConfigReduxWaiting);
  w->cr = cr;
  w->rs = rs;
  w->cb = cb;
  w->cb_cls = cb_cls;
  w->ra.cleanup = &abort_provider_config_cb;
  w->ra.cleanup_cls = w;
  GNUNET_CONTAINER_DLL_insert (cr->w_head,
                               cr->w_tail,
                               w);
  if (NULL == cr->co)
  {
    if (NULL != cr->tt)
      GNUNET_SCHEDULER_cancel (cr->tt);
    cr->tt = GNUNET_SCHEDULER_add_now (&notify_waiting_cb,
                                       cr);
  }
  return &w->ra;
}


/**
 * Context for #ANASTASIS_REDUX_provider_add_().
 */
struct ProviderAddContext
{
  /**
   * Handle we returned for cancellation of the operation.
   */
  struct ANASTASIS_ReduxAction ra;

  /**
   * The /config request we are waiting for, NULL once it completed.
   */
  struct ANASTASIS_ReduxAction *inner;

  /**
   * State we own until we hand it to @e cb.
   */
  struct ANASTASIS_ReduxState *rs;

  /**
   * Function to call with the new state.
   */
  ANASTASIS_ActionCallback cb;

  /**
   * Closure for @e cb.
   */
  void *cb_cls;
};


/**
 * Free @a cls.
 *
 * @param[in] cls a `struct ProviderAddContext *`
 */
static void
provider_add_cleanup (void *cls)
{
  struct ProviderAddContext *pac = cls;

  if (NULL != pac->inner)
    pac->inner->cleanup (pac->inner->cleanup_cls);
  ANASTASIS_REDUX_state_free_ (pac->rs);
  GNUNET_free (pac);
}


/**
 * The /config request completed; return the updated state.
 *
 * @param cls a `struct ProviderAddContext *`
 * @param ec status of the request
 * @param[in] rs the updated state
 */
static void
provider_add_done (void *cls,
                   enum TALER_ErrorCode ec,
                   struct ANASTASIS_ReduxState *rs)
{
  struct ProviderAddContext *pac = cls;
  ANASTASIS_ActionCallback cb = pac->cb;
  void *cb_cls = pac->cb_cls;

  GNUNET_assert (rs == pac->rs);
  /* our waiter unlinks and frees itself right after this call */
  pac->inner = NULL;
  pac->rs = NULL;
  provider_add_cleanup (pac);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           ec);
}


struct ANASTASIS_ReduxAction *
ANASTASIS_REDUX_provider_add_ (const char *url,
                               struct ANASTASIS_ReduxState *rs,
                               ANASTASIS_ActionCallback cb,
                               void *cb_cls)
{
  struct ProviderAddContext *pac;

  pac = GNUNET_new (struct ProviderAddContext);
  pac->rs = rs;
  pac->cb = cb;
  pac->cb_cls = cb_cls;
  pac->inner = ANASTASIS_REDUX_add_provider_to_state_ (url,
                                                       rs,
                                                       &provider_add_done,
                                                       pac);
  /* the /config request never completes synchronously */
  GNUNET_assert (NULL != pac->inner);
  pac->ra.cleanup = &provider_add_cleanup;
  pac->ra.cleanup_cls = pac;
  return &pac->ra;
}


/**
 * DispatchHandler/Callback function which is called for a
 * "enter_user_attributes" action.
 * Returns an #ANASTASIS_ReduxAction if operation is async.
 *
 * @param state state to operate on
 * @param arguments arguments to use for operation on state
 * @param cb callback to call during/after operation
 * @param cb_cls callback closure
 * @return NULL
 */
static struct ANASTASIS_ReduxAction *
enter_user_attributes (struct ANASTASIS_ReduxState *rs,
                       const json_t *arguments,
                       ANASTASIS_ActionCallback cb,
                       void *cb_cls)
{
  const json_t *attributes;

  if (NULL == arguments)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return NULL;
  }
  attributes = json_object_get (arguments,
                                "identity_attributes");
  if (NULL == attributes)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'identity_attributes' missing");
    return NULL;
  }
  json_decref (rs->common.identity_attributes);
  rs->common.identity_attributes = json_incref ((json_t *) attributes);

  if (! rs->common.have_required_attributes)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'required_attributes' must be an array");
    return NULL;
  }
  /* Verify required attributes are present and well-formed */
  for (unsigned int i = 0; i < rs->common.required_attributes_len; i++)
  {
    const struct ANASTASIS_ReduxAttributeSpec *a
      = &rs->common.required_attributes[i];
    const char *attribute_value;

    attribute_value = json_string_value (json_object_get (attributes,
                                                          a->name));
    if (NULL == attribute_value)
    {
      if (a->optional)
        continue;
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Request is missing required attribute `%s'\n",
                  a->name);
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             TALER_EC_GENERIC_PARAMETER_MISSING,
                             a->name);
      return NULL;
    }
    if ( (NULL != a->validation_regex) &&
         (! validate_regex (attribute_value,
                            a->validation_regex)) )
    {
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_REGEX_FAILED,
                             a->name);
      return NULL;
    }
    if (NULL != a->validation_logic)
    {
      ANASTASIS_ValidationLogic regfun;

      regfun = ANASTASIS_REDUX_validation_lookup_ (a->validation_logic);
      if (NULL == regfun)
      {
        /* The name comes from the (untrusted) state, so an unknown one must
           fail the validation instead of skipping it. */
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Validation function `%s' is not known\n",
                    a->validation_logic);
        ANASTASIS_REDUX_fail_ (rs,
                               cb,
                               cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED,
                               a->name);
        return NULL;
      }
      if (! regfun (attribute_value))
      {
        ANASTASIS_REDUX_fail_ (rs,
                               cb,
                               cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED,
                               a->name);
        return NULL;
      }
    }
  } /* end for all attributes loop */

  /* Transition based on mode */
  switch (rs->type)
  {
  case ANASTASIS_RT_BACKUP:
    rs->details.backup.state
      = ANASTASIS_BACKUP_STATE_AUTHENTICATIONS_EDITING;
    return ANASTASIS_REDUX_backup_begin_ (rs,
                                          arguments,
                                          cb,
                                          cb_cls);
  case ANASTASIS_RT_RECOVERY:
    rs->details.recovery.state
      = ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING;
    return ANASTASIS_REDUX_recovery_challenge_begin_ (rs,
                                                      arguments,
                                                      cb,
                                                      cb_cls);
  case ANASTASIS_RT_ERROR:
    break;
  }
  GNUNET_assert (0);
  return NULL;
}


/**
 * DispatchHandler/Callback function which is called for a
 * "add_provider" action.  Adds another Anastasis provider
 * to the list of available providers for storing information.
 *
 * @param state state to operate on
 * @param arguments arguments with a provider URL to add
 * @param cb callback to call during/after operation
 * @param cb_cls callback closure
 */
static struct ANASTASIS_ReduxAction *
add_provider (struct ANASTASIS_ReduxState *rs,
              const json_t *arguments,
              ANASTASIS_ActionCallback cb,
              void *cb_cls)
{
  if (ANASTASIS_add_provider_ (rs,
                               arguments,
                               cb,
                               cb_cls))
    return NULL;
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
  return NULL;
}


bool
ANASTASIS_add_provider_ (struct ANASTASIS_ReduxState *rs,
                         const json_t *arguments,
                         ANASTASIS_ActionCallback cb,
                         void *cb_cls)
{
  const char *url;
  json_t *params;

  if (NULL == arguments)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return true; /* cb was invoked */
  }
  json_object_foreach (((json_t *) arguments), url, params)
  {
    const char *detail;

    if (GNUNET_OK !=
        ANASTASIS_REDUX_provider_set_ (&rs->common,
                                       url,
                                       params,
                                       &detail))
    {
      GNUNET_break_op (0);
      ANASTASIS_REDUX_fail_ (rs,
                             cb,
                             cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                             detail);
      return true; /* cb was invoked */
    }
  }
  return false; /* cb not invoked */
}


struct ANASTASIS_ReduxAction *
ANASTASIS_back_generic_decrement_ (struct ANASTASIS_ReduxState *rs,
                                   const json_t *arguments,
                                   ANASTASIS_ActionCallback cb,
                                   void *cb_cls)
{
  (void) arguments;
  switch (rs->type)
  {
  case ANASTASIS_RT_BACKUP:
    GNUNET_assert (rs->details.backup.state > 0);
    rs->details.backup.state--;
    break;
  case ANASTASIS_RT_RECOVERY:
    GNUNET_assert (rs->details.recovery.state > 0);
    rs->details.recovery.state--;
    break;
  case ANASTASIS_RT_ERROR:
    GNUNET_assert (0);
    break;
  }
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
  return NULL;
}


/**
 * Callback function which is called by the reducer in dependence of
 * given state and action.
 *
 * The handler takes ownership of @a rs and must dispose of it, either by
 * handing it to #ANASTASIS_REDUX_return_ / #ANASTASIS_REDUX_fail_ or by
 * passing it on to another handler that does.
 *
 * @param[in] rs the previous state to operate on
 * @param arguments the arguments needed by operation to operate on state
 * @param cb Callback function which returns the new state
 * @param cb_cls closure for @a cb
 * @return handle to cancel async actions, NULL if @a cb was already called
 */
typedef struct ANASTASIS_ReduxAction *
(*DispatchHandler)(struct ANASTASIS_ReduxState *rs,
                   const json_t *arguments,
                   ANASTASIS_ActionCallback cb,
                   void *cb_cls);


/**
 * Closure for read operations on the external reducer.
 */
struct ExternalReducerCls
{
  struct GNUNET_Buffer read_buffer;
  struct GNUNET_SCHEDULER_Task *read_task;
  struct GNUNET_DISK_PipeHandle *reducer_stdin;
  struct GNUNET_DISK_PipeHandle *reducer_stdout;
  struct GNUNET_Process *reducer_process;
  ANASTASIS_ActionCallback action_cb;
  void *action_cb_cls;
};

/**
 * Clean up and destroy the external reducer state.
 *
 * @param cls closure, a 'struct ExternalReducerCls *'
 */
static void
cleanup_external_reducer (void *cls)
{
  struct ExternalReducerCls *red_cls = cls;

  if (NULL != red_cls->read_task)
  {
    GNUNET_SCHEDULER_cancel (red_cls->read_task);
    red_cls->read_task = NULL;
  }

  GNUNET_buffer_clear (&red_cls->read_buffer);
  if (NULL != red_cls->reducer_stdin)
  {
    GNUNET_DISK_pipe_close (red_cls->reducer_stdin);
    red_cls->reducer_stdin = NULL;
  }
  if (NULL != red_cls->reducer_stdout)
  {
    GNUNET_DISK_pipe_close (red_cls->reducer_stdout);
    red_cls->reducer_stdout = NULL;
  }

  if (NULL != red_cls->reducer_process)
  {
    enum GNUNET_OS_ProcessStatusType type;
    unsigned long code;
    enum GNUNET_GenericReturnValue pwret;

    pwret = GNUNET_process_wait (red_cls->reducer_process,
                                 false,
                                 &type,
                                 &code);
    GNUNET_assert (GNUNET_SYSERR != pwret);
    if (GNUNET_NO == pwret)
    {
      GNUNET_assert (GNUNET_OK ==
                     GNUNET_process_kill (red_cls->reducer_process,
                                          SIGTERM));
      GNUNET_assert (GNUNET_SYSERR !=
                     GNUNET_process_wait (red_cls->reducer_process,
                                          true,
                                          NULL,
                                          NULL));
    }

    GNUNET_process_destroy (red_cls->reducer_process);
    red_cls->reducer_process = NULL;
  }

  GNUNET_free (red_cls);
}


/**
 * Task called when
 *
 * @param cls closure, a 'struct ExternalReducerCls *'
 */
static void
external_reducer_read_cb (void *cls)
{
  struct ExternalReducerCls *red_cls = cls;
  ssize_t sret;
  char buf[256];

  red_cls->read_task = NULL;

  sret = GNUNET_DISK_file_read (GNUNET_DISK_pipe_handle (
                                  red_cls->reducer_stdout,
                                  GNUNET_DISK_PIPE_END_READ),
                                buf,
                                256);
  if (sret < 0)
  {
    GNUNET_break (0);
    red_cls->action_cb (red_cls->action_cb_cls,
                        TALER_EC_ANASTASIS_REDUCER_INTERNAL_ERROR,
                        NULL);
    cleanup_external_reducer (red_cls);
    return;
  }
  else if (0 == sret)
  {
    char *str = GNUNET_buffer_reap_str (&red_cls->read_buffer);
    json_t *json;

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Got external reducer response: '%s'\n",
                str);

    json = json_loads (str, 0, NULL);

    if (NULL == json)
    {
      GNUNET_break (0);
      red_cls->action_cb (red_cls->action_cb_cls,
                          TALER_EC_ANASTASIS_REDUCER_INTERNAL_ERROR,
                          NULL);
      cleanup_external_reducer (red_cls);
      return;
    }

    {
      enum TALER_ErrorCode ec;
      ec = json_integer_value (json_object_get (json, "code"));

      red_cls->action_cb (red_cls->action_cb_cls,
                          ec,
                          json);
    }
    cleanup_external_reducer (red_cls);
    return;
  }
  else
  {
    GNUNET_buffer_write (&red_cls->read_buffer,
                         buf,
                         sret);

    red_cls->read_task = GNUNET_SCHEDULER_add_read_file (
      GNUNET_TIME_UNIT_FOREVER_REL,
      GNUNET_DISK_pipe_handle (
        red_cls->reducer_stdout,
        GNUNET_DISK_PIPE_END_READ),
      external_reducer_read_cb,
      red_cls);
  }
}


/**
 * Handle an action using an external reducer, i.e.
 * by shelling out to another process.
 */
static struct ANASTASIS_ReduxAction *
redux_action_external (const char *ext_reducer,
                       const json_t *state,
                       const char *action,
                       const json_t *arguments,
                       ANASTASIS_ActionCallback cb,
                       void *cb_cls)
{
  char *arg_str;
  char *state_str = json_dumps (state, JSON_COMPACT);
  ssize_t sret;
  struct ExternalReducerCls *red_cls = GNUNET_new (struct ExternalReducerCls);

  if (NULL == arguments)
    arg_str = GNUNET_strdup ("{}");
  else
    arg_str = json_dumps (arguments, JSON_COMPACT);

  red_cls->action_cb = cb;
  red_cls->action_cb_cls = cb_cls;

  GNUNET_assert (NULL != (red_cls->reducer_stdin = GNUNET_DISK_pipe (
                            GNUNET_DISK_PF_NONE)));
  GNUNET_assert (NULL != (red_cls->reducer_stdout = GNUNET_DISK_pipe (
                            GNUNET_DISK_PF_NONE)));

  /* By the time we're here, this variable should be unset, because
     otherwise using anastasis-reducer as the external reducer
     will lead to infinite recursion. */
  GNUNET_assert (NULL == getenv ("ANASTASIS_EXTERNAL_REDUCER"));

  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Starting external reducer with action '%s' and argument '%s'\n",
              action,
              arg_str);

  red_cls->reducer_process
    = GNUNET_process_create (GNUNET_OS_INHERIT_STD_ERR);
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_process_set_options (
                   red_cls->reducer_process,
                   GNUNET_process_option_inherit_rpipe (red_cls->reducer_stdin,
                                                        STDIN_FILENO),
                   GNUNET_process_option_inherit_rpipe (red_cls->reducer_stdout,
                                                        STDOUT_FILENO)));
  if (GNUNET_OK !=
      GNUNET_process_run_command_va (red_cls->reducer_process,
                                     ext_reducer,
                                     ext_reducer,
                                     "-a",
                                     arg_str,
                                     action,
                                     NULL))
  {
    GNUNET_break (0);
    GNUNET_process_destroy (red_cls->reducer_process);
    red_cls->reducer_process = NULL;
    GNUNET_free (arg_str);
    GNUNET_free (state_str);
    cleanup_external_reducer (red_cls);
    return NULL;
  }
  GNUNET_free (arg_str);

  /* Close pipe ends we don't use. */
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_DISK_pipe_close_end (red_cls->reducer_stdin,
                                             GNUNET_DISK_PIPE_END_READ));
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_DISK_pipe_close_end (red_cls->reducer_stdout,
                                             GNUNET_DISK_PIPE_END_WRITE));

  sret = GNUNET_DISK_file_write_blocking (GNUNET_DISK_pipe_handle (
                                            red_cls->reducer_stdin,
                                            GNUNET_DISK_PIPE_END_WRITE),
                                          state_str,
                                          strlen (state_str));
  GNUNET_free (state_str);
  if (sret <= 0)
  {
    GNUNET_break (0);
    cleanup_external_reducer (red_cls);
    return NULL;
  }

  GNUNET_assert (GNUNET_OK ==
                 GNUNET_DISK_pipe_close_end (red_cls->reducer_stdin,
                                             GNUNET_DISK_PIPE_END_WRITE));

  red_cls->read_task = GNUNET_SCHEDULER_add_read_file (
    GNUNET_TIME_UNIT_FOREVER_REL,
    GNUNET_DISK_pipe_handle (
      red_cls->reducer_stdout,
      GNUNET_DISK_PIPE_END_READ),
    external_reducer_read_cb,
    red_cls);

  {
    struct ANASTASIS_ReduxAction *ra
      = GNUNET_new (struct ANASTASIS_ReduxAction);
    ra->cleanup_cls = red_cls;
    ra->cleanup = cleanup_external_reducer;
    return ra;
  }
}


struct ANASTASIS_ReduxAction *
ANASTASIS_redux_action (const json_t *state,
                        const char *action,
                        const json_t *arguments,
                        ANASTASIS_ActionCallback cb,
                        void *cb_cls)
{
  struct Dispatcher
  {
    enum ANASTASIS_GenericState redux_state;
    const char *redux_action;
    DispatchHandler fun;
  } dispatchers[] = {
    {
      ANASTASIS_GENERIC_STATE_CONTINENT_SELECTING,
      "select_continent",
      &select_continent
    },
    /* Deprecated alias for "back" from that state, should be removed eventually. */
    {
      ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING,
      "unselect_continent",
      &unselect_continent
    },
    {
      ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING,
      "back",
      &unselect_continent
    },
    {
      ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING,
      "select_country",
      &select_country
    },
    {
      ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING,
      "select_currency",
      &select_currency
    },
    {
      ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING,
      "select_currency",
      &select_currency
    },
    {
      ANASTASIS_GENERIC_STATE_COUNTRY_SELECTING,
      "select_continent",
      &select_continent
    },
    {
      ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING,
      "enter_user_attributes",
      &enter_user_attributes
    },
    {
      ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING,
      "add_provider",
      &add_provider
    },
    {
      ANASTASIS_GENERIC_STATE_USER_ATTRIBUTES_COLLECTING,
      "back",
      &ANASTASIS_back_generic_decrement_
    },
    { ANASTASIS_GENERIC_STATE_INVALID, NULL, NULL }
  };
  struct ANASTASIS_ReduxState *rs;
  enum ANASTASIS_GenericState gs;

  /* If requested, handle action with external reducer, used for testing. */
  {
    const char *ext_reducer = ANASTASIS_REDUX_probe_external_reducer ();
    if (NULL != ext_reducer)
      return redux_action_external (ext_reducer,
                                    state,
                                    action,
                                    arguments,
                                    cb,
                                    cb_cls);
  }

  /* This is the one place where a state is read.  Everything below
     operates on the typed representation, and every path back out goes
     through ANASTASIS_REDUX_return_() or ANASTASIS_REDUX_fail_(). */
  {
    enum TALER_ErrorCode ec;
    const char *detail;

    rs = ANASTASIS_REDUX_state_parse_ (state,
                                       &ec,
                                       &detail);
    if (NULL == rs)
    {
      GNUNET_break_op (0);
      ANASTASIS_redux_fail_ (cb,
                             cb_cls,
                             ec,
                             detail);
      return NULL;
    }
  }
  if (ANASTASIS_RT_ERROR == rs->type)
  {
    /* An error state has no step to advance from. */
    GNUNET_break_op (0);
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "reducer_type");
    return NULL;
  }
  gs = get_generic_state (rs);
  if (ANASTASIS_GENERIC_STATE_INVALID != gs)
  {
    for (unsigned int i = 0; NULL != dispatchers[i].fun; i++)
    {
      if ( (gs == dispatchers[i].redux_state) &&
           (0 == strcmp (action,
                         dispatchers[i].redux_action)) )
        return dispatchers[i].fun (rs,
                                   arguments,
                                   cb,
                                   cb_cls);
    }
  }
  if (ANASTASIS_RT_RECOVERY == rs->type)
    return ANASTASIS_recovery_action_ (rs,
                                       action,
                                       arguments,
                                       cb,
                                       cb_cls);
  return ANASTASIS_backup_action_ (rs,
                                   action,
                                   arguments,
                                   cb,
                                   cb_cls);
}


void
ANASTASIS_redux_action_cancel (struct ANASTASIS_ReduxAction *ra)
{
  ra->cleanup (ra->cleanup_cls);
}


json_t *
ANASTASIS_REDUX_load_continents_ ()
{
  const json_t *countries;
  json_t *continents;
  const json_t *rc = ANASTASIS_redux_countries_init_ ();

  if (NULL == rc)
  {
    GNUNET_break (0);
    return NULL;
  }
  countries = json_object_get (rc,
                               "countries");
  if (NULL == countries)
  {
    GNUNET_break (0);
    return NULL;
  }
  continents = json_array ();
  GNUNET_assert (NULL != continents);

  {
    json_t *country;
    size_t index;

    json_array_foreach (countries, index, country)
    {
      json_t *ex = NULL;
      const json_t *continent;

      continent = json_object_get (country,
                                   "continent");
      if ( (NULL == continent) ||
           (! json_is_string (continent)) )
      {
        GNUNET_break (0);
        continue;
      }
      {
        size_t inner_index;
        json_t *inner_continent;

        json_array_foreach (continents, inner_index, inner_continent)
        {
          const json_t *name;

          name = json_object_get (inner_continent,
                                  "name");
          if (1 == json_equal (continent,
                               name))
          {
            ex = inner_continent;
            break;
          }
        }
      }
      if (NULL == ex)
      {
        ex = GNUNET_JSON_PACK (
          GNUNET_JSON_pack_string ("name",
                                   json_string_value (continent)));
        GNUNET_assert (0 ==
                       json_array_append_new (continents,
                                              ex));
      }

      {
        json_t *i18n_continent;
        json_t *name_ex;

        i18n_continent = json_object_get (country,
                                          "continent_i18n");
        name_ex = json_object_get (ex,
                                   "name_i18n");
        if (NULL != i18n_continent)
        {
          const char *lang;
          json_t *trans;

          json_object_foreach (i18n_continent, lang, trans)
          {
            if (NULL == name_ex)
            {
              name_ex = json_object ();
              GNUNET_assert (NULL != name_ex);
              GNUNET_assert (0 ==
                             json_object_set_new (ex,
                                                  "name_i18n",
                                                  name_ex));
            }
            if (NULL == json_object_get (name_ex,
                                         lang))
            {
              GNUNET_assert (0 ==
                             json_object_set (name_ex,
                                              lang,
                                              trans));
            }
          }
        }
      }
    }
  }
  return GNUNET_JSON_PACK (
    GNUNET_JSON_pack_array_steal ("continents",
                                  continents));
}


enum GNUNET_GenericReturnValue
ANASTASIS_REDUX_lookup_salt_ (
  const struct ANASTASIS_ReduxCommon *common,
  const char *provider_url,
  struct ANASTASIS_CRYPTO_ProviderSaltP *provider_salt)
{
  const struct ANASTASIS_ReduxProvider *p;

  p = ANASTASIS_REDUX_provider_find_ (common,
                                      provider_url);
  if (NULL == p)
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  if (ANASTASIS_RPS_DISABLED == p->status)
    return GNUNET_NO;
  if (! p->have_config)
    return GNUNET_NO;
  *provider_salt = p->config.provider_salt;
  return GNUNET_OK;
}


/**
 * Lookup @a provider_salt of @a provider_url in @a state.
 *
 * The public API still speaks JSON here, so this is the typed lookup
 * with a parse in front of it.
 *
 * @param state the state to inspect
 * @param provider_url provider to look into
 * @param[out] provider_salt value to extract
 * @return #GNUNET_OK on success
 */
enum GNUNET_GenericReturnValue
ANASTASIS_reducer_lookup_salt (
  const json_t *state,
  const char *provider_url,
  struct ANASTASIS_CRYPTO_ProviderSaltP *provider_salt)
{
  struct ANASTASIS_ReduxState *rs;
  enum GNUNET_GenericReturnValue ret;

  {
    enum TALER_ErrorCode ec;
    const char *detail;

    rs = ANASTASIS_REDUX_state_parse_ (state,
                                       &ec,
                                       &detail);
    if (NULL == rs)
    {
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
  }
  ret = ANASTASIS_REDUX_lookup_salt_ (&rs->common,
                                      provider_url,
                                      provider_salt);
  ANASTASIS_REDUX_state_free_ (rs);
  return ret;
}
