/*
  This file is part of TALER
  (C) 2014-2021 Taler Systems SA

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

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

  You should have received a copy of the GNU General Public
  License along with TALER; see the file COPYING.  If not,
  see <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-merchant-httpd_post-orders-ID-abort.c
 * @brief handling of POST /orders/$ID/abort requests
 * @author Marcello Stanisci
 * @author Christian Grothoff
 * @author Florian Dold
 */
#include "platform.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_exchange_service.h>
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_post-orders-ID-abort.h"


/**
 * How long to wait before giving up processing with the exchange?
 */
#define ABORT_GENERIC_TIMEOUT (GNUNET_TIME_relative_multiply ( \
                                 GNUNET_TIME_UNIT_SECONDS, \
                                 30))

/**
 * How often do we retry the (complex!) database transaction?
 */
#define MAX_RETRIES 5

/**
 * Information we keep for an individual call to the /abort handler.
 */
struct AbortContext;

/**
 * Information kept during a /abort request for each coin.
 */
struct RefundDetails
{

  /**
   * Public key of the coin.
   */
  struct TALER_CoinSpendPublicKeyP coin_pub;

  /**
   * Signature from the exchange confirming the refund.
   * Set if we were successful (status 200).
   */
  struct TALER_ExchangeSignatureP exchange_sig;

  /**
   * Public key used for @e exchange_sig.
   * Set if we were successful (status 200).
   */
  struct TALER_ExchangePublicKeyP exchange_pub;

  /**
   * Reference to the main AbortContext
   */
  struct AbortContext *ac;

  /**
   * Handle to the refund operation we are performing for
   * this coin, NULL after the operation is done.
   */
  struct TALER_EXCHANGE_RefundHandle *rh;

  /**
   * URL of the exchange that issued this coin.
   */
  char *exchange_url;

  /**
   * Body of the response from the exchange.  Note that the body returned MUST
   * be freed (if non-NULL).
   */
  json_t *exchange_reply;

  /**
   * Amount this coin contributes to the total purchase price.
   * This amount includes the deposit fee.
   */
  struct TALER_Amount amount_with_fee;

  /**
   * Offset of this coin into the `rd` array of all coins in the
   * @e ac.
   */
  unsigned int index;

  /**
   * HTTP status returned by the exchange (if any).
   */
  unsigned int http_status;

  /**
   * Did we try to process this refund yet?
   */
  bool processed;

  /**
   * Did we find the deposit in our own database?
   */
  bool found_deposit;
};


/**
 * Information we keep for an individual call to the /abort handler.
 */
struct AbortContext
{

  /**
   * Hashed contract terms (according to client).
   */
  struct TALER_PrivateContractHashP h_contract_terms;

  /**
   * Context for our operation.
   */
  struct TMH_HandlerContext *hc;

  /**
   * Stored in a DLL.
   */
  struct AbortContext *next;

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

  /**
   * Array with @e coins_cnt coins we are despositing.
   */
  struct RefundDetails *rd;

  /**
   * MHD connection to return to
   */
  struct MHD_Connection *connection;

  /**
   * Task called when the (suspended) processing for
   * the /abort request times out.
   * Happens when we don't get a response from the exchange.
   */
  struct GNUNET_SCHEDULER_Task *timeout_task;

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

  /**
   * Handle to the exchange that we are doing the abortment with.
   * (initially NULL while @e fo is trying to find a exchange).
   */
  struct TALER_EXCHANGE_Handle *mh;

  /**
   * Handle for operation to lookup /keys (and auditors) from
   * the exchange used for this transaction; NULL if no operation is
   * pending.
   */
  struct TMH_EXCHANGES_KeysOperation *fo;

  /**
   * URL of the exchange used for the last @e fo.
   */
  const char *current_exchange;

  /**
   * Number of coins this abort is for.  Length of the @e rd array.
   */
  size_t coins_cnt;

  /**
   * How often have we retried the 'main' transaction?
   */
  unsigned int retry_counter;

