/*
  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"
#include "anastasis_api_redux_state.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];
}


/**
 * Transition @a rs to @a new_recovery_state.
 *
 * @param[in,out] rs state to transition
 * @param new_recovery_state state to transition to
 */
static void
set_state (struct ANASTASIS_ReduxState *rs,
           enum ANASTASIS_RecoveryState new_recovery_state)
{
  GNUNET_assert (ANASTASIS_RT_RECOVERY == rs->type);
  rs->details.recovery.state = 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)
    {
      GNUNET_assert (0 ==
                     close (pipefd_stdout[0]));
      GNUNET_assert (STDOUT_FILENO ==
                     dup2 (pipefd_stdout[1],
                           STDOUT_FILENO));
      execlp (external_reducer,
              external_reducer,
              "-r",
              NULL);
      GNUNET_assert (0);
    }

    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")));
  GNUNET_assert (
    0 ==
    json_object_set_new (
      initial_state,
      "recovery_state",
      json_string (
        ANASTASIS_recovery_state_to_string_ (
          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;

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

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

  /**
   * Our state; we own it.  The recovery operation we drive lives in
   * `rs->details.recovery.r`, so there is no second handle to keep in
   * sync here.
   */
  struct ANASTASIS_ReduxState *rs;

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

  /* Freeing the state also aborts the recovery operation, if one is
     still running. */
  ANASTASIS_REDUX_state_free_ (sctx->rs);
  json_decref (sctx->args);
  if (NULL != sctx->delayed_report)
  {
    GNUNET_SCHEDULER_cancel (sctx->delayed_report);
    sctx->delayed_report = NULL;
  }
  GNUNET_free (sctx);
}


/**
 * Hand our state back to the application and dispose of @a sctx.
 *
 * @param[in] sctx context to finish
 * @param ec error code to report alongside the state
 */
static void
sctx_return (struct SelectChallengeContext *sctx,
             enum TALER_ErrorCode ec)
{
  struct ANASTASIS_ReduxState *rs = sctx->rs;
  ANASTASIS_ActionCallback cb = sctx->cb;
  void *cb_cls = sctx->cb_cls;

  sctx->rs = NULL;
  sctx_free (sctx);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           ec);
}


/**
 * Report an error to the application and dispose of @a sctx.
 *
 * @param[in] sctx context to finish
 * @param ec error to report
 * @param detail human-readable detail, may be NULL
 */
static void
sctx_fail (struct SelectChallengeContext *sctx,
           enum TALER_ErrorCode ec,
           const char *detail)
{
  ANASTASIS_ActionCallback cb = sctx->cb;
  void *cb_cls = sctx->cb_cls;

  sctx_free (sctx);
  ANASTASIS_redux_fail_ (cb,
                         cb_cls,
                         ec,
                         detail);
}


/**
 * Translate a recovery failure into an error code and message.
 *
 * @param rc status to translate
 * @param[out] msg set to a human-readable, translatable message
 * @return the error code to report
 */
static enum TALER_ErrorCode
error_by_status (enum ANASTASIS_RecoveryStatus rc,
                 const char **msg)
{
  enum TALER_ErrorCode ec = TALER_EC_INVALID;

  *msg = NULL;
  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;
  case ANASTASIS_RS_POLICY_DECRYPTION_FAILED:
    /* The two causes are indistinguishable to us: a wrong key and a key
       derived differently both just fail to authenticate the ciphertext.
       Name both, or a user with a pre-flag-day backup retypes their
       identity attributes forever.  See NEWS on the key derivation
       change. */
    *msg = gettext_noop (
      "failed to decrypt policy document: either the identity attributes are not the ones used for the backup, or the backup was made before the key derivation change and cannot be recovered with this version");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED;
    break;
  case ANASTASIS_RS_CORE_SECRET_RECOVERY_FAILED:
    *msg = gettext_noop (
      "failed to reassemble the core secret from the recovered key shares");
    ec = TALER_EC_ANASTASIS_REDUCER_POLICY_MALFORMED;
    break;
  }
  return ec;
}


/**
 * 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;
  struct ANASTASIS_ReduxRecovery *rr = &sctx->rs->details.recovery;

  /* the library disposed of the operation before calling us */
  rr->r = NULL;
  if (ANASTASIS_RS_SUCCESS == rc)
  {
    json_t *jsecret;

    jsecret = json_loadb (secret,
                          secret_size,
                          JSON_REJECT_DUPLICATES,
                          NULL);
    if (NULL == jsecret)
    {
      sctx_fail (sctx,
                 TALER_EC_ANASTASIS_REDUCER_SECRET_MALFORMED,
                 NULL);
      return;
    }
    json_decref (rr->core_secret);
    rr->core_secret = jsecret;
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_RECOVERY_FINISHED);
    sctx_return (sctx,
                 TALER_EC_NONE);
    return;
  }
  {
    const char *msg;
    enum TALER_ErrorCode ec;

    ec = error_by_status (rc,
                          &msg);
    sctx_fail (sctx,
               ec,
               msg);
  }
}


