/*
  This file is part of Anastasis
  Copyright (C) 2020, 2021 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_recovery_redux.c
 * @brief anastasis reducer recovery 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 "anastasis_api_redux.h"


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


enum ANASTASIS_RecoveryState
ANASTASIS_recovery_state_from_string_ (const char *state_string)
{
  for (enum ANASTASIS_RecoveryState i = 0;
       i < sizeof (recovery_strings) / sizeof(*recovery_strings);
       i++)
    if (0 == strcmp (state_string,
                     recovery_strings[i]))
      return i;
  return ANASTASIS_RECOVERY_STATE_INVALID;
}


const char *
ANASTASIS_recovery_state_to_string_ (enum ANASTASIS_RecoveryState rs)
{
  if ( (rs < 0) ||
       (rs >= sizeof (recovery_strings) / sizeof(*recovery_strings)) )
  {
    GNUNET_break_op (0);
    return NULL;
  }
  return recovery_strings[rs];
}


static void
set_state (json_t *state,
           enum ANASTASIS_RecoveryState new_recovery_state)
{
  GNUNET_assert (
    0 ==
    json_object_set_new (
      state,
      "recovery_state",
      json_string (ANASTASIS_recovery_state_to_string_ (new_recovery_state))));
}


/**
 * Returns an initial ANASTASIS recovery state.
 *
 * @return NULL on failure
 */
json_t *
ANASTASIS_recovery_start (const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  json_t *initial_state;
  const char *external_reducer = ANASTASIS_REDUX_probe_external_reducer ();

  if (NULL != external_reducer)
  {
    int pipefd_stdout[2];
    pid_t pid = 0;
    int status;
    FILE *reducer_stdout;

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Using external reducer '%s' for recovery start status\n",
                external_reducer);

    GNUNET_assert (0 == pipe (pipefd_stdout));
    pid = fork ();
    if (pid == 0)
    {
      (void) close (pipefd_stdout[0]);
      (void) dup2 (pipefd_stdout[1],
                   STDOUT_FILENO);
      execlp (external_reducer,
              external_reducer,
              "-r",
              NULL);
      GNUNET_assert (0);
    }

    close (pipefd_stdout[1]);
    reducer_stdout = fdopen (pipefd_stdout[0],
                             "r");
    {
      json_error_t err;

      initial_state = json_loadf (reducer_stdout,
                                  0,
                                  &err);

      if (NULL == initial_state)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                    "External reducer did not output valid JSON: %s:%d:%d %s\n",
                    err.source,
                    err.line,
                    err.column,
                    err.text);
        GNUNET_assert (0 == fclose (reducer_stdout));
        waitpid (pid, &status, 0);
        return NULL;
      }
    }

    GNUNET_assert (NULL != initial_state);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Waiting for external reducer to terminate.\n");
    GNUNET_assert (0 == fclose (reducer_stdout));
    reducer_stdout = NULL;
    waitpid (pid, &status, 0);

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "External reducer finished with exit status '%d'\n",
                status);
    return initial_state;
  }

  (void) cfg;
  initial_state = ANASTASIS_REDUX_load_continents_ ();
  if (NULL == initial_state)
    return NULL;
  GNUNET_assert (
    0 ==
    json_object_set_new (initial_state,
                         "reducer_type",
                         json_string ("recovery")));
  set_state (initial_state,
             ANASTASIS_RECOVERY_STATE_CONTINENT_SELECTING);
  return initial_state;
}


/**
 * Context for a "select_challenge" operation.
 */
struct SelectChallengeContext
{
  /**
   * Handle we returned for cancellation of the operation.
   */
  struct ANASTASIS_ReduxAction ra;

  /**
   * UUID of the challenge selected by the user for solving.
   */
  struct ANASTASIS_CRYPTO_TruthUUIDP uuid;

  /**
   * Which timeout was set for the operation?
   */
  struct GNUNET_TIME_Relative timeout;

  /**
   * Overall recovery action.
   */
  struct ANASTASIS_Recovery *r;

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

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

  /**
   * Our state.
   */
  json_t *state;

  /**
   * Our arguments (like answers to the challenge, if already provided).
   */
  json_t *args;

  /**
   * Task scheduled for delayed success reporting. Needed to make
   * sure that the solved challenge was really the final result,
   * cancelled if the solved challenge resulted in the secret being
   * recovered.
   */
  struct GNUNET_SCHEDULER_Task *delayed_report;

  /**
   * Payment secret, if we are in the "pay" state.
   */
  struct ANASTASIS_PaymentSecretP ps;

  /**
   * Application asked us to only poll for existing
   * asynchronous challenges, and not to being a
   * new one.
   */
  bool poll_only;
};


/**
 * Cleanup a select challenge context.
 *
 * @param cls a `struct SelectChallengeContext *`
 */
static void
sctx_free (void *cls)
{
  struct SelectChallengeContext *sctx = cls;

  if (NULL != sctx->r)
  {
    ANASTASIS_recovery_abort (sctx->r);
    sctx->r = NULL;
  }
  json_decref (sctx->state);
  json_decref (sctx->args);
  if (NULL != sctx->delayed_report)
  {
    GNUNET_SCHEDULER_cancel (sctx->delayed_report);
    sctx->delayed_report = NULL;
  }
  GNUNET_free (sctx);
}


/**
 * Call the action callback with an error result
 *
 * @param cb action callback to call
 * @param cb_cls closure for @a cb
 * @param rc error code to translate to JSON
 */
void
fail_by_error (ANASTASIS_ActionCallback cb,
               void *cb_cls,
               enum ANASTASIS_RecoveryStatus rc)
{
  const char *msg = NULL;
  enum TALER_ErrorCode ec = TALER_EC_INVALID;

  switch (rc)
  {
  case ANASTASIS_RS_SUCCESS:
    GNUNET_assert (0);
    break;
  case ANASTASIS_RS_POLICY_DOWNLOAD_FAILED:
    msg = gettext_noop ("download failed due to unexpected network issue");
    ec = TALER_EC_ANASTASIS_REDUCER_NETWORK_FAILED;
    break;
  case ANASTASIS_RS_POLICY_DOWNLOAD_NO_POLICY:
    GNUNET_break (0);
    msg = gettext_noop ("policy document returned was malformed");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED;
    break;
  case ANASTASIS_RS_POLICY_DOWNLOAD_TOO_BIG:
    GNUNET_break (0);
    msg = gettext_noop ("policy document too large for client memory");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED;
    break;
  case ANASTASIS_RS_POLICY_DOWNLOAD_INVALID_COMPRESSION:
    GNUNET_break (0);
    msg = gettext_noop ("failed to decompress policy document");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED;
    break;
  case ANASTASIS_RS_POLICY_DOWNLOAD_NO_JSON:
    GNUNET_break (0);
    msg = gettext_noop ("policy document returned was not in JSON format");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED;
    break;
  case ANASTASIS_RS_POLICY_MALFORMED_JSON:
    GNUNET_break (0);
    msg = gettext_noop (
      "policy document returned was not in required JSON format");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED;
    break;
  case ANASTASIS_RS_POLICY_SERVER_ERROR:
    msg = gettext_noop ("Anastasis server reported transient internal error");
    ec = TALER_EC_ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED;
    break;
  case ANASTASIS_RS_POLICY_GONE:
    msg = gettext_noop ("policy document no longer exists");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED;
    break;
  case ANASTASIS_RS_POLICY_UNKNOWN:
    msg = gettext_noop ("account unknown to Anastasis server");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED;
    break;
  }
  ANASTASIS_redux_fail_ (cb,
                         cb_cls,
                         ec,
                         msg);
}


/**
 * This function is called whenever the recovery process ends.
 * On success, the secret is returned in @a secret.
 *
 * @param cls handle for the callback
 * @param rc error code
 * @param secret contains the core secret which is passed to the user
 * @param secret_size defines the size of the core secret
 */