  /**
   * Number of transactions still pending.  Initially set to
   * @e coins_cnt, decremented on each transaction that
   * successfully finished.
   */
  size_t pending;

  /**
   * Number of transactions still pending for the currently selected
   * exchange.  Initially set to the number of coins started at the
   * exchange, decremented on each transaction that successfully
   * finished.  Once it hits zero, we pick the next exchange.
   */
  size_t pending_at_ce;

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

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

};


/**
 * Head of active abort context DLL.
 */
static struct AbortContext *ac_head;

/**
 * Tail of active abort context DLL.
 */
static struct AbortContext *ac_tail;


/**
 * Abort all pending /deposit operations.
 *
 * @param ac abort context to abort
 */
static void
abort_refunds (struct AbortContext *ac)
{
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Aborting pending /deposit operations\n");
  for (size_t i = 0; i<ac->coins_cnt; i++)
  {
    struct RefundDetails *rdi = &ac->rd[i];

    if (NULL != rdi->rh)
    {
      TALER_EXCHANGE_refund_cancel (rdi->rh);
      rdi->rh = NULL;
    }
  }
}


void
TMH_force_ac_resume ()
{
  for (struct AbortContext *ac = ac_head;
       NULL != ac;
       ac = ac->next)
  {
    abort_refunds (ac);
    if (NULL != ac->timeout_task)
    {
      GNUNET_SCHEDULER_cancel (ac->timeout_task);
      ac->timeout_task = NULL;
    }
    if (GNUNET_YES == ac->suspended)
    {
      ac->suspended = GNUNET_SYSERR;
      MHD_resume_connection (ac->connection);
    }
  }
}


/**
 * Resume the given abort context and send the given response.
 * Stores the response in the @a ac and signals MHD to resume
 * the connection.  Also ensures MHD runs immediately.
 *
 * @param ac abortment context
 * @param response_code response code to use
 * @param response response data to send back
 */
static void
resume_abort_with_response (struct AbortContext *ac,
                            unsigned int response_code,
                            struct MHD_Response *response)
{
  abort_refunds (ac);
  ac->response_code = response_code;
  ac->response = response;
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Resuming /abort handling as exchange interaction is done (%u)\n",
              response_code);
  if (NULL != ac->timeout_task)
  {
    GNUNET_SCHEDULER_cancel (ac->timeout_task);
    ac->timeout_task = NULL;
  }
  GNUNET_assert (GNUNET_YES == ac->suspended);
  ac->suspended = GNUNET_NO;
  MHD_resume_connection (ac->connection);
  TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
}


/**
 * Resume abortment processing with an error.
 *
 * @param ac operation to resume
 * @param http_status http status code to return
 * @param ec taler error code to return
 * @param msg human readable error message
 */
static void
resume_abort_with_error (struct AbortContext *ac,
                         unsigned int http_status,
                         enum TALER_ErrorCode ec,
                         const char *msg)
{
  resume_abort_with_response (ac,
                              http_status,
                              TALER_MHD_make_error (ec,
                                                    msg));
}


/**
 * Generate a response that indicates abortment success.
 *
 * @param ac abortment context
 */