/**
 * 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->rs,
             ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
  sctx_return (sctx,
               TALER_EC_NONE);
}


/**
 * Obtain the feedback entry for challenge @a uuid, reset to a fresh
 * variant of kind @a status.
 *
 * @param[in,out] sctx context whose state to update
 * @param uuid challenge the feedback is about
 * @param status which variant the caller is about to fill in
 * @return the entry to fill in
 */
static struct ANASTASIS_ReduxFeedback *
set_feedback (struct SelectChallengeContext *sctx,
              const struct ANASTASIS_CRYPTO_TruthUUIDP *uuid,
              enum ANASTASIS_ReduxFeedbackStatus status)
{
  struct ANASTASIS_ReduxFeedback *fb;

  fb = ANASTASIS_REDUX_feedback_get_ (&sctx->rs->details.recovery,
                                      uuid);
  fb->status = status;
  return fb;
}


/**
 * 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;
  struct ANASTASIS_ReduxRecovery *rr = &sctx->rs->details.recovery;
  const struct ANASTASIS_ChallengeDetails *cd;
  struct ANASTASIS_ReduxFeedback *fb;

  cd = ANASTASIS_challenge_get_details (csr->challenge);
  switch (csr->cs)
  {
  case ANASTASIS_CHALLENGE_START_STATUS_FILENAME_PROVIDED:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_CODE_IN_FILE);
    fb->details.code_in_file.filename
      = GNUNET_strdup (csr->details.tan_filename);
    GNUNET_asprintf (&fb->display_hint,
                     _ ("Required TAN can be found in `%s'"),
                     csr->details.tan_filename);
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx_return (sctx,
                 TALER_EC_NONE);
    return;
  case ANASTASIS_CHALLENGE_START_STATUS_TAN_SENT_HINT_PROVIDED:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_SEND_TO_ADDRESS);
    fb->details.send_to_address.address_hint
      = GNUNET_strdup (csr->details.tan_address_hint);
    GNUNET_asprintf (&fb->display_hint,
                     _ ("TAN code was sent to `%s'"),
                     csr->details.tan_address_hint);
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx_return (sctx,
                 TALER_EC_NONE);
    return;

  case ANASTASIS_CHALLENGE_START_STATUS_TAN_ALREADY_SENT:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_SEND_TO_ADDRESS);
    GNUNET_asprintf (&fb->display_hint,
                     _ ("TAN code already sent."));
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx_return (sctx,
                 TALER_EC_NONE);
    return;

  case ANASTASIS_CHALLENGE_START_STATUS_PAYMENT_REQUIRED:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_TALER_PAYMENT);
    fb->details.taler_payment.taler_pay_uri
      = GNUNET_strdup (csr->details.payment_required.taler_pay_uri);
    ANASTASIS_REDUX_provider_url_set_ (&fb->details.taler_payment.provider,
                                       cd->provider_url);
    fb->details.taler_payment.payment_secret
      = csr->details.payment_required.payment_secret;
    GNUNET_asprintf (&fb->display_hint,
                     _ ("Taler payment to `%s' required"),
                     csr->details.payment_required.taler_pay_uri);
    /* Remember payment secret for later (once application claims it paid) */
    {
      struct ANASTASIS_ReduxChallengeInfo *ci;

      ci = ANASTASIS_REDUX_challenge_find_ (rr,
                                            &cd->uuid);
      GNUNET_assert (NULL != ci);
      ci->payment_secret = csr->details.payment_required.payment_secret;
      ci->have_payment_secret = true;
    }
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING);
    sctx_return (sctx,
                 TALER_EC_NONE);
    return;
  case ANASTASIS_CHALLENGE_START_STATUS_SERVER_FAILURE:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_SERVER_FAILURE);
    fb->details.server_failure.http_status = csr->http_status;
    fb->details.server_failure.ec = csr->ec;
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx_return (sctx,
                 csr->ec);
    return;
  case ANASTASIS_CHALLENGE_START_STATUS_TRUTH_UNKNOWN:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_TRUTH_UNKNOWN);
    fb->details.server_failure.http_status = csr->http_status;
    fb->details.server_failure.ec = TALER_EC_ANASTASIS_TRUTH_UNKNOWN;
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx_return (sctx,
                 TALER_EC_ANASTASIS_TRUTH_UNKNOWN);
    return;
  case ANASTASIS_CHALLENGE_START_STATUS_BANK_TRANSFER_REQUIRED:
    /* The provider turned this into an asynchronous challenge: remember
       the answer code so that a later "poll" can pick it up again. */
    ANASTASIS_challenge_set_async (
      csr->challenge,
      csr->details.bank_transfer_required.answer_code);
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_IBAN_INSTRUCTIONS);
    fb->details.iban_instructions.target_iban
      = GNUNET_strdup (csr->details.bank_transfer_required.target_iban);
    fb->details.iban_instructions.target_business_name
      = GNUNET_strdup (
          csr->details.bank_transfer_required.target_business_name);
    fb->details.iban_instructions.wire_transfer_subject
      = GNUNET_strdup (
          csr->details.bank_transfer_required.wire_transfer_subject);
    fb->details.iban_instructions.amount
      = csr->details.bank_transfer_required.amount;
    GNUNET_asprintf (&fb->display_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);
    rr->selected_challenge = cd->uuid;
    rr->have_selected_challenge = true;
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx_return (sctx,
                 TALER_EC_NONE);
    return;
  }
  GNUNET_break (0);
  sctx_fail (sctx,
             TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
             NULL);
}