static void
core_secret_cb (void *cls,
                enum ANASTASIS_RecoveryStatus rc,
                const void *secret,
                size_t secret_size)
{
  struct SelectChallengeContext *sctx = cls;

  sctx->r = NULL;
  if (ANASTASIS_RS_SUCCESS == rc)
  {
    json_t *jsecret;

    jsecret = json_loadb (secret,
                          secret_size,
                          JSON_REJECT_DUPLICATES,
                          NULL);
    if (NULL == jsecret)
    {
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_SECRET_MALFORMED,
                             NULL);
      sctx_free (sctx);
      return;
    }
    GNUNET_assert (0 ==
                   json_object_set_new (sctx->state,
                                        "core_secret",
                                        jsecret));
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_RECOVERY_FINISHED);
    sctx->cb (sctx->cb_cls,
              TALER_EC_NONE,
              sctx->state);
    sctx_free (sctx);
    return;
  }
  fail_by_error (sctx->cb,
                 sctx->cb_cls,
                 rc);
  sctx_free (sctx);
}


/**
 * A challenge was solved, but we are not yet finished.
 * Report to caller that the challenge was completed.
 *
 * @param cls a `struct SelectChallengeContext`
 */
static void
report_solved (void *cls)
{
  struct SelectChallengeContext *sctx = cls;

  sctx->delayed_report = NULL;
  set_state (sctx->state,
             ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
  sctx->cb (sctx->cb_cls,
            TALER_EC_NONE,
            sctx->state);
  sctx_free (sctx);
}


/**
 * Find challenge of @a uuid in @a state under "recovery_information".
 *
 * @param state the state to search
 * @param uuid the UUID to search for
 * @return NULL on error, otherwise challenge entry; RC is NOT incremented
 */
static json_t *
find_challenge_in_ri (json_t *state,
                      const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid)
{
  struct ANASTASIS_CRYPTO_TruthUUIDP u;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("uuid",
                                 &u),
    GNUNET_JSON_spec_end ()
  };
  json_t *ri;
  json_t *challenges;
  json_t *challenge;
  size_t index;

  ri = json_object_get (state,
                        "recovery_information");
  if (NULL == ri)
  {
    GNUNET_break (0);
    return NULL;
  }
  challenges = json_object_get (ri,
                                "challenges");
  if (NULL == challenges)
  {
    GNUNET_break (0);
    return NULL;
  }
  json_array_foreach (challenges, index, challenge)
  {
    if (GNUNET_OK !=
        GNUNET_JSON_parse (challenge,
                           spec,
                           NULL, NULL))
    {
      GNUNET_break (0);
      return NULL;
    }
    if (0 ==
        GNUNET_memcmp (&u,
                       uuid))
    {
      return challenge;
    }
  }
  return NULL;
}


/**
 * Find challenge of @a uuid in @a state under "challenges".
 *
 * @param state the state to search
 * @param uuid the UUID to search for
 * @return NULL on error, otherwise challenge entry; RC is NOT incremented
 */
static json_t *
find_challenge_in_cs (json_t *state,
                      const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid)
{
  json_t *rd = json_object_get (state,
                                "recovery_document");
  json_t *cs = json_object_get (rd,
                                "challenges");
  json_t *c;
  size_t off;

  json_array_foreach (cs, off, c)
  {
    struct ANASTASIS_CRYPTO_TruthUUIDP u;
    struct GNUNET_JSON_Specification spec[] = {
      GNUNET_JSON_spec_fixed_auto ("uuid",
                                   &u),
      GNUNET_JSON_spec_end ()
    };

    if (GNUNET_OK !=
        GNUNET_JSON_parse (c,
                           spec,
                           NULL, NULL))
    {
      GNUNET_break (0);
      continue;
    }
    if (0 !=
        GNUNET_memcmp (uuid,
                       &u))
      continue;
    return c;
  }
  return NULL;
}


/**
 * Defines a callback for the response status for a challenge start
 * operation.
 *
 * @param cls a `struct SelectChallengeContext *`
 * @param csr response details
 */
static void
start_feedback_cb (
  void *cls,
  const struct ANASTASIS_ChallengeStartResponse *csr)
{
  struct SelectChallengeContext *sctx = cls;
  const struct ANASTASIS_ChallengeDetails *cd;
  char uuid[sizeof (cd->uuid) * 2];
  char *end;
  json_t *feedback;

  cd = ANASTASIS_challenge_get_details (csr->challenge);
  end = GNUNET_STRINGS_data_to_string (&cd->uuid,
                                       sizeof (cd->uuid),
                                       uuid,
                                       sizeof (uuid));
  GNUNET_assert (NULL != end);
  *end = '\0';
  feedback = json_object_get (sctx->state,
                              "challenge_feedback");
  if (NULL == feedback)
  {
    feedback = json_object ();
    GNUNET_assert (0 ==
                   json_object_set_new (sctx->state,
                                        "challenge_feedback",
                                        feedback));
  }
  switch (csr->cs)
  {
  case ANASTASIS_CHALLENGE_START_STATUS_FILENAME_PROVIDED:
    {
      json_t *instructions;
      char *hint;

      GNUNET_asprintf (&hint,
                       _ ("Required TAN can be found in `%s'"),
                       csr->details.tan_filename);
      instructions = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "code-in-file"),
        GNUNET_JSON_pack_string ("filename",
                                 csr->details.tan_filename),
        GNUNET_JSON_pack_string ("display_hint",
                                 hint));
      GNUNET_free (hint);
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          instructions));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_NONE,
              sctx->state);
    sctx_free (sctx);
    return;
  case ANASTASIS_CHALLENGE_START_STATUS_TAN_SENT_HINT_PROVIDED:
    {
      json_t *instructions;
      char *hint;

      GNUNET_asprintf (&hint,
                       _ ("TAN code was sent to `%s'"),
                       csr->details.tan_address_hint);
      instructions = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "send-to-address"),
        GNUNET_JSON_pack_string ("address_hint",
                                 csr->details.tan_address_hint),
        GNUNET_JSON_pack_string ("display_hint",
                                 hint));
      GNUNET_free (hint);
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          instructions));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_NONE,
              sctx->state);
    sctx_free (sctx);
    return;

  case ANASTASIS_CHALLENGE_START_STATUS_TAN_ALREADY_SENT:
    {
      json_t *instructions;
      char *hint;

      GNUNET_asprintf (&hint,
                       _ ("TAN code already sent."));
      instructions = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "send-to-address"),
        GNUNET_JSON_pack_string ("display_hint",
                                 hint));
      GNUNET_free (hint);
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          instructions));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_NONE,
              sctx->state);
    sctx_free (sctx);
    return;

  case ANASTASIS_CHALLENGE_START_STATUS_PAYMENT_REQUIRED:
    {
      json_t *pay;
      char *hint;

      GNUNET_asprintf (&hint,
                       _ ("Taler payment to `%s' required"),
                       csr->details.payment_required.taler_pay_uri);
      pay = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "taler-payment"),
        GNUNET_JSON_pack_string (
          "taler_pay_uri",
          csr->details.payment_required.taler_pay_uri),
        GNUNET_JSON_pack_string ("provider",
                                 cd->provider_url),
        GNUNET_JSON_pack_string ("display_hint",
                                 hint),
        GNUNET_JSON_pack_data_auto (
          "payment_secret",
          &csr->details.payment_required.payment_secret));
      GNUNET_free (hint);
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          pay));
    }
    /* Remember payment secret for later (once application claims it paid) */
    {
      json_t *challenge = find_challenge_in_ri (sctx->state,
                                                &cd->uuid);

      GNUNET_assert (NULL != challenge);
      GNUNET_assert (0 ==
                     json_object_set_new (
                       challenge,
                       "payment_secret",
                       GNUNET_JSON_from_data_auto (
                         &csr->details.payment_required.payment_secret)));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_NONE,
              sctx->state);
    sctx_free (sctx);
    return;
  case ANASTASIS_CHALLENGE_START_STATUS_SERVER_FAILURE:
    {
      json_t *err;

      err = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "server-failure"),
        GNUNET_JSON_pack_uint64 ("http_status",
                                 csr->http_status),
        GNUNET_JSON_pack_uint64 ("error_code",
                                 csr->ec));
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          err));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx->cb (sctx->cb_cls,
              csr->ec,
              sctx->state);
    sctx_free (sctx);
    return;
  case ANASTASIS_CHALLENGE_START_STATUS_TRUTH_UNKNOWN:
    {
      json_t *err;

      err = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "truth-unknown"),
        GNUNET_JSON_pack_uint64 ("http_status",
                                 csr->http_status),
        GNUNET_JSON_pack_uint64 ("error_code",
                                 TALER_EC_ANASTASIS_TRUTH_UNKNOWN));
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          err));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_ANASTASIS_TRUTH_UNKNOWN,
              sctx->state);
    sctx_free (sctx);
    return;
  case ANASTASIS_CHALLENGE_START_STATUS_BANK_TRANSFER_REQUIRED:
    {
      json_t *reply;
      json_t *c;
      char *hint;

      c = find_challenge_in_cs (sctx->state,
                                &cd->uuid);
      if (NULL == c)
      {
        GNUNET_break (0);
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                               NULL);
        sctx_free (sctx);
        return;
      }
      GNUNET_assert (0 ==
                     json_object_set_new (c,
                                          "async",
                                          json_true ()));
      GNUNET_assert (
        0 ==
        json_object_set_new (
          c,
          "answer-pin",
          json_integer (
            csr->details.bank_transfer_required.answer_code)));
      GNUNET_asprintf (&hint,
                       _ ("Wire %s to %s (%s) with subject %s\n"),
                       TALER_amount2s (
                         &csr->details.bank_transfer_required.amount),
                       csr->details.bank_transfer_required.target_iban,
                       csr->details.bank_transfer_required.target_business_name,
                       csr->details.bank_transfer_required.wire_transfer_subject);
      reply = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "iban-instructions"),
        GNUNET_JSON_pack_string (
          "target_iban",
          csr->details.bank_transfer_required.target_iban),
        GNUNET_JSON_pack_string (
          "display_hint",
          hint),
        GNUNET_JSON_pack_string (
          "target_business_name",
          csr->details.bank_transfer_required.target_business_name),
        GNUNET_JSON_pack_string (
          "wire_transfer_subject",
          csr->details.bank_transfer_required.wire_transfer_subject),
        TALER_JSON_pack_amount (
          "challenge_amount",
          &csr->details.bank_transfer_required.amount));
      GNUNET_free (hint);
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          reply));
    }
    GNUNET_assert (0 ==
                   json_object_set_new (sctx->state,
                                        "selected_challenge_uuid",
                                        GNUNET_JSON_from_data_auto (
                                          &cd->uuid)));
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_NONE,
              sctx->state);
    sctx_free (sctx);
    return;
  }
  GNUNET_break (0);
  ANASTASIS_redux_fail_ (sctx->cb,
                         sctx->cb_cls,
                         TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                         NULL);
  sctx_free (sctx);
}