static void
generate_success_response (struct AbortContext *ac)
{
  json_t *refunds;
  unsigned int hc = MHD_HTTP_OK;

  refunds = json_array ();
  if (NULL == refunds)
  {
    GNUNET_break (0);
    resume_abort_with_error (ac,
                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                             TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE,
                             "could not create JSON array");
    return;
  }
  for (size_t i = 0; i<ac->coins_cnt; i++)
  {
    struct RefundDetails *rdi = &ac->rd[i];
    json_t *detail;

    if (rdi->found_deposit)
    {
      if ( ( (MHD_HTTP_BAD_REQUEST <= rdi->http_status) &&
             (MHD_HTTP_NOT_FOUND != rdi->http_status) &&
             (MHD_HTTP_GONE != rdi->http_status) ) ||
           (0 == rdi->http_status) ||
           (NULL == rdi->exchange_reply) )
      {
        hc = MHD_HTTP_BAD_GATEWAY;
      }
    }
    if (! rdi->found_deposit)
    {
      detail = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_string ("type",
                                 "undeposited"));
    }
    else
    {
      if (MHD_HTTP_OK != rdi->http_status)
      {
        detail = GNUNET_JSON_PACK (
          GNUNET_JSON_pack_string ("type",
                                   "failure"),
          GNUNET_JSON_pack_uint64 ("exchange_status",
                                   rdi->http_status),
          GNUNET_JSON_pack_uint64 ("exchange_code",
                                   (NULL != rdi->exchange_reply)
                                   ? TALER_JSON_get_error_code (
                                     rdi->exchange_reply)
                                   : TALER_EC_GENERIC_INVALID_RESPONSE),
          GNUNET_JSON_pack_allow_null (
            GNUNET_JSON_pack_object_incref ("exchange_reply",
                                            rdi->exchange_reply)));
      }
      else
      {
        detail = GNUNET_JSON_PACK (
          GNUNET_JSON_pack_string ("type",
                                   "success"),
          GNUNET_JSON_pack_uint64 ("exchange_status",
                                   rdi->http_status),
          GNUNET_JSON_pack_data_auto ("exchange_sig",
                                      &rdi->exchange_sig),
          GNUNET_JSON_pack_data_auto ("exchange_pub",
                                      &rdi->exchange_pub));
      }
    }
    GNUNET_assert (0 ==
                   json_array_append_new (refunds,
                                          detail));
  }

  /* Resume and send back the response.  */
  resume_abort_with_response (
    ac,
    hc,
    TALER_MHD_MAKE_JSON_PACK (
      GNUNET_JSON_pack_array_steal ("refunds",
                                    refunds)));
}


/**
 * Custom cleanup routine for a `struct AbortContext`.
 *
 * @param cls the `struct AbortContext` to clean up.
 */
static void
abort_context_cleanup (void *cls)
{
  struct AbortContext *ac = cls;

  if (NULL != ac->timeout_task)
  {
    GNUNET_SCHEDULER_cancel (ac->timeout_task);
    ac->timeout_task = NULL;
  }
  abort_refunds (ac);
  for (size_t i = 0; i<ac->coins_cnt; i++)
  {
    struct RefundDetails *rdi = &ac->rd[i];

    if (NULL != rdi->exchange_reply)
    {
      json_decref (rdi->exchange_reply);
      rdi->exchange_reply = NULL;
    }
    GNUNET_free (rdi->exchange_url);
  }
  GNUNET_free (ac->rd);
  if (NULL != ac->fo)
  {
    TMH_EXCHANGES_keys4exchange_cancel (ac->fo);
    ac->fo = NULL;
  }
  if (NULL != ac->response)
  {
    MHD_destroy_response (ac->response);
    ac->response = NULL;
  }
  GNUNET_CONTAINER_DLL_remove (ac_head,
                               ac_tail,
                               ac);
  GNUNET_free (ac);
}


/**
 * Find the exchange we need to talk to for the next
 * pending deposit permission.
 *
 * @param ac abortment context we are processing
 */
static void
find_next_exchange (struct AbortContext *ac);


/**
 * Function called with the result from the exchange (to be
 * passed back to the wallet).
 *
 * @param cls closure
 * @param rr response data
 */
static void
refund_cb (void *cls,
           const struct TALER_EXCHANGE_RefundResponse *rr)
{
  struct RefundDetails *rd = cls;
  const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr;
  struct AbortContext *ac = rd->ac;

  rd->rh = NULL;
  rd->http_status = hr->http_status;
  rd->exchange_reply = json_incref ((json_t*) hr->reply);
  if (MHD_HTTP_OK == hr->http_status)
  {
    rd->exchange_pub = rr->details.ok.exchange_pub;
    rd->exchange_sig = rr->details.ok.exchange_sig;
  }
  ac->pending_at_ce--;
  if (0 == ac->pending_at_ce)
    find_next_exchange (ac);
}