/**
 * 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;
  struct ANASTASIS_ReduxRecovery *rr = &sctx->rs->details.recovery;
  const struct ANASTASIS_ChallengeDetails *cd;
  struct ANASTASIS_ReduxFeedback *fb;

  cd = ANASTASIS_challenge_get_details (csr->challenge);
  switch (csr->cs)
  {
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_SOLVED:
    (void) set_feedback (sctx,
                         &cd->uuid,
                         ANASTASIS_RFS_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:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_INCORRECT_ANSWER);
    fb->details.incorrect_answer.ec = csr->ec;
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
    sctx_return (sctx,
                 TALER_EC_NONE);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_PAYMENT_REQUIRED:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_TALER_PAYMENT);
    fb->details.taler_payment.taler_pay_uri
      = GNUNET_strdup (csr->details.payment_required.taler_pay_uri);
    ANASTASIS_REDUX_provider_url_set_ (&fb->details.taler_payment.provider,
                                       cd->provider_url);
    fb->details.taler_payment.payment_secret
      = csr->details.payment_required.payment_secret;
    GNUNET_asprintf (&fb->display_hint,
                     _ ("Taler payment to `%s' required"),
                     csr->details.payment_required.taler_pay_uri);
    /* Remember payment secret for later (once application claims it paid) */
    {
      struct ANASTASIS_ReduxChallengeInfo *ci;

      ci = ANASTASIS_REDUX_challenge_find_ (rr,
                                            &cd->uuid);
      GNUNET_assert (NULL != ci);
      ci->payment_secret = csr->details.payment_required.payment_secret;
      ci->have_payment_secret = true;
    }
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_PAYING);
    sctx_return (sctx,
                 TALER_EC_NONE);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_SERVER_FAILURE:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_SERVER_FAILURE);
    fb->details.server_failure.http_status = csr->http_status;
    fb->details.server_failure.ec = csr->ec;
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx_return (sctx,
                 csr->ec);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_TRUTH_UNKNOWN:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_TRUTH_UNKNOWN);
    fb->details.server_failure.http_status = csr->http_status;
    fb->details.server_failure.ec = TALER_EC_ANASTASIS_TRUTH_UNKNOWN;
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx_return (sctx,
                 TALER_EC_ANASTASIS_TRUTH_UNKNOWN);
    return;
  case ANASTASIS_CHALLENGE_ANSWER_STATUS_RATE_LIMIT_EXCEEDED:
    fb = set_feedback (sctx,
                       &cd->uuid,
                       ANASTASIS_RFS_RATE_LIMIT_EXCEEDED);
    fb->details.rate_limit_exceeded.ec
      = TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED;
    fb->details.rate_limit_exceeded.request_limit
      = csr->details.rate_limit_exceeded.request_limit;
    fb->details.rate_limit_exceeded.request_frequency
      = csr->details.rate_limit_exceeded.request_frequency;
    GNUNET_asprintf (
      &fb->display_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));
    set_state (sctx->rs,
               ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
    sctx_return (sctx,
                 TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED);
    return;
  }
  GNUNET_break (0);
  sctx_fail (sctx,
             TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
             NULL);
}