/**
 * Defines a callback for the response status for a challenge answer
 * operation.
 *
 * @param cls a `struct SelectChallengeContext *`
 * @param csr response details
 */
static void
answer_feedback_cb (
  void *cls,
  const struct ANASTASIS_ChallengeAnswerResponse *csr)
{
  struct SelectChallengeContext *sctx = cls;
  const struct ANASTASIS_ChallengeDetails *cd;
  char uuid[sizeof (cd->uuid) * 2];
  char *end;
  json_t *feedback;

  cd = ANASTASIS_challenge_get_details (csr->challenge);
  end = GNUNET_STRINGS_data_to_string (&cd->uuid,
                                       sizeof (cd->uuid),
                                       uuid,
                                       sizeof (uuid));
  GNUNET_assert (NULL != end);
  *end = '\0';
  feedback = json_object_get (sctx->state,
                              "challenge_feedback");
  if (NULL == feedback)
  {
    feedback = json_object ();
    GNUNET_assert (0 ==
                   json_object_set_new (sctx->state,
                                        "challenge_feedback",
                                        feedback));
  }
  switch (csr->cs)
  {
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_SOLVED:
    {
      json_t *rd;

      rd = ANASTASIS_recovery_serialize (sctx->r);
      if (NULL == rd)
      {
        GNUNET_break (0);
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                               "unable to serialize recovery state");
        sctx_free (sctx);
        return;
      }
      GNUNET_assert (0 ==
                     json_object_set_new (sctx->state,
                                          "recovery_document",
                                          rd));
    }
    {
      json_t *solved;

      solved = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "solved"));
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          solved));
    }
    /* Delay reporting challenge success, as we MAY still
       also see a secret recovery success (and we can only
       call the callback once) */
    sctx->delayed_report = GNUNET_SCHEDULER_add_now (&report_solved,
                                                     sctx);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_INVALID_ANSWER:
    {
      json_t *instructions;

      instructions = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "incorrect-answer"),
        GNUNET_JSON_pack_uint64 ("error_code",
                                 csr->ec));
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          instructions));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_NONE,
              sctx->state);
    sctx_free (sctx);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_PAYMENT_REQUIRED:
    {
      json_t *pay;
      char *hint;

      GNUNET_asprintf (&hint,
                       _ ("Taler payment to `%s' required"),
                       csr->details.payment_required.taler_pay_uri);
      pay = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "taler-payment"),
        GNUNET_JSON_pack_string (
          "taler_pay_uri",
          csr->details.payment_required.taler_pay_uri),
        GNUNET_JSON_pack_string (
          "display_hint",
          hint),
        GNUNET_JSON_pack_string ("provider",
                                 cd->provider_url),
        GNUNET_JSON_pack_data_auto (
          "payment_secret",
          &csr->details.payment_required.payment_secret));
      GNUNET_free (hint);
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          pay));
    }
    /* Remember payment secret for later (once application claims it paid) */
    {
      json_t *challenge = find_challenge_in_ri (sctx->state,
                                                &cd->uuid);

      GNUNET_assert (NULL != challenge);
      GNUNET_assert (0 ==
                     json_object_set_new (
                       challenge,
                       "payment_secret",
                       GNUNET_JSON_from_data_auto (
                         &csr->details.payment_required.payment_secret)));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_NONE,
              sctx->state);
    sctx_free (sctx);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_SERVER_FAILURE:
    {
      json_t *err;

      err = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "server-failure"),
        GNUNET_JSON_pack_uint64 ("http_status",
                                 csr->http_status),
        GNUNET_JSON_pack_uint64 ("error_code",
                                 csr->ec));
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          err));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx->cb (sctx->cb_cls,
              csr->ec,
              sctx->state);
    sctx_free (sctx);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_TRUTH_UNKNOWN:
    {
      json_t *err;

      err = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("state",
                                 "truth-unknown"),
        GNUNET_JSON_pack_uint64 ("http_status",
                                 csr->http_status),
        GNUNET_JSON_pack_uint64 ("error_code",
                                 TALER_EC_ANASTASIS_TRUTH_UNKNOWN));
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          err));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_ANASTASIS_TRUTH_UNKNOWN,
              sctx->state);
    sctx_free (sctx);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_RATE_LIMIT_EXCEEDED:
    {
      json_t *err;
      char *hint;

      GNUNET_asprintf (
        &hint,
        _ ("exceeded limit of %llu attempts in %s"),
        (unsigned long long) csr->details.rate_limit_exceeded.request_limit,
        GNUNET_TIME_relative2s (
          csr->details.rate_limit_exceeded.request_frequency,
          true));
      err = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string (
          "state",
          "rate-limit-exceeded"),
        GNUNET_JSON_pack_string (
          "display_hint",
          hint),
        GNUNET_JSON_pack_uint64 (
          "request_limit",
          csr->details.rate_limit_exceeded.request_limit),
        GNUNET_JSON_pack_time_rel (
          "request_frequency",
          csr->details.rate_limit_exceeded.request_frequency),
        GNUNET_JSON_pack_uint64 (
          "error_code",
          TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED));
      GNUNET_free (hint);
      GNUNET_assert (0 ==
                     json_object_set_new (feedback,
                                          uuid,
                                          err));
    }
    set_state (sctx->state,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx->cb (sctx->cb_cls,
              TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED,
              sctx->state);
    sctx_free (sctx);
    return;
  }
  GNUNET_break (0);
  ANASTASIS_redux_fail_ (sctx->cb,
                         sctx->cb_cls,
                         TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                         NULL);
  sctx_free (sctx);
}