/**
 * Function called with the result of our exchange lookup.
 *
 * @param cls the `struct AbortContext`
 * @param keys keys of the exchange
 * @param exchange representation of the exchange
 */
static void
process_abort_with_exchange (void *cls,
                             struct TALER_EXCHANGE_Keys *keys,
                             struct TMH_Exchange *exchange)
{
  struct AbortContext *ac = cls;

  (void) exchange;
  ac->fo = NULL;
  GNUNET_assert (GNUNET_YES == ac->suspended);
  if (NULL == keys)
  {
    resume_abort_with_response (
      ac,
      MHD_HTTP_GATEWAY_TIMEOUT,
      TALER_MHD_make_error (
        TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT,
        NULL));
    return;
  }
  /* Initiate refund operation for all coins of
     the current exchange (!) */
  GNUNET_assert (0 == ac->pending_at_ce);
  for (size_t i = 0; i<ac->coins_cnt; i++)
  {
    struct RefundDetails *rdi = &ac->rd[i];

    if (rdi->processed)
      continue;
    GNUNET_assert (NULL == rdi->rh);
    if (0 != strcmp (rdi->exchange_url,
                     ac->current_exchange))
      continue;
    rdi->processed = true;
    ac->pending--;
    if (! rdi->found_deposit)
    {
      /* Coin wasn't even deposited yet, we do not need to refund it. */
      continue;
    }
    rdi->rh = TALER_EXCHANGE_refund (
      TMH_curl_ctx,
      ac->current_exchange,
      keys,
      &rdi->amount_with_fee,
      &ac->h_contract_terms,
      &rdi->coin_pub,
      0,                                /* rtransaction_id */
      &ac->hc->instance->merchant_priv,
      &refund_cb,
      rdi);
    if (NULL == rdi->rh)
    {
      GNUNET_break_op (0);
      resume_abort_with_error (ac,
                               MHD_HTTP_INTERNAL_SERVER_ERROR,
                               TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED,
                               "Failed to start refund with exchange");
      return;
    }
    ac->pending_at_ce++;
  }
  /* Still continue if no coins for this exchange were deposited. */
  if (0 == ac->pending_at_ce)
    find_next_exchange (ac);
}


/**
 * Begin of the DB transaction.  If required (from
 * soft/serialization errors), the transaction can be
 * restarted here.
 *
 * @param ac abortment context to transact
 */
static void
begin_transaction (struct AbortContext *ac);


/**
 * Find the exchange we need to talk to for the next
 * pending deposit permission.
 *
 * @param ac abortment context we are processing
 */
static void
find_next_exchange (struct AbortContext *ac)
{
  for (size_t i = 0; i<ac->coins_cnt; i++)
  {
    struct RefundDetails *rdi = &ac->rd[i];

    if (! rdi->processed)
    {
      ac->current_exchange = rdi->exchange_url;
      ac->fo = TMH_EXCHANGES_keys4exchange (ac->current_exchange,
                                            false,
                                            &process_abort_with_exchange,
                                            ac);
      if (NULL == ac->fo)
      {
        /* strange, should have happened on pay! */
        GNUNET_break (0);
        resume_abort_with_error (ac,
                                 MHD_HTTP_INTERNAL_SERVER_ERROR,
                                 TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED,
                                 ac->current_exchange);
        return;
      }
      return;
    }
  }
  ac->current_exchange = NULL;
  GNUNET_assert (0 == ac->pending);
  /* We are done with all the HTTP requests, go back and try
     the 'big' database transaction! (It should work now!) */
  begin_transaction (ac);
}