/**
 * 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;
  struct ANASTASIS_ReduxRecovery *rr = &sctx->rs->details.recovery;
  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 ()
  };
  struct ANASTASIS_ReduxChallengeInfo *sci;

  if (NULL == ri)
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
               "recovery information could not be deserialized");
    return;
  }

  if ( (NULL != sctx->args) &&
       (GNUNET_OK !=
        GNUNET_JSON_parse (sctx->args,
                           tspec,
                           NULL, NULL)) )
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
               "'timeout' malformed");
    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;

      cd = ANASTASIS_challenge_get_details (ci);
      if (cd->solved ||
          (! cd->async) )
        continue;
      if (! cd->have_answer_pin)
      {
        GNUNET_break_op (0);
        sctx_fail (sctx,
                   TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                   "async challenge 'answer-pin' not found");
        return;
      }
      if (GNUNET_OK !=
          ANASTASIS_challenge_answer2 (ci,
                                       psp,
                                       timeout,
                                       cd->answer_pin,
                                       &answer_feedback_cb,
                                       sctx))
      {
        sctx_fail (sctx,
                   TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                   "Failed to begin answering asynchronous challenge");
        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 */
  sci = ANASTASIS_REDUX_challenge_find_ (rr,
                                         &sctx->uuid);
  if (NULL == sci)
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
               "challenge not found");
    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);
      sctx_fail (sctx,
                 TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                 "'payment_secret' malformed");
      return;
    }
    psp = &ps;
  }
  else if (sci->have_payment_secret)
  {
    psp = &sci->payment_secret;
  }

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

    cd = ANASTASIS_challenge_get_details (ci);
    if (cd->async)
      continue; /* handled above */
    if (0 !=
        GNUNET_memcmp (&sctx->uuid,
                       &cd->uuid))
      continue;
    if (cd->solved)
    {
      sctx_fail (sctx,
                 TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                 "Selected challenge already solved");
      return;
    }
    if (0 == strcmp ("question",
                     cd->type))
    {
      /* security question, answer must be a string */
      const char *answer = json_string_value (json_object_get (sctx->args,
                                                               "answer"));

      if (NULL == answer)
      {
        sctx_fail (sctx,
                   TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                   "'answer' missing");
        return;
      }
      /* persist answer, in case payment is required */
      GNUNET_free (sci->answer);
      sci->answer = GNUNET_strdup (answer);
      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 */
        ANASTASIS_challenge_set_answer_pin (ci,
                                            ianswer);
        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))
        {
          sctx_fail (sctx,
                     TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                     "'answer' malformed");
          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)
    {
      sctx_fail (sctx,
                 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                 "Failed to begin answering challenge");
      return;
    }
    return;   /* await answer feedback */
  }
  sctx_fail (sctx,
             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
             "'uuid' not in list of challenges");
}