/**
 * Callback which passes back the recovery document and its possible
 * policies. Also passes back the version of the document for the user
 * to check.
 *
 * We find the selected challenge and try to answer it (or begin
 * the process).
 *
 * @param cls a `struct SelectChallengeContext *`
 * @param ri recovery information struct which contains the policies
 */
static void
solve_challenge_cb (void *cls,
                    const struct ANASTASIS_RecoveryInformation *ri)
{
  struct SelectChallengeContext *sctx = cls;
  const struct ANASTASIS_PaymentSecretP *psp = NULL;
  struct ANASTASIS_PaymentSecretP ps;
  struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO;
  struct GNUNET_JSON_Specification tspec[] = {
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("timeout",
                                      &timeout),
      NULL),
    GNUNET_JSON_spec_end ()
  };
  struct GNUNET_JSON_Specification pspec[] = {
    GNUNET_JSON_spec_fixed_auto ("payment_secret",
                                 &ps),
    GNUNET_JSON_spec_end ()
  };
  json_t *challenge;

  if (NULL == ri)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (sctx->cb,
                           sctx->cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "recovery information could not be deserialized");
    sctx_free (sctx);
    return;
  }

  if ( (NULL != sctx->args) &&
       (GNUNET_OK !=
        GNUNET_JSON_parse (sctx->args,
                           tspec,
                           NULL, NULL)) )
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (sctx->cb,
                           sctx->cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'timeout' malformed");
    sctx_free (sctx);
    return;
  }

  /* resume all async, unsolved challenges */
  {
    bool poll_started = false;

    for (unsigned int i = 0; i<ri->cs_len; i++)
    {
      struct ANASTASIS_Challenge *ci = ri->cs[i];
      const struct ANASTASIS_ChallengeDetails *cd;
      json_t *challenge;
      json_t *pin;

      cd = ANASTASIS_challenge_get_details (ci);
      if (cd->solved ||
          (! cd->async) )
        continue;

      challenge = find_challenge_in_cs (sctx->state,
                                        &cd->uuid);
      if (NULL == challenge)
      {
        GNUNET_break_op (0);
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                               "challenge not found");
        sctx_free (sctx);
        return;
      }
      pin = json_object_get (challenge,
                             "answer-pin");
      if (! json_is_integer (pin))
      {
        GNUNET_break_op (0);
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                               "async challenge 'answer-pin' not found");
        sctx_free (sctx);
        return;
      }
      if (GNUNET_OK !=
          ANASTASIS_challenge_answer2 (ci,
                                       psp,
                                       timeout,
                                       json_integer_value (pin),
                                       &answer_feedback_cb,
                                       sctx))
      {
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                               "Failed to begin answering asynchronous challenge");
        sctx_free (sctx);
        return;
      }
      poll_started = true;
    }

    if (sctx->poll_only)
    {
      if (! poll_started)
      {
        GNUNET_break_op (0);
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID,
                               "no challenge available for polling");
        return;
      }
      /* only polling, do not start new challenges */
      return;
    }
  } /* end resuming async challenges */

  /* Check if we got a payment_secret */
  challenge = find_challenge_in_ri (sctx->state,
                                    &sctx->uuid);
  if (NULL == challenge)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (sctx->cb,
                           sctx->cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "challenge not found");
    sctx_free (sctx);
    return;
  }

  if (NULL !=
      json_object_get (sctx->args,
                       "payment_secret"))
  {
    /* check if we got payment secret in args */
    if (GNUNET_OK !=
        GNUNET_JSON_parse (sctx->args,
                           pspec,
                           NULL, NULL))
    {
      GNUNET_break_op (0);
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                             "'payment_secret' malformed");
      sctx_free (sctx);
      return;
    }
    psp = &ps;
  }
  else if (NULL !=
           json_object_get (challenge,
                            "payment_secret"))
  {
    if (GNUNET_OK !=
        GNUNET_JSON_parse (challenge,
                           pspec,
                           NULL, NULL))
    {
      GNUNET_break_op (0);
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                             "'payment_secret' malformed");
      sctx_free (sctx);
      return;
    }
    psp = &ps;
  }

  /* start or solve selected challenge */
  for (unsigned int i = 0; i<ri->cs_len; i++)
  {
    struct ANASTASIS_Challenge *ci = ri->cs[i];
    const struct ANASTASIS_ChallengeDetails *cd;
    int ret;
    json_t *c;

    cd = ANASTASIS_challenge_get_details (ci);
    if (cd->async)
      continue; /* handled above */
    if (0 !=
        GNUNET_memcmp (&sctx->uuid,
                       &cd->uuid))
      continue;
    if (cd->solved)
    {
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                             "Selected challenge already solved");
      sctx_free (sctx);
      return;
    }
    c = find_challenge_in_cs (sctx->state,
                              &cd->uuid);
    GNUNET_assert (NULL != c);
    if (0 == strcmp ("question",
                     cd->type))
    {
      /* security question, answer must be a string */
      json_t *janswer = json_object_get (sctx->args,
                                         "answer");
      const char *answer = json_string_value (janswer);

      if (NULL == answer)
      {
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                               "'answer' missing");
        sctx_free (sctx);
        return;
      }
      /* persist answer, in case payment is required */
      GNUNET_assert (0 ==
                     json_object_set (c,
                                      "answer",
                                      janswer));
      ret = ANASTASIS_challenge_answer (ci,
                                        psp,
                                        timeout,
                                        answer,
                                        &answer_feedback_cb,
                                        sctx);
    }
    else
    {
      /* Check if we got a PIN or a HASH */
      json_t *pin = json_object_get (sctx->args,
                                     "pin");
      json_t *hash = json_object_get (sctx->args,
                                      "hash");
      if (json_is_integer (pin))
      {
        uint64_t ianswer = json_integer_value (pin);

        /* persist answer, in case async processing
           happens via poll */
        GNUNET_assert (0 ==
                       json_object_set (c,
                                        "answer-pin",
                                        pin));
        ret = ANASTASIS_challenge_answer2 (ci,
                                           psp,
                                           timeout,
                                           ianswer,
                                           &answer_feedback_cb,
                                           sctx);
      }
      else if (NULL != hash)
      {
        struct GNUNET_HashCode hashed_answer;
        struct GNUNET_JSON_Specification spec[] = {
          GNUNET_JSON_spec_fixed_auto ("hash",
                                       &hashed_answer),
          GNUNET_JSON_spec_end ()
        };

        if (GNUNET_OK !=
            GNUNET_JSON_parse (sctx->args,
                               spec,
                               NULL, NULL))
        {
          ANASTASIS_redux_fail_ (sctx->cb,
                                 sctx->cb_cls,
                                 TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                                 "'answer' malformed");
          sctx_free (sctx);
          return;
        }
        ret = ANASTASIS_challenge_answer3 (ci,
                                           psp,
                                           timeout,
                                           &hashed_answer,
                                           &answer_feedback_cb,
                                           sctx);
      }
      else
      {
        /* no answer provided */
        ret = ANASTASIS_challenge_start (ci,
                                         psp,
                                         &start_feedback_cb,
                                         sctx);
      }
    }
    if (GNUNET_OK != ret)
    {
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                             "Failed to begin answering challenge");
      sctx_free (sctx);
      return;
    }
    return;   /* await answer feedback */
  }
  ANASTASIS_redux_fail_ (sctx->cb,
                         sctx->cb_cls,
                         TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                         "'uuid' not in list of challenges");
  sctx_free (sctx);
}