/**
 * Function called with information about a coin that was deposited.
 *
 * @param cls closure
 * @param exchange_url exchange where @a coin_pub was deposited
 * @param coin_pub public key of the coin
 * @param amount_with_fee amount the exchange will deposit for this coin
 * @param deposit_fee fee the exchange will charge for this coin
 * @param refund_fee fee the exchange will charge for refunding this coin
 */
static void
refund_coins (void *cls,
              const char *exchange_url,
              const struct TALER_CoinSpendPublicKeyP *coin_pub,
              const struct TALER_Amount *amount_with_fee,
              const struct TALER_Amount *deposit_fee,
              const struct TALER_Amount *refund_fee)
{
  struct AbortContext *ac = cls;
  struct GNUNET_TIME_Timestamp now;

  (void) deposit_fee;
  (void) refund_fee;
  now = GNUNET_TIME_timestamp_get ();
  for (size_t i = 0; i<ac->coins_cnt; i++)
  {
    struct RefundDetails *rdi = &ac->rd[i];
    enum GNUNET_DB_QueryStatus qs;

    if ( (0 !=
          GNUNET_memcmp (coin_pub,
                         &rdi->coin_pub)) ||
         (0 !=
          strcmp (exchange_url,
                  rdi->exchange_url)) )
      continue; /* not in request */
    rdi->found_deposit = true;
    rdi->amount_with_fee = *amount_with_fee;
    /* Store refund in DB */
    qs = TMH_db->refund_coin (TMH_db->cls,
                              ac->hc->instance->settings.id,
                              &ac->h_contract_terms,
                              now,
                              coin_pub,
                              /* justification */
                              "incomplete abortment aborted");
    if (0 > qs)
    {
      TMH_db->rollback (TMH_db->cls);
      if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
      {
        begin_transaction (ac);
        return;
      }
      /* Always report on hard error as well to enable diagnostics */
      GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
      resume_abort_with_error (ac,
                               MHD_HTTP_INTERNAL_SERVER_ERROR,
                               TALER_EC_GENERIC_DB_STORE_FAILED,
                               "refund_coin");
      return;
    }
  } /* for all coins */
}


/**
 * Begin of the DB transaction.  If required (from soft/serialization errors),
 * the transaction can be restarted here.
 *
 * @param ac abortment context to transact
 */