/**
 * 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;
  struct ANASTASIS_ReduxRecovery *rr = &sctx->rs->details.recovery;
  struct ANASTASIS_ReduxChallengeInfo *sci;

  if (NULL == ri)
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
               "recovery information could not be deserialized");
    return;
  }

  sci = ANASTASIS_REDUX_challenge_find_ (rr,
                                         &sctx->uuid);
  if (NULL == sci)
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
               "challenge not found");
    return;
  }
  /* persist payment, in case we need to run the request again */
  sci->payment_secret = sctx->ps;
  sci->have_payment_secret = true;

  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)
    {
      sctx_fail (sctx,
                 TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                 "Selected challenge already solved");
      return;
    }

    if (0 == strcmp ("question",
                     cd->type))
    {
      /* security question, answer must be a string and already ready */
      if (NULL == sci->answer)
      {
        sctx_fail (sctx,
                   TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                   "'answer' missing");
        return;
      }
      ret = ANASTASIS_challenge_answer (ci,
                                        &sctx->ps,
                                        sctx->timeout,
                                        sci->answer,
                                        &answer_feedback_cb,
                                        sctx);
    }
    else
    {
      ret = ANASTASIS_challenge_start (ci,
                                       &sctx->ps,
                                       &start_feedback_cb,
                                       sctx);
    }
    if (GNUNET_OK != ret)
    {
      sctx_fail (sctx,
                 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                 "Failed to begin answering challenge");
      return;
    }
    return;   /* await answer feedback */
  }
  sctx_fail (sctx,
             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
             "'uuid' not in list of challenges");
}


/**
 * 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 (struct ANASTASIS_ReduxState *rs,
                 const json_t *arguments,
                 ANASTASIS_ActionCallback cb,
                 void *cb_cls)
{
  struct ANASTASIS_ReduxRecovery *rr = &rs->details.recovery;
  struct SelectChallengeContext *sctx;

  if (NULL == arguments)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "arguments missing");
    return NULL;
  }
  if (! rr->have_selected_challenge)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'selected_challenge_uuid' missing");
    return NULL;
  }
  if (NULL == rr->r)
  {
    GNUNET_break_op (0);
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "solve_challenge");
    return NULL;
  }
  sctx = GNUNET_new (struct SelectChallengeContext);
  sctx->uuid = rr->selected_challenge;
  sctx->cb = cb;
  sctx->cb_cls = cb_cls;
  sctx->rs = rs;
  sctx->args = json_incref ((json_t*) arguments);
  if (GNUNET_OK !=
      ANASTASIS_recovery_resume (rr->r,
                                 &solve_challenge_cb,
                                 sctx,
                                 &core_secret_cb,
                                 sctx))
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               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 (struct ANASTASIS_ReduxState *rs,
                 const json_t *arguments,
                 ANASTASIS_ActionCallback cb,
                 void *cb_cls)
{
  struct ANASTASIS_ReduxRecovery *rr = &rs->details.recovery;
  struct SelectChallengeContext *sctx;

  if (NULL == rr->r)
  {
    GNUNET_break_op (0);
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "poll_challenges");
    return NULL;
  }
  sctx = GNUNET_new (struct SelectChallengeContext);
  sctx->poll_only = true;
  sctx->cb = cb;
  sctx->cb_cls = cb_cls;
  sctx->rs = rs;
  sctx->args = json_incref ((json_t*) arguments);
  if (GNUNET_OK !=
      ANASTASIS_recovery_resume (rr->r,
                                 &solve_challenge_cb,
                                 sctx,
                                 &core_secret_cb,
                                 sctx))
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               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 (struct ANASTASIS_ReduxState *rs,
               const json_t *arguments,
               ANASTASIS_ActionCallback cb,
               void *cb_cls)
{
  struct ANASTASIS_ReduxRecovery *rr = &rs->details.recovery;
  struct SelectChallengeContext *sctx;
  struct GNUNET_TIME_Relative timeout = GNUNET_TIME_UNIT_ZERO;
  struct ANASTASIS_PaymentSecretP ps;
  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",
                                 &ps),
    GNUNET_JSON_spec_end ()
  };

  if (NULL == arguments)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           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_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'payment_secret' missing");
    return NULL;
  }
  if (! rr->have_selected_challenge)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'selected_challenge_uuid' missing");
    return NULL;
  }
  if (NULL == rr->r)
  {
    GNUNET_break_op (0);
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "pay_challenge");
    return NULL;
  }
  sctx = GNUNET_new (struct SelectChallengeContext);
  sctx->uuid = rr->selected_challenge;
  sctx->ps = ps;
  sctx->timeout = timeout;
  sctx->cb = cb;
  sctx->cb_cls = cb_cls;
  sctx->rs = rs;
  sctx->args = json_incref ((json_t*) arguments);
  if (GNUNET_OK !=
      ANASTASIS_recovery_resume (rr->r,
                                 &pay_challenge_cb,
                                 sctx,
                                 &core_secret_cb,
                                 sctx))
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               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;
  struct ANASTASIS_ReduxRecovery *rr = &sctx->rs->details.recovery;
  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);
    sctx_fail (sctx,
               TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
               "recovery information could not be deserialized");
    return;
  }

  if (GNUNET_OK !=
      GNUNET_JSON_parse (sctx->args,
                         tspec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
               "'timeout' malformed");
    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);
      sctx_fail (sctx,
                 TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                 "'payment_secret' malformed");
      return;
    }
    psp = &ps;
  }
  else
  {
    /* Check if we got a payment_secret in state */
    const struct ANASTASIS_ReduxChallengeInfo *sci;

    sci = ANASTASIS_REDUX_challenge_find_ (rr,
                                           &sctx->uuid);
    if (NULL == sci)
    {
      GNUNET_break_op (0);
      sctx_fail (sctx,
                 TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                 "challenge not found");
      return;
    }
    if (sci->have_payment_secret)
    {
      ps = sci->payment_secret;
      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)
    {
      sctx_fail (sctx,
                 TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                 "Selected challenge already solved");
      return;
    }
    rr->selected_challenge = cd->uuid;
    rr->have_selected_challenge = true;
    if ( (0 == strcmp ("question",
                       cd->type)) ||
         (0 == strcmp ("totp",
                       cd->type)) )
    {
      /* security question or TOTP:
         immediately request user to answer it */
      set_state (sctx->rs,
                 ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING);
      sctx_return (sctx,
                   TALER_EC_NONE);
      return;
    }
    /* trigger challenge */
    if (cd->have_answer_pin)
      ret = ANASTASIS_challenge_answer2 (ci,
                                         psp,
                                         timeout,
                                         cd->answer_pin,
                                         &answer_feedback_cb,
                                         sctx);
    else
      ret = ANASTASIS_challenge_start (ci,
                                       psp,
                                       &start_feedback_cb,
                                       sctx);
    if (GNUNET_OK != ret)
    {
      sctx_fail (sctx,
                 TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                 "Failed to begin answering challenge");
      return;
    }
    return;   /* await answer feedback */
  }
  sctx_fail (sctx,
             TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
             "'uuid' not in list of challenges");
}