/**
 * Callback which passes back the recovery document and its possible
 * policies. Also passes back the version of the document for the user
 * to check.
 *
 * We find the selected challenge and try to answer it (or begin
 * the process).
 *
 * @param cls a `struct SelectChallengeContext *`
 * @param ri recovery information struct which contains the policies
 */
static void
pay_challenge_cb (void *cls,
                  const struct ANASTASIS_RecoveryInformation *ri)
{
  struct SelectChallengeContext *sctx = cls;
  json_t *challenge;

  if (NULL == ri)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (sctx->cb,
                           sctx->cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "recovery information could not be deserialized");
    sctx_free (sctx);
    return;
  }

  challenge = find_challenge_in_ri (sctx->state,
                                    &sctx->uuid);
  if (NULL == challenge)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (sctx->cb,
                           sctx->cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "challenge not found");
    sctx_free (sctx);
    return;
  }
  /* persist payment, in case we need to run the request again */
  GNUNET_assert (
    0 ==
    json_object_set_new (challenge,
                         "payment_secret",
                         GNUNET_JSON_from_data_auto (&sctx->ps)));

  for (unsigned int i = 0; i<ri->cs_len; i++)
  {
    struct ANASTASIS_Challenge *ci = ri->cs[i];
    const struct ANASTASIS_ChallengeDetails *cd;
    int ret;

    cd = ANASTASIS_challenge_get_details (ci);
    if (0 !=
        GNUNET_memcmp (&sctx->uuid,
                       &cd->uuid))
      continue;
    if (cd->solved)
    {
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                             "Selected challenge already solved");
      sctx_free (sctx);
      return;
    }

    if (0 == strcmp ("question",
                     cd->type))
    {
      /* security question, answer must be a string and already ready */
      json_t *janswer = json_object_get (challenge,
                                         "answer");
      const char *answer = json_string_value (janswer);

      if (NULL == answer)
      {
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                               "'answer' missing");
        sctx_free (sctx);
        return;
      }
      ret = ANASTASIS_challenge_answer (ci,
                                        &sctx->ps,
                                        sctx->timeout,
                                        answer,
                                        &answer_feedback_cb,
                                        sctx);
    }
    else
    {
      ret = ANASTASIS_challenge_start (ci,
                                       &sctx->ps,
                                       &start_feedback_cb,
                                       sctx);
    }
    if (GNUNET_OK != ret)
    {
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                             "Failed to begin answering challenge");
      sctx_free (sctx);
      return;
    }
    return;   /* await answer feedback */
  }
  ANASTASIS_redux_fail_ (sctx->cb,
                         sctx->cb_cls,
                         TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                         "'uuid' not in list of challenges");
  sctx_free (sctx);
}


/**
 * The user selected a challenge to be solved. Begin the solving
 * process.
 *
 * @param[in] state we are in
 * @param arguments our arguments with the solution
 * @param cb functiont o call with the new state
 * @param cb_cls closure for @a cb
 * @return handle to cancel challenge selection step
 */
static struct ANASTASIS_ReduxAction *
solve_challenge (json_t *state,
                 const json_t *arguments,
                 ANASTASIS_ActionCallback cb,
                 void *cb_cls)
{
  struct SelectChallengeContext *sctx
    = GNUNET_new (struct SelectChallengeContext);
  json_t *rd;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid",
                                 &sctx->uuid),
    GNUNET_JSON_spec_end ()
  };

  if (NULL == arguments)
  {
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return NULL;
  }
  if (GNUNET_OK !=
      GNUNET_JSON_parse (state,
                         spec,
                         NULL, NULL))
  {
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'selected_challenge_uuid' missing");
    return NULL;
  }
  rd = json_object_get (state,
                        "recovery_document");
  if (NULL == rd)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "solve_challenge");
    return NULL;
  }
  sctx->cb = cb;
  sctx->cb_cls = cb_cls;
  sctx->state = json_incref (state);
  sctx->args = json_incref ((json_t*) arguments);
  sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_,
                                            rd,
                                            &solve_challenge_cb,
                                            sctx,
                                            &core_secret_cb,
                                            sctx);
  if (NULL == sctx->r)
  {
    json_decref (sctx->state);
    json_decref (sctx->args);
    GNUNET_free (sctx);
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'recovery_document' invalid");
    return NULL;
  }
  sctx->ra.cleanup = &sctx_free;
  sctx->ra.cleanup_cls = sctx;
  return &sctx->ra;
}


/**
 * The user asked for us to poll on pending
 * asynchronous challenges to see if they have
 * now completed / been satisfied.
 *
 * @param[in] state we are in
 * @param arguments our arguments with the solution
 * @param cb functiont o call with the new state
 * @param cb_cls closure for @a cb
 * @return handle to cancel challenge selection step
 */
static struct ANASTASIS_ReduxAction *
poll_challenges (json_t *state,
                 const json_t *arguments,
                 ANASTASIS_ActionCallback cb,
                 void *cb_cls)
{
  struct SelectChallengeContext *sctx
    = GNUNET_new (struct SelectChallengeContext);
  json_t *rd;

  rd = json_object_get (state,
                        "recovery_document");
  if (NULL == rd)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "poll_challenges");
    return NULL;
  }
  sctx->poll_only = true;
  sctx->cb = cb;
  sctx->cb_cls = cb_cls;
  sctx->state = json_incref (state);
  sctx->args = json_incref ((json_t*) arguments);
  sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_,
                                            rd,
                                            &solve_challenge_cb,
                                            sctx,
                                            &core_secret_cb,
                                            sctx);
  if (NULL == sctx->r)
  {
    json_decref (sctx->state);
    json_decref (sctx->args);
    GNUNET_free (sctx);
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'recovery_document' invalid");
    return NULL;
  }
  sctx->ra.cleanup = &sctx_free;
  sctx->ra.cleanup_cls = sctx;
  return &sctx->ra;
}


/**
 * The user selected a challenge to be solved. Handle the payment
 * process.
 *
 * @param[in] state we are in
 * @param arguments our arguments with the solution
 * @param cb functiont o call with the new state
 * @param cb_cls closure for @a cb
 * @return handle to cancel challenge selection step
 */
static struct ANASTASIS_ReduxAction *
pay_challenge (json_t *state,
               const json_t *arguments,
               ANASTASIS_ActionCallback cb,
               void *cb_cls)
{
  struct SelectChallengeContext *sctx
    = GNUNET_new (struct SelectChallengeContext);
  json_t *rd;
  struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("selected_challenge_uuid",
                                 &sctx->uuid),
    GNUNET_JSON_spec_end ()
  };
  struct GNUNET_JSON_Specification aspec[] = {
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("timeout",
                                      &timeout),
      NULL),
    GNUNET_JSON_spec_fixed_auto ("payment_secret",
                                 &sctx->ps),
    GNUNET_JSON_spec_end ()
  };

  if (NULL == arguments)
  {
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return NULL;
  }
  if (GNUNET_OK !=
      GNUNET_JSON_parse (arguments,
                         aspec,
                         NULL, NULL))
  {
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'payment_secret' missing");
    return NULL;
  }
  if (GNUNET_OK !=
      GNUNET_JSON_parse (state,
                         spec,
                         NULL, NULL))
  {
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'selected_challenge_uuid' missing");
    return NULL;
  }
  rd = json_object_get (state,
                        "recovery_document");
  if (NULL == rd)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "pay_challenge");
    return NULL;
  }
  sctx->timeout = timeout;
  sctx->cb = cb;
  sctx->cb_cls = cb_cls;
  sctx->state = json_incref (state);
  sctx->args = json_incref ((json_t*) arguments);
  sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_,
                                            rd,
                                            &pay_challenge_cb,
                                            sctx,
                                            &core_secret_cb,
                                            sctx);
  if (NULL == sctx->r)
  {
    json_decref (sctx->state);
    json_decref (sctx->args);
    GNUNET_free (sctx);
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'recovery_document' invalid");
    return NULL;
  }
  sctx->ra.cleanup = &sctx_free;
  sctx->ra.cleanup_cls = sctx;
  return &sctx->ra;
}