static void
begin_transaction (struct AbortContext *ac)
{
  enum GNUNET_DB_QueryStatus qs;

  /* Avoid re-trying transactions on soft errors forever! */
  if (ac->retry_counter++ > MAX_RETRIES)
  {
    GNUNET_break (0);
    resume_abort_with_error (ac,
                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                             TALER_EC_GENERIC_DB_SOFT_FAILURE,
                             NULL);
    return;
  }
  GNUNET_assert (GNUNET_YES == ac->suspended);

  /* First, try to see if we have all we need already done */
  TMH_db->preflight (TMH_db->cls);
  if (GNUNET_OK !=
      TMH_db->start (TMH_db->cls,
                     "run abort"))
  {
    GNUNET_break (0);
    resume_abort_with_error (ac,
                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                             TALER_EC_GENERIC_DB_START_FAILED,
                             NULL);
    return;
  }

  /* check payment was indeed incomplete
     (now that we are in the transaction scope!) */
  {
    struct TALER_PrivateContractHashP h_contract_terms;
    bool paid;

    qs = TMH_db->lookup_order_status (TMH_db->cls,
                                      ac->hc->instance->settings.id,
                                      ac->hc->infix,
                                      &h_contract_terms,
                                      &paid);
    switch (qs)
    {
    case GNUNET_DB_STATUS_SOFT_ERROR:
    case GNUNET_DB_STATUS_HARD_ERROR:
      /* Always report on hard error to enable diagnostics */
      GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
      TMH_db->rollback (TMH_db->cls);
      if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
      {
        begin_transaction (ac);
        return;
      }
      /* Always report on hard error as well to enable diagnostics */
      GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
      resume_abort_with_error (ac,
                               MHD_HTTP_INTERNAL_SERVER_ERROR,
                               TALER_EC_GENERIC_DB_FETCH_FAILED,
                               "order status");
      return;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      TMH_db->rollback (TMH_db->cls);
      resume_abort_with_error (ac,
                               MHD_HTTP_NOT_FOUND,
                               TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND,
                               "Could not find contract");
      return;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      if (paid)
      {
        /* Payment is complete, refuse to abort. */
        TMH_db->rollback (TMH_db->cls);
        resume_abort_with_error (ac,
                                 MHD_HTTP_PRECONDITION_FAILED,
                                 TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE,
                                 "Payment was complete, refusing to abort");
        return;
      }
    }
    if (0 !=
        GNUNET_memcmp (&ac->h_contract_terms,
                       &h_contract_terms))
    {
      GNUNET_break_op (0);
      TMH_db->rollback (TMH_db->cls);
      resume_abort_with_error (ac,
                               MHD_HTTP_FORBIDDEN,
                               TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH,
                               "Provided hash does not match order on file");
      return;
    }
  }

  /* Mark all deposits we have in our database for the order as refunded. */
  qs = TMH_db->lookup_deposits (TMH_db->cls,
                                ac->hc->instance->settings.id,
                                &ac->h_contract_terms,
                                &refund_coins,
                                ac);
  if (0 > qs)
  {
    TMH_db->rollback (TMH_db->cls);
    if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    {
      begin_transaction (ac);
      return;
    }
    /* Always report on hard error as well to enable diagnostics */
    GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
    resume_abort_with_error (ac,
                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                             TALER_EC_GENERIC_DB_FETCH_FAILED,
                             "deposits");
    return;
  }

  qs = TMH_db->commit (TMH_db->cls);
  if (0 > qs)
  {
    TMH_db->rollback (TMH_db->cls);
    if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
    {
      begin_transaction (ac);
      return;
    }
    resume_abort_with_error (ac,
                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                             TALER_EC_GENERIC_DB_COMMIT_FAILED,
                             NULL);
    return;
  }

  /* At this point, the refund got correctly committed
     into the database. Tell exchange about abort/refund. */
  if (ac->pending > 0)
  {
    find_next_exchange (ac);
    return;
  }
  generate_success_response (ac);
}


/**
 * Try to parse the abort request into the given abort context.
 * Schedules an error response in the connection on failure.
 *
 * @param connection HTTP connection we are receiving abortment on
 * @param hc context we use to handle the abortment
 * @param ac state of the /abort call
 * @return #GNUNET_OK on success,
 *         #GNUNET_NO on failure (response was queued with MHD)
 *         #GNUNET_SYSERR on hard error (MHD connection must be dropped)
 */
static enum GNUNET_GenericReturnValue
parse_abort (struct MHD_Connection *connection,
             struct TMH_HandlerContext *hc,
             struct AbortContext *ac)
{
  const json_t *coins;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_array_const ("coins",
                                  &coins),
    GNUNET_JSON_spec_fixed_auto ("h_contract",
                                 &ac->h_contract_terms),