/**
 * 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 (struct ANASTASIS_ReduxState *rs,
                  const json_t *arguments,
                  ANASTASIS_ActionCallback cb,
                  void *cb_cls)
{
  struct ANASTASIS_ReduxRecovery *rr = &rs->details.recovery;
  struct SelectChallengeContext *sctx;
  struct ANASTASIS_CRYPTO_TruthUUIDP uuid;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("uuid",
                                 &uuid),
    GNUNET_JSON_spec_end ()
  };

  if (NULL == arguments)
  {
    ANASTASIS_REDUX_fail_ (rs,
                           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_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           "'uuid' missing");
    return NULL;
  }
  if (NULL == rr->r)
  {
    GNUNET_break_op (0);
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "select_challenge");
    return NULL;
  }
  sctx = GNUNET_new (struct SelectChallengeContext);
  sctx->uuid = uuid;
  sctx->cb = cb;
  sctx->cb_cls = cb_cls;
  sctx->rs = rs;
  sctx->args = json_incref ((json_t*) arguments);
  if (GNUNET_OK !=
      ANASTASIS_recovery_resume (rr->r,
                                 &select_challenge_cb,
                                 sctx,
                                 &core_secret_cb,
                                 sctx))
  {
    GNUNET_break_op (0);
    sctx_fail (sctx,
               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 (struct ANASTASIS_ReduxState *rs,
                        const json_t *arguments,
                        ANASTASIS_ActionCallback cb,
                        void *cb_cls)
{
  (void) arguments;
  rs->details.recovery.have_selected_challenge = false;
  set_state (rs,
             ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
  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; we own it.
   */
  struct ANASTASIS_ReduxState *rs;

};