/**
 * Callback which passes back the recovery document and its possible
 * policies. Also passes back the version of the document for the user
 * to check.
 *
 * We find the selected challenge and try to answer it (or begin
 * the process).
 *
 * @param cls a `struct SelectChallengeContext *`
 * @param ri recovery information struct which contains the policies
 */
static void
select_challenge_cb (void *cls,
                     const struct ANASTASIS_RecoveryInformation *ri)
{
  struct SelectChallengeContext *sctx = cls;
  const struct ANASTASIS_PaymentSecretP *psp = NULL;
  struct ANASTASIS_PaymentSecretP ps;
  struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO;
  struct GNUNET_JSON_Specification tspec[] = {
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("timeout",
                                      &timeout),
      NULL),
    GNUNET_JSON_spec_end ()
  };
  struct GNUNET_JSON_Specification pspec[] = {
    GNUNET_JSON_spec_fixed_auto ("payment_secret",
                                 &ps),
    GNUNET_JSON_spec_end ()
  };


  if (NULL == ri)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (sctx->cb,
                           sctx->cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "recovery information could not be deserialized");
    sctx_free (sctx);
    return;
  }

  if (GNUNET_OK !=
      GNUNET_JSON_parse (sctx->args,
                         tspec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (sctx->cb,
                           sctx->cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'timeout' malformed");
    sctx_free (sctx);
    return;
  }

  /* NOTE: do we need both ways to pass payment secrets? */
  if (NULL !=
      json_object_get (sctx->args,
                       "payment_secret"))
  {
    /* check if we got payment secret in args */
    if (GNUNET_OK !=
        GNUNET_JSON_parse (sctx->args,
                           pspec,
                           NULL, NULL))
    {
      GNUNET_break_op (0);
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                             "'payment_secret' malformed");
      sctx_free (sctx);
      return;
    }
    psp = &ps;
  }
  else
  {
    /* Check if we got a payment_secret in state */
    json_t *challenge = find_challenge_in_ri (sctx->state,
                                              &sctx->uuid);

    if (NULL == challenge)
    {
      GNUNET_break_op (0);
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                             "challenge not found");
      sctx_free (sctx);
      return;
    }
    if (NULL !=
        json_object_get (challenge,
                         "payment_secret"))
    {
      if (GNUNET_OK !=
          GNUNET_JSON_parse (challenge,
                             pspec,
                             NULL, NULL))
      {
        GNUNET_break_op (0);
        ANASTASIS_redux_fail_ (sctx->cb,
                               sctx->cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                               "'payment_secret' malformed");
        sctx_free (sctx);
        return;
      }
      psp = &ps;
    }
  }

  for (unsigned int i = 0; i<ri->cs_len; i++)
  {
    struct ANASTASIS_Challenge *ci = ri->cs[i];
    const struct ANASTASIS_ChallengeDetails *cd;
    int ret;

    cd = ANASTASIS_challenge_get_details (ci);
    if (0 !=
        GNUNET_memcmp (&sctx->uuid,
                       &cd->uuid))
      continue;
    if (cd->solved)
    {
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                             "Selected challenge already solved");
      sctx_free (sctx);
      return;
    }
    GNUNET_assert (
      0 ==
      json_object_set_new (sctx->state,
                           "selected_challenge_uuid",
                           GNUNET_JSON_from_data_auto (&cd->uuid)));
    if ( (0 == strcmp ("question",
                       cd->type)) ||
         (0 == strcmp ("totp",
                       cd->type)) )
    {
      /* security question or TOTP:
         immediately request user to answer it */
      set_state (sctx->state,
                 ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
      sctx->cb (sctx->cb_cls,
                TALER_EC_NONE,
                sctx->state);
      sctx_free (sctx);
      return;
    }
    /* trigger challenge */
    {
      json_t *c = find_challenge_in_cs (sctx->state,
                                        &cd->uuid);
      json_t *pin = json_object_get (c,
                                     "answer-pin");

      if (NULL != pin)
      {
        uint64_t ianswer = json_integer_value (pin);

        ret = ANASTASIS_challenge_answer2 (ci,
                                           psp,
                                           timeout,
                                           ianswer,
                                           &answer_feedback_cb,
                                           sctx);
      }
      else
      {
        ret = ANASTASIS_challenge_start (ci,
                                         psp,
                                         &start_feedback_cb,
                                         sctx);
      }
    }
    if (GNUNET_OK != ret)
    {
      ANASTASIS_redux_fail_ (sctx->cb,
                             sctx->cb_cls,
                             TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                             "Failed to begin answering challenge");
      sctx_free (sctx);
      return;
    }
    return;   /* await answer feedback */
  }
  ANASTASIS_redux_fail_ (sctx->cb,
                         sctx->cb_cls,
                         TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                         "'uuid' not in list of challenges");
  sctx_free (sctx);
}


/**
 * The user selected a challenge to be solved. Begin the solving
 * process.
 *
 * @param[in] state we are in
 * @param arguments our arguments with the solution
 * @param cb functiont o call with the new state
 * @param cb_cls closure for @a cb
 * @return handle to cancel challenge selection step
 */
static struct ANASTASIS_ReduxAction *
select_challenge (json_t *state,
                  const json_t *arguments,
                  ANASTASIS_ActionCallback cb,
                  void *cb_cls)
{
  struct SelectChallengeContext *sctx
    = GNUNET_new (struct SelectChallengeContext);
  json_t *rd;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("uuid",
                                 &sctx->uuid),
    GNUNET_JSON_spec_end ()
  };

  if (NULL == arguments)
  {
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return NULL;
  }
  if (GNUNET_OK !=
      GNUNET_JSON_parse (arguments,
                         spec,
                         NULL, NULL))
  {
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'uuid' missing");
    return NULL;
  }
  rd = json_object_get (state,
                        "recovery_document");
  if (NULL == rd)
  {
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "select_challenge");
    return NULL;
  }
  sctx->cb = cb;
  sctx->cb_cls = cb_cls;
  sctx->state = json_incref (state);
  sctx->args = json_incref ((json_t*) arguments);
  sctx->r = ANASTASIS_recovery_deserialize (ANASTASIS_REDUX_ctx_,
                                            rd,
                                            &select_challenge_cb,
                                            sctx,
                                            &core_secret_cb,
                                            sctx);
  if (NULL == sctx->r)
  {
    json_decref (sctx->state);
    json_decref (sctx->args);
    GNUNET_free (sctx);
    GNUNET_break_op (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'recovery_document' invalid");
    return NULL;
  }
  sctx->ra.cleanup = &sctx_free;
  sctx->ra.cleanup_cls = sctx;
  return &sctx->ra;
}


/**
 * The user pressed "back" during challenge solving.
 * Transition back to selecting another challenge.
 *
 * @param[in] state we are in
 * @param arguments our arguments (unused)
 * @param cb functiont o call with the new state
 * @param cb_cls closure for @a cb
 * @return NULL (synchronous operation)
 */