    GNUNET_JSON_spec_end ()
  };
  enum GNUNET_GenericReturnValue res;

  res = TALER_MHD_parse_json_data (connection,
                                   hc->request_body,
                                   spec);
  if (GNUNET_YES != res)
  {
    GNUNET_break_op (0);
    return res;
  }
  ac->coins_cnt = json_array_size (coins);
  if (0 == ac->coins_cnt)
  {
    GNUNET_break_op (0);
    return (MHD_YES ==
            TALER_MHD_reply_with_error (connection,
                                        MHD_HTTP_BAD_REQUEST,
                                        TALER_EC_MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY,
                                        "coins"))
      ? GNUNET_NO
      : GNUNET_SYSERR;
  }
  /* note: 1 coin = 1 deposit confirmation expected */
  ac->pending = ac->coins_cnt;
  ac->rd = GNUNET_new_array (ac->coins_cnt,
                             struct RefundDetails);
  /* This loop populates the array 'rd' in 'ac' */
  {
    unsigned int coins_index;
    json_t *coin;
    json_array_foreach (coins, coins_index, coin)
    {
      struct RefundDetails *rd = &ac->rd[coins_index];
      const char *exchange_url;
      struct GNUNET_JSON_Specification ispec[] = {
        TALER_JSON_spec_web_url ("exchange_url",
                                 &exchange_url),
        GNUNET_JSON_spec_fixed_auto ("coin_pub",
                                     &rd->coin_pub),
        GNUNET_JSON_spec_end ()
      };

      res = TALER_MHD_parse_json_data (connection,
                                       coin,
                                       ispec);
      if (GNUNET_YES != res)
      {
        GNUNET_break_op (0);
        return res;
      }
      rd->exchange_url = GNUNET_strdup (exchange_url);
      rd->index = coins_index;
      rd->ac = ac;
    }
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Handling /abort for order `%s' with contract hash `%s'\n",
              ac->hc->infix,
              GNUNET_h2s (&ac->h_contract_terms.hash));
  return GNUNET_OK;
}


/**
 * Handle a timeout for the processing of the abort request.
 *
 * @param cls our `struct AbortContext`
 */
static void
handle_abort_timeout (void *cls)
{
  struct AbortContext *ac = cls;

  ac->timeout_task = NULL;
  GNUNET_assert (GNUNET_YES == ac->suspended);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Resuming abort with error after timeout\n");
  if (NULL != ac->fo)
  {
    TMH_EXCHANGES_keys4exchange_cancel (ac->fo);
    ac->fo = NULL;
  }
  resume_abort_with_error (ac,
                           MHD_HTTP_GATEWAY_TIMEOUT,
                           TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT,
                           NULL);
}


MHD_RESULT
TMH_post_orders_ID_abort (const struct TMH_RequestHandler *rh,
                          struct MHD_Connection *connection,
                          struct TMH_HandlerContext *hc)
{
  struct AbortContext *ac = hc->ctx;

  if (NULL == ac)
  {
    ac = GNUNET_new (struct AbortContext);
    GNUNET_CONTAINER_DLL_insert (ac_head,
                                 ac_tail,
                                 ac);
    ac->connection = connection;
    ac->hc = hc;
    hc->ctx = ac;
    hc->cc = &abort_context_cleanup;
  }
  if (GNUNET_SYSERR == ac->suspended)
    return MHD_NO; /* during shutdown, we don't generate any more replies */
  if (0 != ac->response_code)
  {
    MHD_RESULT res;

    /* We are *done* processing the request,
       just queue the response (!) */
    if (UINT_MAX == ac->response_code)
    {
      GNUNET_break (0);
      return MHD_NO; /* hard error */
    }
    res = MHD_queue_response (connection,
                              ac->response_code,
                              ac->response);
    MHD_destroy_response (ac->response);
    ac->response = NULL;
    GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
                "Queueing response (%u) for /abort (%s).\n",
                (unsigned int) ac->response_code,
                res ? "OK" : "FAILED");
    return res;
  }
  {
    enum GNUNET_GenericReturnValue ret;

    ret = parse_abort (connection,
                       hc,
                       ac);
    if (GNUNET_OK != ret)
      return (GNUNET_NO == ret)
             ? MHD_YES
             : MHD_NO;
  }

  /* Abort not finished, suspend while we interact with the exchange */
  GNUNET_assert (GNUNET_NO == ac->suspended);
  MHD_suspend_connection (connection);
  ac->suspended = GNUNET_YES;
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Suspending abort handling while working with the exchange\n");
  ac->timeout_task = GNUNET_SCHEDULER_add_delayed (ABORT_GENERIC_TIMEOUT,
                                                   &handle_abort_timeout,
                                                   ac);
  begin_transaction (ac);
  return MHD_YES;
}


/* end of taler-merchant-httpd_post-orders-ID-abort.c */