/**
 * 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);
  ANASTASIS_REDUX_state_free_ (pd->rs);
  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";
  ANASTASIS_ActionCallback cb = pd->cb;
  void *cb_cls = pd->cb_cls;

  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Provider offline!\n");
  free_pd (pd);
  ANASTASIS_redux_fail_ (cb,
                         cb_cls,
                         ec,
                         detail);
}


/**
 * 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;
  struct ANASTASIS_ReduxRecovery *rr = &pd->rs->details.recovery;
  struct ANASTASIS_ReduxRecoveryInfo *nri;
  struct ANASTASIS_ReduxState *rs;
  ANASTASIS_ActionCallback cb;
  void *cb_cls;

  if (NULL == ri)
  {
    /* Woopsie, failed hard. */
    return_no_policy (pd,
                      false);
    return;
  }
  nri = GNUNET_new (struct ANASTASIS_ReduxRecoveryInfo);
  nri->challenges_len = ri->cs_len;
  nri->challenges = GNUNET_new_array (nri->challenges_len,
                                      struct ANASTASIS_ReduxChallengeInfo);
  for (unsigned int i = 0; i<ri->cs_len; i++)
  {
    struct ANASTASIS_ReduxChallengeInfo *ci = &nri->challenges[i];
    const struct ANASTASIS_ChallengeDetails *cd;

    cd = ANASTASIS_challenge_get_details (ri->cs[i]);
    ci->uuid = cd->uuid;
    ci->type = GNUNET_strdup (cd->type);
    ci->instructions = GNUNET_strdup (cd->instructions);
  } /* end for all challenges */
  nri->policies_len = ri->dps_len;
  nri->policies = GNUNET_new_array (nri->policies_len,
                                    struct ANASTASIS_ReduxRecoveryPolicy);
  for (unsigned int i = 0; i<ri->dps_len; i++)
  {
    const struct ANASTASIS_DecryptionPolicy *dps = ri->dps[i];
    struct ANASTASIS_ReduxRecoveryPolicy *p = &nri->policies[i];

    p->uuids_len = dps->challenges_length;
    p->uuids = GNUNET_new_array (p->uuids_len,
                                 struct ANASTASIS_CRYPTO_TruthUUIDP);
    for (unsigned int j = 0; j<dps->challenges_length; j++)
    {
      const struct ANASTASIS_ChallengeDetails *cd;

      cd = ANASTASIS_challenge_get_details (dps->challenges[j]);
      p->uuids[j] = cd->uuid;
    }
  } /* end for all policies */
  if (NULL != ri->secret_name)
    nri->secret_name = GNUNET_strdup (ri->secret_name);
  ANASTASIS_REDUX_provider_url_set_ (&nri->provider_url,
                                     pd->backend_url);
  nri->version = ri->version;
  ANASTASIS_REDUX_recovery_info_free_ (rr->ri);
  rr->ri = nri;
  /* The state takes over the operation: it is what gets serialized as
     `recovery_document`, and it is aborted when the state is freed. */
  GNUNET_assert (NULL == rr->r);
  rr->r = pd->recovery;
  pd->recovery = NULL;
  set_state (pd->rs,
             ANASTASIS_RECOVERY_STATE_CHALLENGE_SELECTING);
  rs = pd->rs;
  cb = pd->cb;
  cb_cls = pd->cb_cls;
  pd->rs = NULL;
  free_pd (pd);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
}