static struct ANASTASIS_ReduxAction *
back_challenge_solving (json_t *state,
                        const json_t *arguments,
                        ANASTASIS_ActionCallback cb,
                        void *cb_cls)
{
  (void) arguments;
  GNUNET_assert (0 ==
                 json_object_del (state,
                                  "selected_challenge_uuid"));
  set_state (state,
             ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
  cb (cb_cls,
      TALER_EC_NONE,
      state);
  return NULL;
}


/**
 * State for a "policy download" as part of a recovery operation.
 */
struct PolicyDownloadEntry
{

  /**
   * Redux action handle associated with this state.
   */
  struct ANASTASIS_ReduxAction ra;

  /**
   * Backend we are querying.
   */
  char *backend_url;

  /**
   * The /policy GET operation handle.
   */
  struct ANASTASIS_Recovery *recovery;

  /**
   * Function to call with the result.
   */
  ANASTASIS_ActionCallback cb;

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

  /**
   * State we are using.
   */
  json_t *state;

};


/**
 * Free @a cls data structure.
 *
 * @param[in] cls data structure to free, must be a `struct PolicyDownloadEntry *`
 */
static void
free_pd (void *cls)
{
  struct PolicyDownloadEntry *pd = cls;

  if (NULL != pd->recovery)
  {
    ANASTASIS_recovery_abort (pd->recovery);
    pd->recovery = NULL;
  }
  GNUNET_free (pd->backend_url);
  json_decref (pd->state);
  GNUNET_free (pd);
}


/**
 * We failed to download a policy. Show an error to the user and
 * allow the user to specify alternative providers and/or policy
 * versions.
 *
 * @param[in] pd state to fail with the policy download
 * @param offline true of the reason to show is that all providers
 *        were offline / did not return a salt to us
 */
static void
return_no_policy (struct PolicyDownloadEntry *pd,
                  bool offline)
{
  enum TALER_ErrorCode ec = TALER_EC_ANASTASIS_REDUCER_NETWORK_FAILED;
  const char *detail = (offline)
                       ? "could not contact provider (offline)"
                       : "provider does not know this policy";
  json_t *estate;

  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Provider offline!\n");
  estate = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("detail",
                               detail)),
    GNUNET_JSON_pack_uint64 ("code",
                             ec),
    GNUNET_JSON_pack_string ("hint",
                             TALER_ErrorCode_get_hint (ec)));
  pd->cb (pd->cb_cls,
          ec,
          estate);
  free_pd (pd);
}


/**
 * Callback which passes back the recovery document and its possible
 * policies. Also passes back the version of the document for the user
 * to check.
 *
 * Once the first policy lookup succeeds, we update our state and
 * cancel all of the others, passing the obtained recovery information
 * back to the user.
 *
 * @param cls closure for the callback with a `struct PolicyDownloadEntry *`
 * @param ri recovery information struct which contains the policies
 */
static void
policy_lookup_cb (void *cls,
                  const struct ANASTASIS_RecoveryInformation *ri)
{
  struct PolicyDownloadEntry *pd = cls;
  json_t *policies;
  json_t *challenges;
  json_t *recovery_information;

  if (NULL == ri)
  {
    /* Woopsie, failed hard. */
    ANASTASIS_recovery_abort (pd->recovery);
    GNUNET_free (pd->backend_url);
    GNUNET_free (pd);
    return_no_policy (pd,
                      false);
    return;
  }
  policies = json_array ();
  GNUNET_assert (NULL != policies);
  for (unsigned int i = 0; i<ri->dps_len; i++)
  {
    struct ANASTASIS_DecryptionPolicy *dps = ri->dps[i];
    json_t *pchallenges;

    pchallenges = json_array ();
    GNUNET_assert (NULL != pchallenges);
    for (unsigned int j = 0; j<dps->challenges_length; j++)
    {
      struct ANASTASIS_Challenge *c = dps->challenges[j];
      const struct ANASTASIS_ChallengeDetails *cd;
      json_t *cj;

      cd = ANASTASIS_challenge_get_details (c);
      cj = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_data_auto ("uuid",
                                    &cd->uuid));
      GNUNET_assert (0 ==
                     json_array_append_new (pchallenges,
                                            cj));

    }
    GNUNET_assert (0 ==
                   json_array_append_new (policies,
                                          pchallenges));
  } /* end for all policies */
  challenges = json_array ();
  GNUNET_assert (NULL != challenges);
  for (unsigned int i = 0; i<ri->cs_len; i++)
  {
    struct ANASTASIS_Challenge *c = ri->cs[i];
    const struct ANASTASIS_ChallengeDetails *cd;
    json_t *cj;

    cd = ANASTASIS_challenge_get_details (c);
    cj = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_data_auto ("uuid",
                                  &cd->uuid),
      GNUNET_JSON_pack_string ("type",
                               cd->type),
      GNUNET_JSON_pack_string ("uuid-display",
                               ANASTASIS_CRYPTO_uuid2s (&cd->uuid)),
      GNUNET_JSON_pack_string ("instructions",
                               cd->instructions));
    GNUNET_assert (0 ==
                   json_array_append_new (challenges,
                                          cj));
  } /* end for all challenges */
  recovery_information = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_array_steal ("challenges",
                                  challenges),
    GNUNET_JSON_pack_array_steal ("policies",
                                  policies),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("secret_name",
                               ri->secret_name)),
    GNUNET_JSON_pack_string ("provider_url",
                             pd->backend_url),
    GNUNET_JSON_pack_uint64 ("version",
                             ri->version));
  GNUNET_assert (0 ==
                 json_object_set_new (pd->state,
                                      "recovery_information",
                                      recovery_information));
  {
    json_t *rd;

    rd = ANASTASIS_recovery_serialize (pd->recovery);
    if (NULL == rd)
    {
      GNUNET_break (0);
      ANASTASIS_redux_fail_ (pd->cb,
                             pd->cb_cls,
                             TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                             "unable to serialize recovery state");
      free_pd (pd);
      return;
    }
    GNUNET_assert (0 ==
                   json_object_set_new (pd->state,
                                        "recovery_document",
                                        rd));
  }
  set_state (pd->state,
             ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
  pd->cb (pd->cb_cls,
          TALER_EC_NONE,
          pd->state);
  free_pd (pd);
}


/**
 * This function is called whenever the recovery process ends.
 * In this case, that should not be possible as this callback
 * is used before we even begin with the challenges. So if
 * we are called, it is because of some fatal error.
 *
 * @param cls a `struct PolicyDownloadEntry`
 * @param rc error code
 * @param secret contains the core secret which is passed to the user
 * @param secret_size defines the size of the core secret
 */
static void
core_early_secret_cb (void *cls,
                      enum ANASTASIS_RecoveryStatus rc,
                      const void *secret,
                      size_t secret_size)
{
  struct PolicyDownloadEntry *pd = cls;

  pd->recovery = NULL;
  GNUNET_assert (NULL == secret);
  GNUNET_assert (ANASTASIS_RS_SUCCESS != rc);
  fail_by_error (pd->cb,
                 pd->cb_cls,
                 rc);
  free_pd (pd);
}


/**
 * DispatchHandler/Callback function which is called for a
 * "next" action in "secret_selecting" state.
 *
 * @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 *
done_secret_selecting (json_t *state,
                       const json_t *arguments,
                       ANASTASIS_ActionCallback cb,
                       void *cb_cls)
{
  uint32_t mask;
  json_t *pa;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_uint32 ("attribute_mask",
                             &mask),
    GNUNET_JSON_spec_json ("providers",
                           &pa),
    GNUNET_JSON_spec_end ()
  };
  struct ANASTASIS_CRYPTO_ProviderSaltP provider_salt;
  struct GNUNET_JSON_Specification pspec[] = {
    GNUNET_JSON_spec_fixed_auto ("provider_salt",
                                 &provider_salt),
    GNUNET_JSON_spec_end ()
  };
  json_t *p_cfg;
  json_t *id_data;
  const json_t *providers;

  if (GNUNET_OK !=
      GNUNET_JSON_parse (arguments,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break (0);
    json_dumpf (arguments,
                stderr,
                JSON_INDENT (2));
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           NULL);
    return NULL;
  }
  providers = json_object_get (state,
                               "authentication_providers");
  if ( (NULL == providers) ||
       (! json_is_object (providers)) )
  {
    GNUNET_break (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'authentication_providers' missing");
    return NULL;
  }

  {
    size_t poff;
    json_t *pe;
    uint64_t version;
    const char *provider_url;

    json_array_foreach (pa, poff, pe)
    {
      struct GNUNET_JSON_Specification ispec[] = {
        GNUNET_JSON_spec_uint64 ("version",
                                 &version),
        GNUNET_JSON_spec_string ("url",
                                 &provider_url),
        GNUNET_JSON_spec_end ()
      };

      if (GNUNET_OK !=
          GNUNET_JSON_parse (pe,
                             ispec,
                             NULL, NULL))
      {
        GNUNET_break (0);
        json_dumpf (pe,
                    stderr,
                    JSON_INDENT (2));
        ANASTASIS_redux_fail_ (cb,
                               cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                               NULL);
        return NULL;
      }

      p_cfg = json_object_get (providers,
                               provider_url);
      if (MHD_HTTP_OK !=
          json_integer_value (json_object_get (p_cfg,
                                               "http_status")))
        continue;
      if (GNUNET_OK !=
          GNUNET_JSON_parse (p_cfg,
                             pspec,
                             NULL, NULL))
      {
        GNUNET_break (0); /* should be impossible for well-formed state */
        ANASTASIS_redux_fail_ (cb,
                               cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                               "Salt unknown for selected provider");
        return NULL;
      }
      id_data = json_object_get (state,
                                 "identity_attributes");
      if (NULL == id_data)
      {
        GNUNET_break (0); /* should be impossible for well-formed state */
        ANASTASIS_redux_fail_ (cb,
                               cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                               "'identity_attributes' missing");
        return NULL;
      }
      {
        struct PolicyDownloadEntry *pd
          = GNUNET_new (struct PolicyDownloadEntry);

        pd->cb = cb;
        pd->cb_cls = cb_cls;
        pd->state = json_incref (state);
        pd->backend_url = GNUNET_strdup (provider_url);
        pd->recovery = ANASTASIS_recovery_begin (ANASTASIS_REDUX_ctx_,
                                                 id_data,
                                                 version,
                                                 pd->backend_url,
                                                 &provider_salt,
                                                 &policy_lookup_cb,
                                                 pd,
                                                 &core_early_secret_cb,
                                                 pd);
        if (NULL == pd->recovery)
        {
          GNUNET_break (0);
          ANASTASIS_redux_fail_ (cb,
                                 cb_cls,
                                 TALER_EC_ANASTASIS_REDUCER_INTERNAL_ERROR,
                                 NULL);
          GNUNET_free (pd->backend_url);
          json_decref (pd->state);
          GNUNET_free (pd);
          return NULL;
        }
        pd->ra.cleanup = &free_pd;
        pd->ra.cleanup_cls = pd;
        return &pd->ra;
      }

    }
  }

  /* no provider worked */
  ANASTASIS_redux_fail_ (cb,
                         cb_cls,
                         TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                         "selected provider is not online");
  return NULL;
}


/**
 * The user wants us to add another provider. Download /config.
 *
 * @param[in] state we are in
 * @param arguments our arguments with the solution
 * @param cb function to call with the new state
 * @param cb_cls closure for @a cb
 * @return handle to cancel challenge selection step
 */
static struct ANASTASIS_ReduxAction *
add_provider (json_t *state,
              const json_t *arguments,
              ANASTASIS_ActionCallback cb,
              void *cb_cls)
{
  const char *provider_url;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_string ("provider_url",
                             &provider_url),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (arguments,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           NULL);
    return NULL;
  }
  return ANASTASIS_REDUX_add_provider_to_state_ (provider_url,
                                                 state,
                                                 cb,
                                                 cb_cls);
}


/**
 * Signature of callback function that implements a state transition.
 *
 *  @param state current state
 *  @param arguments arguments for the state transition
 *  @param cb function to call when done
 *  @param cb_cls closure for @a cb
 */
typedef struct ANASTASIS_ReduxAction *
(*DispatchHandler)(json_t *state,
                   const json_t *arguments,
                   ANASTASIS_ActionCallback cb,
                   void *cb_cls);


struct ANASTASIS_ReduxAction *
ANASTASIS_recovery_action_ (json_t *state,
                            const char *action,
                            const json_t *arguments,
                            ANASTASIS_ActionCallback cb,
                            void *cb_cls)
{
  struct Dispatcher
  {
    enum ANASTASIS_RecoveryState recovery_state;
    const char *recovery_action;
    DispatchHandler fun;
  } dispatchers[] = {
    {
      ANASTASIS_RECOVERY_STATE_SECRET_SELECTING,
      "add_provider",
      &add_provider
    },
    {
      ANASTASIS_RECOVERY_STATE_SECRET_SELECTING,
      "poll_providers",
      &ANASTASIS_REDUX_poll_providers_
    },
    {
      ANASTASIS_RECOVERY_STATE_SECRET_SELECTING,
      "select_version",
      &done_secret_selecting
    },
    {
      ANASTASIS_RECOVERY_STATE_SECRET_SELECTING,
      "back",
      &ANASTASIS_back_generic_decrement_
    },
    {
      ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING,
      "select_challenge",
      &select_challenge
    },
    {
      ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING,
      "sync_providers",
      &ANASTASIS_REDUX_sync_providers_
    },
    {
      ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING,
      "poll",
      &poll_challenges
    },
    {
      ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING,
      "back",
      &ANASTASIS_back_generic_decrement_
    },
    {
      ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING,
      "pay",
      &pay_challenge
    },
    {
      ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING,
      "back",
      &ANASTASIS_back_generic_decrement_
    },
    {
      ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING,
      "solve_challenge",
      &solve_challenge
    },
    {
      ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING,
      "back",
      &back_challenge_solving
    },
    { ANASTASIS_RECOVERY_STATE_INVALID, NULL, NULL }
  };
  const char *s = json_string_value (json_object_get (state,
                                                      "recovery_state"));
  enum ANASTASIS_RecoveryState rs;

  GNUNET_assert (NULL != s);
  rs = ANASTASIS_recovery_state_from_string_ (s);
  if (ANASTASIS_RECOVERY_STATE_INVALID == rs)
  {
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'recovery_state' field invalid");
    return NULL;
  }
  for (unsigned int i = 0; NULL != dispatchers[i].fun; i++)
  {
    if ( (rs == dispatchers[i].recovery_state) &&
         (0 == strcmp (action,
                       dispatchers[i].recovery_action)) )
    {
      return dispatchers[i].fun (state,
                                 arguments,
                                 cb,
                                 cb_cls);
    }
  }
  ANASTASIS_redux_fail_ (cb,
                         cb_cls,
                         TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID,
                         action);
  return NULL;
}


struct ANASTASIS_ReduxAction *
ANASTASIS_REDUX_recovery_challenge_begin_ (json_t *state,
                                           const json_t *arguments,
                                           ANASTASIS_ActionCallback cb,
                                           void *cb_cls)
{
  const json_t *providers;
  json_t *attributes;

  providers = json_object_get (state,
                               "authentication_providers");
  if ( (NULL == providers) ||
       (! json_is_object (providers)) )
  {
    GNUNET_break (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'authentication_providers' missing");
    return NULL;
  }
  attributes = json_object_get (arguments,
                                "identity_attributes");
  if ( (NULL == attributes) ||
       (! json_is_object (attributes)) )
  {
    GNUNET_break (0);
    ANASTASIS_redux_fail_ (cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'identity_attributes' missing");
    return NULL;
  }
  json_object_set (state,
                   "identity_attributes",
                   attributes);
  set_state (state,
             ANASTASIS_RECOVERY_STATE_SECRET_SELECTING);
  cb (cb_cls,
      TALER_EC_NONE,
      state);
  return NULL;
}