/**
 * 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;
  ANASTASIS_ActionCallback cb = pd->cb;
  void *cb_cls = pd->cb_cls;
  const char *msg;
  enum TALER_ErrorCode ec;

  pd->recovery = NULL;
  GNUNET_assert (NULL == secret);
  GNUNET_assert (ANASTASIS_RS_SUCCESS != rc);
  ec = error_by_status (rc,
                        &msg);
  free_pd (pd);
  ANASTASIS_redux_fail_ (cb,
                         cb_cls,
                         ec,
                         msg);
}


/**
 * 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 (struct ANASTASIS_ReduxState *rs,
                       const json_t *arguments,
                       ANASTASIS_ActionCallback cb,
                       void *cb_cls)
{
  uint32_t mask;
  const json_t *pa;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_uint32 ("attribute_mask",
                             &mask),
    GNUNET_JSON_spec_array_const ("providers",
                                  &pa),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (arguments,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break (0);
    json_dumpf (arguments,
                stderr,
                JSON_INDENT (2));
    ANASTASIS_REDUX_fail_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           NULL);
    return NULL;
  }
  if (! rs->common.have_providers)
  {
    GNUNET_break (0);
    ANASTASIS_REDUX_fail_ (rs,
                           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)
    {
      const struct ANASTASIS_ReduxProvider *p;
      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_ (rs,
                               cb,
                               cb_cls,
                               TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                               NULL);
        return NULL;
      }

      p = ANASTASIS_REDUX_provider_find_ (&rs->common,
                                          provider_url);
      if ( (NULL == p) ||
           (! p->have_config) ||
           (MHD_HTTP_OK != p->config.http_status) )
        continue;
      if (NULL == rs->common.identity_attributes)
      {
        GNUNET_break (0); /* should be impossible for well-formed state */
        ANASTASIS_REDUX_fail_ (rs,
                               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->rs = rs;
        pd->backend_url = GNUNET_strdup (provider_url);
        pd->recovery = ANASTASIS_recovery_begin (
          ANASTASIS_REDUX_ctx_,
          rs->common.identity_attributes,
          version,
          pd->backend_url,
          &p->config.provider_salt,
          &policy_lookup_cb,
          pd,
          &core_early_secret_cb,
          pd);
        if (NULL == pd->recovery)
        {
          GNUNET_break (0);
          free_pd (pd);
          ANASTASIS_redux_fail_ (cb,
                                 cb_cls,
                                 TALER_EC_ANASTASIS_REDUCER_INTERNAL_ERROR,
                                 NULL);
          return NULL;
        }
        pd->ra.cleanup = &free_pd;
        pd->ra.cleanup_cls = pd;
        return &pd->ra;
      }
    }
  }

  /* no provider worked */
  ANASTASIS_REDUX_fail_ (rs,
                         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 (struct ANASTASIS_ReduxState *rs,
              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_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_INPUT_INVALID,
                           NULL);
    return NULL;
  }
  return ANASTASIS_REDUX_provider_add_ (provider_url,
                                        rs,
                                        cb,
                                        cb_cls);
}


/**
 * Signature of callback function that implements a state transition.
 *
 *  @param[in] rs 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)(struct ANASTASIS_ReduxState *rs,
                   const json_t *arguments,
                   ANASTASIS_ActionCallback cb,
                   void *cb_cls);


struct ANASTASIS_ReduxAction *
ANASTASIS_recovery_action_ (struct ANASTASIS_ReduxState *rs,
                            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 }
  };
  enum ANASTASIS_RecoveryState state = rs->details.recovery.state;

  GNUNET_assert (ANASTASIS_RT_RECOVERY == rs->type);
  for (unsigned int i = 0; NULL != dispatchers[i].fun; i++)
  {
    if ( (state == dispatchers[i].recovery_state) &&
         (0 == strcmp (action,
                       dispatchers[i].recovery_action)) )
    {
      return dispatchers[i].fun (rs,
                                 arguments,
                                 cb,
                                 cb_cls);
    }
  }
  ANASTASIS_REDUX_fail_ (rs,
                         cb,
                         cb_cls,
                         TALER_EC_ANASTASIS_REDUCER_ACTION_INVALID,
                         action);
  return NULL;
}


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

  if (! rs->common.have_providers)
  {
    GNUNET_break (0);
    ANASTASIS_REDUX_fail_ (rs,
                           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_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_ANASTASIS_REDUCER_STATE_INVALID,
                           "'identity_attributes' missing");
    return NULL;
  }
  json_decref (rs->common.identity_attributes);
  rs->common.identity_attributes = json_incref (attributes);
  set_state (rs,
             ANASTASIS_RECOVERY_STATE_SECRET_SELECTING);
  ANASTASIS_REDUX_return_ (rs,
                           cb,
                           cb_cls,
                           TALER_EC_NONE);
  return NULL;
}
