/*
  This file is part of TALER
  Copyright (C) 2019 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 Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file sync-httpd_backup_post.c
 * @brief functions to handle incoming requests for backups
 * @author Christian Grothoff
 */
#include "platform.h"
#include "sync-httpd.h"
#include <gnunet/gnunet_util_lib.h>
#include "sync-httpd_backup.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_merchant_service.h>
#include <taler/taler_signatures.h>


/**
 * How long do we hold an HTTP client connection if
 * we are awaiting payment before giving up?
 */
#define CHECK_PAYMENT_GENERIC_TIMEOUT GNUNET_TIME_relative_multiply ( \
    GNUNET_TIME_UNIT_MINUTES, 30)


/**
 * Context for an upload operation.
 */
struct BackupContext
{

  /**
   * Context for cleanup logic.
   */
  struct TM_HandlerContext hc;

  /**
   * Signature of the account holder.
   */
  struct SYNC_AccountSignatureP account_sig;

  /**
   * Public key of the account holder.
   */
  struct SYNC_AccountPublicKeyP account;

  /**
   * Hash of the previous upload, or zeros if first upload.
   */
  struct GNUNET_HashCode old_backup_hash;

  /**
   * Hash of the upload we are receiving right now (as promised
   * by the client, to be verified!).
   */
  struct GNUNET_HashCode new_backup_hash;

  /**
   * Claim token, all zeros if not known. Only set if @e existing_order_id is non-NULL.
   */
  struct TALER_ClaimTokenP token;

  /**
   * Hash context for the upload.
   */
  struct GNUNET_HashContext *hash_ctx;

  /**
   * Kept in DLL for shutdown handling while suspended.
   */
  struct BackupContext *next;

  /**
   * Kept in DLL for shutdown handling while suspended.
   */
  struct BackupContext *prev;

  /**
   * Used while suspended for resumption.
   */
  struct MHD_Connection *con;

  /**
   * Upload, with as many bytes as we have received so far.
   */
  char *upload;

  /**
   * Used while we are awaiting proposal creation.
   */
  struct TALER_MERCHANT_PostOrdersHandle *po;

  /**
   * Used while we are waiting payment.
   */
  struct TALER_MERCHANT_OrderMerchantGetHandle *omgh;

  /**
   * HTTP response code to use on resume, if non-NULL.

   */
  struct MHD_Response *resp;

  /**
   * Order under which the client promised payment, or NULL.
   */
  const char *order_id;

  /**
   * Order ID for the client that we found in our database.
   */
  char *existing_order_id;

  /**
   * Timestamp of the order in @e existing_order_id. Used to
   * select the most recent unpaid offer.
   */
  struct GNUNET_TIME_Timestamp existing_order_timestamp;

  /**
   * Expected total upload size.
   */
  size_t upload_size;

  /**
   * Current offset for the upload.
   */
  size_t upload_off;

  /**
   * HTTP response code to use on resume, if resp is set.
   */
  unsigned int response_code;

  /**
   * Do not look for an existing order, force a fresh order to be created.
   */
  bool force_fresh_order;
};


/**
 * Kept in DLL for shutdown handling while suspended.
 */
static struct BackupContext *bc_head;

/**
 * Kept in DLL for shutdown handling while suspended.
 */
static struct BackupContext *bc_tail;


/**
 * Service is shutting down, resume all MHD connections NOW.
 */
void
SH_resume_all_bc ()
{
  struct BackupContext *bc;

  while (NULL != (bc = bc_head))
  {
    GNUNET_CONTAINER_DLL_remove (bc_head,
                                 bc_tail,
                                 bc);
    MHD_resume_connection (bc->con);
    if (NULL != bc->po)
    {
      TALER_MERCHANT_orders_post_cancel (bc->po);
      bc->po = NULL;
    }
    if (NULL != bc->omgh)
    {
      TALER_MERCHANT_merchant_order_get_cancel (bc->omgh);
      bc->omgh = NULL;
    }
  }
}


/**
 * Function called to clean up a backup context.
 *
 * @param hc a `struct BackupContext`
 */
static void
cleanup_ctx (struct TM_HandlerContext *hc)
{
  struct BackupContext *bc = (struct BackupContext *) hc;

  if (NULL != bc->po)
    TALER_MERCHANT_orders_post_cancel (bc->po);
  if (NULL != bc->hash_ctx)
    GNUNET_CRYPTO_hash_context_abort (bc->hash_ctx);
  if (NULL != bc->resp)
    MHD_destroy_response (bc->resp);
  GNUNET_free (bc->existing_order_id);
  GNUNET_free (bc->upload);
  GNUNET_free (bc);
}


/**
 * Transmit a payment request for @a order_id on @a connection
 *
 * @param order_id our backend's order ID
 * @param token the claim token generated by the merchant (NULL if
 *        it wasn't generated).
 * @return MHD response to use
 */
static struct MHD_Response *
make_payment_request (const char *order_id,
                      const struct TALER_ClaimTokenP *token)
{
  struct MHD_Response *resp;

  /* request payment via Taler */
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Creating payment request for order `%s'\n",
              order_id);
  resp = MHD_create_response_from_buffer (0,
                                          NULL,
                                          MHD_RESPMEM_PERSISTENT);
  TALER_MHD_add_global_headers (resp);
  {
    char *hdr;
    char *pfx;
    char *hn;
    struct GNUNET_Buffer hdr_buf = { 0 };

    if (0 == strncasecmp ("https://",
                          SH_backend_url,
                          strlen ("https://")))
    {
      pfx = "taler://";
      hn = &SH_backend_url[strlen ("https://")];
    }
    else if (0 == strncasecmp ("http://",
                               SH_backend_url,
                               strlen ("http://")))
    {
      pfx = "taler+http://";
      hn = &SH_backend_url[strlen ("http://")];
    }
    else
    {
      GNUNET_break (0);
      MHD_destroy_response (resp);
      return NULL;
    }
    if (0 == strlen (hn))
    {
      GNUNET_break (0);
      MHD_destroy_response (resp);
      return NULL;
    }

    GNUNET_buffer_write_str (&hdr_buf, pfx);
    GNUNET_buffer_write_str (&hdr_buf, "pay/");
    GNUNET_buffer_write_str (&hdr_buf, hn);
    GNUNET_buffer_write_path (&hdr_buf, order_id);
    /* No session ID */
    GNUNET_buffer_write_path (&hdr_buf, "");
    if (NULL != token)
    {
      GNUNET_buffer_write_str (&hdr_buf, "?c=");
      GNUNET_buffer_write_data_encoded (&hdr_buf, token, sizeof (*token));
    }
    hdr = GNUNET_buffer_reap_str (&hdr_buf);
    GNUNET_break (MHD_YES ==
                  MHD_add_response_header (resp,
                                           "Taler",
                                           hdr));
    GNUNET_free (hdr);
  }
  return resp;
}


/**
 * Callbacks of this type are used to serve the result of submitting a
 * /contract request to a merchant.
 *
 * @param cls our `struct BackupContext`
 * @param por response details
 */
static void
proposal_cb (void *cls,
             const struct TALER_MERCHANT_PostOrdersReply *por)
{
  struct BackupContext *bc = cls;
  enum SYNC_DB_QueryStatus qs;

  bc->po = NULL;
  GNUNET_CONTAINER_DLL_remove (bc_head,
                               bc_tail,
                               bc);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Resuming connection with order `%s'\n",
              bc->order_id);
  MHD_resume_connection (bc->con);
  SH_trigger_daemon ();
  if (MHD_HTTP_OK != por->hr.http_status)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Backend returned status %u/%u\n",
                por->hr.http_status,
                (unsigned int) por->hr.ec);
    GNUNET_break_op (0);
    bc->resp = TALER_MHD_MAKE_JSON_PACK (
      TALER_JSON_pack_ec (TALER_EC_SYNC_PAYMENT_CREATE_BACKEND_ERROR),
      GNUNET_JSON_pack_uint64 ("backend-ec",
                               por->hr.ec),
      GNUNET_JSON_pack_uint64 ("backend-http-status",
                               por->hr.http_status),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_object_incref ("backend-reply",
                                        (json_t *) por->hr.reply)));
    bc->response_code = MHD_HTTP_BAD_GATEWAY;
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Storing payment request for order `%s'\n",
              por->details.ok.order_id);
  qs = db->store_payment_TR (db->cls,
                             &bc->account,
                             por->details.ok.order_id,
                             por->details.ok.token,
                             &SH_annual_fee);
  if (0 >= qs)
  {
    GNUNET_break (0);
    bc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
                                     "Failed to persist payment request in sync database");
    GNUNET_assert (NULL != bc->resp);
    bc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Obtained fresh order `%s'\n",
              por->details.ok.order_id);
  bc->resp = make_payment_request (por->details.ok.order_id,
                                   por->details.ok.token);
  GNUNET_assert (NULL != bc->resp);
  bc->response_code = MHD_HTTP_PAYMENT_REQUIRED;
}


/**
 * Function called on all pending payments for the right
 * account.
 *
 * @param cls closure, our `struct BackupContext`
 * @param timestamp for how long have we been waiting
 * @param order_id order id in the backend
 * @param token claim token to use (or NULL for none)
 * @param amount how much is the order for
 */
static void
ongoing_payment_cb (void *cls,
                    struct GNUNET_TIME_Timestamp timestamp,
                    const char *order_id,
                    const struct TALER_ClaimTokenP *token,
                    const struct TALER_Amount *amount)
{
  struct BackupContext *bc = cls;

  (void) amount;
  if (0 != TALER_amount_cmp (amount,
                             &SH_annual_fee))
    return; /* can't reuse, fees changed */
  if ( (NULL == bc->existing_order_id) ||
       (GNUNET_TIME_timestamp_cmp (bc->existing_order_timestamp,
                                   <,
                                   timestamp)) )
  {
    GNUNET_free (bc->existing_order_id);
    bc->existing_order_id = GNUNET_strdup (order_id);
    bc->existing_order_timestamp = timestamp;
    if (NULL != token)
      bc->token = *token;
  }
}


/**
 * Callback to process a GET /check-payment request
 *
 * @param cls our `struct BackupContext`
 * @param osr order status
 */
static void
check_payment_cb (void *cls,
                  const struct TALER_MERCHANT_OrderStatusResponse *osr)
{
  struct BackupContext *bc = cls;
  const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr;

  /* refunds are not supported, verify */
  bc->omgh = NULL;
  GNUNET_CONTAINER_DLL_remove (bc_head,
                               bc_tail,
                               bc);
  MHD_resume_connection (bc->con);
  SH_trigger_daemon ();
  switch (hr->http_status)
  {
  case 0:
    /* Likely timeout, complain! */
    bc->response_code = MHD_HTTP_GATEWAY_TIMEOUT;
    bc->resp = TALER_MHD_make_error (
      TALER_EC_SYNC_GENERIC_BACKEND_TIMEOUT,
      NULL);
    return;
  case MHD_HTTP_OK:
    break; /* handled below */
  default:
    /* Unexpected backend response */
    bc->response_code = MHD_HTTP_BAD_GATEWAY;
    bc->resp = TALER_MHD_MAKE_JSON_PACK (
      GNUNET_JSON_pack_uint64 ("code",
                               TALER_EC_SYNC_GENERIC_BACKEND_ERROR),
      GNUNET_JSON_pack_string ("hint",
                               TALER_ErrorCode_get_hint (
                                 TALER_EC_SYNC_GENERIC_BACKEND_ERROR)),
      GNUNET_JSON_pack_uint64 ("backend-ec",
                               (json_int_t) hr->ec),
      GNUNET_JSON_pack_uint64 ("backend-http-status",
                               (json_int_t) hr->http_status),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_object_incref ("backend-reply",
                                        (json_t *) hr->reply)));
    return;
  }

  GNUNET_assert (MHD_HTTP_OK == hr->http_status);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Payment status checked: %d\n",
              osr->details.ok.status);
  switch (osr->details.ok.status)
  {
  case TALER_MERCHANT_OSC_PAID:
    {
      enum SYNC_DB_QueryStatus qs;

      qs = db->increment_lifetime_TR (db->cls,
                                      &bc->account,
                                      bc->order_id,
                                      GNUNET_TIME_UNIT_YEARS); /* always annual */
      if (0 <= qs)
        return; /* continue as planned */
      GNUNET_break (0);
      bc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED,
                                       "increment lifetime");
      GNUNET_assert (NULL != bc->resp);
      bc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR;
      return; /* continue as planned */
    }
  case TALER_MERCHANT_OSC_UNPAID:
  case TALER_MERCHANT_OSC_CLAIMED:
    break;
  }
  if (NULL != bc->existing_order_id)
  {
    /* repeat payment request */
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Repeating payment request\n");
    bc->resp = make_payment_request (bc->existing_order_id,
                                     (GNUNET_YES == GNUNET_is_zero (&bc->token))
                                     ? NULL
                                     : &bc->token);
    GNUNET_assert (NULL != bc->resp);
    bc->response_code = MHD_HTTP_PAYMENT_REQUIRED;
    return;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Timeout waiting for payment\n");
  bc->resp = TALER_MHD_make_error (TALER_EC_SYNC_PAYMENT_GENERIC_TIMEOUT,
                                   "Timeout awaiting promised payment");
  GNUNET_assert (NULL != bc->resp);
  bc->response_code = MHD_HTTP_REQUEST_TIMEOUT;
}


/**
 * Helper function used to ask our backend to await
 * a payment for the user's account.
 *
 * @param bc context to begin payment for.
 * @param timeout when to give up trying
 * @param order_id which order to check for the payment
 */
static void
await_payment (struct BackupContext *bc,
               struct GNUNET_TIME_Relative timeout,
               const char *order_id)
{
  GNUNET_CONTAINER_DLL_insert (bc_head,
                               bc_tail,
                               bc);
  MHD_suspend_connection (bc->con);
  bc->order_id = order_id;
  bc->omgh = TALER_MERCHANT_merchant_order_get (SH_ctx,
                                                SH_backend_url,
                                                order_id,
                                                NULL /* our payments are NOT session-bound */,
                                                timeout,
                                                &check_payment_cb,
                                                bc);
  SH_trigger_curl ();
}


/**
 * Helper function used to ask our backend to begin
 * processing a payment for the user's account.
 * May perform asynchronous operations by suspending the connection
 * if required.
 *
 * @param bc context to begin payment for.
 * @param pay_req #GNUNET_YES if payment was explicitly requested,
 *                #GNUNET_NO if payment is needed
 * @return MHD status code
 */
static MHD_RESULT
begin_payment (struct BackupContext *bc,
               int pay_req)
{
  static const char *no_uuids[1] = { NULL };
  json_t *order;

  if (! bc->force_fresh_order)
  {
    enum GNUNET_DB_QueryStatus qs;

    qs = db->lookup_pending_payments_by_account_TR (db->cls,
                                                    &bc->account,
                                                    &ongoing_payment_cb,
                                                    bc);
    if (qs < 0)
    {
      struct MHD_Response *resp;
      MHD_RESULT ret;

      resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_FETCH_FAILED,
                                   "pending payments");
      ret = MHD_queue_response (bc->con,
                                MHD_HTTP_INTERNAL_SERVER_ERROR,
                                resp);
      GNUNET_break (MHD_YES == ret);
      MHD_destroy_response (resp);
      return ret;
    }
    if (NULL != bc->existing_order_id)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Have existing order, waiting for `%s' to complete\n",
                  bc->existing_order_id);
      await_payment (bc,
                     GNUNET_TIME_UNIT_ZERO /* no long polling */,
                     bc->existing_order_id);
      return MHD_YES;
    }
  }
  GNUNET_CONTAINER_DLL_insert (bc_head,
                               bc_tail,
                               bc);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Suspending connection while creating order at `%s'\n",
              SH_backend_url);
  MHD_suspend_connection (bc->con);
  order = GNUNET_JSON_PACK (
    TALER_JSON_pack_amount ("amount",
                            &SH_annual_fee),
    GNUNET_JSON_pack_string ("summary",
                             "annual fee for sync service"),
    GNUNET_JSON_pack_string ("fulfillment_url",
                             SH_fulfillment_url));
  bc->po = TALER_MERCHANT_orders_post2 (SH_ctx,
                                        SH_backend_url,
                                        order,
                                        GNUNET_TIME_UNIT_ZERO,
                                        NULL, /* no payment target */
                                        0,
                                        NULL, /* no inventory products */
                                        0,
                                        no_uuids, /* no uuids */
                                        false, /* do NOT require claim token */
                                        &proposal_cb,
                                        bc);
  SH_trigger_curl ();
  json_decref (order);
  return MHD_YES;
}


/**
 * We got some query status from the DB.  Handle the error cases.
 * May perform asynchronous operations by suspending the connection
 * if required.
 *
 * @param bc connection to handle status for
 * @param qs query status to handle
 * @return #MHD_YES or #MHD_NO
 */
static MHD_RESULT
handle_database_error (struct BackupContext *bc,
                       enum SYNC_DB_QueryStatus qs)
{
  switch (qs)
  {
  case SYNC_DB_OLD_BACKUP_MISSING:
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Update failed: no existing backup\n");
    return TALER_MHD_reply_with_error (bc->con,
                                       MHD_HTTP_NOT_FOUND,
                                       TALER_EC_SYNC_PREVIOUS_BACKUP_UNKNOWN,
                                       NULL);
  case SYNC_DB_OLD_BACKUP_MISMATCH:
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Conflict detected, returning existing backup\n");
    return SH_return_backup (bc->con,
                             &bc->account,
                             MHD_HTTP_CONFLICT);
  case SYNC_DB_PAYMENT_REQUIRED:
    {
      const char *order_id;

      order_id = MHD_lookup_connection_value (bc->con,
                                              MHD_GET_ARGUMENT_KIND,
                                              "paying");
      if (NULL == order_id)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Payment required, starting payment process\n");
        return begin_payment (bc,
                              GNUNET_NO);
      }
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Payment required, awaiting completion of `%s'\n",
                  order_id);
      await_payment (bc,
                     CHECK_PAYMENT_GENERIC_TIMEOUT,
                     order_id);
    }
    return MHD_YES;
  case SYNC_DB_HARD_ERROR:
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (bc->con,
                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
                                       TALER_EC_GENERIC_DB_COMMIT_FAILED,
                                       NULL);
  case SYNC_DB_SOFT_ERROR:
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (bc->con,
                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
                                       TALER_EC_GENERIC_DB_SOFT_FAILURE,
                                       NULL);
  case SYNC_DB_NO_RESULTS:
    GNUNET_assert (0);
    return MHD_NO;
  /* intentional fall-through! */
  case SYNC_DB_ONE_RESULT:
    GNUNET_assert (0);
    return MHD_NO;
  }
  GNUNET_break (0);
  return MHD_NO;
}


MHD_RESULT
SH_backup_post (struct MHD_Connection *connection,
                void **con_cls,
                const struct SYNC_AccountPublicKeyP *account,
                const char *upload_data,
                size_t *upload_data_size)
{
  struct BackupContext *bc;

  bc = *con_cls;
  if (NULL == bc)
  {
    /* first call, setup internals */
    bc = GNUNET_new (struct BackupContext);
    bc->hc.cc = &cleanup_ctx;
    bc->con = connection;
    bc->account = *account;
    {
      const char *fresh;

      fresh = MHD_lookup_connection_value (connection,
                                           MHD_GET_ARGUMENT_KIND,
                                           "fresh");
      if (NULL != fresh)
        bc->force_fresh_order = true;
    }
    *con_cls = bc;

    /* now setup 'bc' */
    {
      const char *lens;
      unsigned long len;

      lens = MHD_lookup_connection_value (connection,
                                          MHD_HEADER_KIND,
                                          MHD_HTTP_HEADER_CONTENT_LENGTH);
      if ( (NULL == lens) ||
           (1 !=
            sscanf (lens,
                    "%lu",
                    &len)) )
      {
        GNUNET_break_op (0);
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_BAD_REQUEST,
          (NULL == lens)
          ? TALER_EC_SYNC_MALFORMED_CONTENT_LENGTH
          : TALER_EC_SYNC_MISSING_CONTENT_LENGTH,
          lens);
      }
      if (len / 1024 / 1024 >= SH_upload_limit_mb)
      {
        GNUNET_break_op (0);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_PAYLOAD_TOO_LARGE,
                                           TALER_EC_SYNC_EXCESSIVE_CONTENT_LENGTH,
                                           NULL);
      }
      bc->upload = GNUNET_malloc_large (len);
      if (NULL == bc->upload)
      {
        GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                             "malloc");
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_PAYLOAD_TOO_LARGE,
                                           TALER_EC_SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH,
                                           NULL);
      }
      bc->upload_size = (size_t) len;
    }
    {
      const char *im;

      im = MHD_lookup_connection_value (connection,
                                        MHD_HEADER_KIND,
                                        MHD_HTTP_HEADER_IF_MATCH);
      if (NULL != im)
      {
        if ( (2 >= strlen (im)) ||
             ('"' != im[0]) ||
             ('"' != im[strlen (im) - 1]) ||
             (GNUNET_OK !=
              GNUNET_STRINGS_string_to_data (im + 1,
                                             strlen (im) - 2,
                                             &bc->old_backup_hash,
                                             sizeof (bc->old_backup_hash))) )
        {
          GNUNET_break_op (0);
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_BAD_REQUEST,
                                             TALER_EC_SYNC_BAD_IF_MATCH,
                                             NULL);
        }
      }
    }
    {
      const char *sig_s;

      sig_s = MHD_lookup_connection_value (connection,
                                           MHD_HEADER_KIND,
                                           "Sync-Signature");
      if ( (NULL == sig_s) ||
           (GNUNET_OK !=
            GNUNET_STRINGS_string_to_data (sig_s,
                                           strlen (sig_s),
                                           &bc->account_sig,
                                           sizeof (bc->account_sig))) )
      {
        GNUNET_break_op (0);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_BAD_REQUEST,
                                           TALER_EC_SYNC_BAD_SYNC_SIGNATURE,
                                           NULL);
      }
    }
    {
      const char *etag;

      etag = MHD_lookup_connection_value (connection,
                                          MHD_HEADER_KIND,
                                          MHD_HTTP_HEADER_IF_NONE_MATCH);
      if ( (NULL == etag) ||
           (2 >= strlen (etag)) ||
           ('"' != etag[0]) ||
           ('"' != etag[strlen (etag) - 1]) ||
           (GNUNET_OK !=
            GNUNET_STRINGS_string_to_data (etag + 1,
                                           strlen (etag) - 2,
                                           &bc->new_backup_hash,
                                           sizeof (bc->new_backup_hash))) )
      {
        GNUNET_break_op (0);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_BAD_REQUEST,
                                           TALER_EC_SYNC_BAD_IF_NONE_MATCH,
                                           NULL);
      }
    }
    /* validate signature */
    {
      struct SYNC_UploadSignaturePS usp = {
        .purpose.size = htonl (sizeof (usp)),
        .purpose.purpose = htonl (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD),
        .old_backup_hash = bc->old_backup_hash,
        .new_backup_hash = bc->new_backup_hash
      };

      if (GNUNET_OK !=
          GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_SYNC_BACKUP_UPLOAD,
                                      &usp,
                                      &bc->account_sig.eddsa_sig,
                                      &account->eddsa_pub))
      {
        GNUNET_break_op (0);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_FORBIDDEN,
                                           TALER_EC_SYNC_INVALID_SIGNATURE,
                                           NULL);
      }
    }
    /* get ready to hash (done here as we may go async for payments next) */
    bc->hash_ctx = GNUNET_CRYPTO_hash_context_start ();

    /* Check database to see if the transaction is permissible */
    {
      struct GNUNET_HashCode hc;
      enum SYNC_DB_QueryStatus qs;

      qs = db->lookup_account_TR (db->cls,
                                  account,
                                  &hc);
      if (qs < 0)
        return handle_database_error (bc,
                                      qs);
      if (SYNC_DB_NO_RESULTS == qs)
        memset (&hc, 0, sizeof (hc));
      if (0 == GNUNET_memcmp (&hc,
                              &bc->new_backup_hash))
      {
        /* Refuse upload: we already have that backup! */
        struct MHD_Response *resp;
        MHD_RESULT ret;

        resp = MHD_create_response_from_buffer (0,
                                                NULL,
                                                MHD_RESPMEM_PERSISTENT);
        TALER_MHD_add_global_headers (resp);
        ret = MHD_queue_response (connection,
                                  MHD_HTTP_NOT_MODIFIED,
                                  resp);
        GNUNET_break (MHD_YES == ret);
        MHD_destroy_response (resp);
        return ret;
      }
      if (0 != GNUNET_memcmp (&hc,
                              &bc->old_backup_hash))
      {
        /* Refuse upload: if-none-match failed! */
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Conflict detected, returning existing backup\n");
        return SH_return_backup (connection,
                                 account,
                                 MHD_HTTP_CONFLICT);
      }
    }
    /* check if the client insists on paying */
    {
      const char *order_req;

      order_req = MHD_lookup_connection_value (connection,
                                               MHD_GET_ARGUMENT_KIND,
                                               "pay");
      if (NULL != order_req)
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Payment requested, starting payment process\n");
        return begin_payment (bc,
                              GNUNET_YES);
      }
    }
    /* ready to begin! */
    return MHD_YES;
  }
  /* handle upload */
  if (0 != *upload_data_size)
  {
    /* check MHD invariant */
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Processing %u bytes of upload data\n",
                (unsigned int) *upload_data_size);
    GNUNET_assert (bc->upload_off + *upload_data_size <= bc->upload_size);
    memcpy (&bc->upload[bc->upload_off],
            upload_data,
            *upload_data_size);
    bc->upload_off += *upload_data_size;
    GNUNET_CRYPTO_hash_context_read (bc->hash_ctx,
                                     upload_data,
                                     *upload_data_size);
    *upload_data_size = 0;
    return MHD_YES;
  }
  else if ( (0 == bc->upload_off) &&
            (0 != bc->upload_size) &&
            (NULL == bc->resp) )
  {
    /* wait for upload */
    return MHD_YES;
  }
  if (NULL != bc->resp)
  {
    MHD_RESULT ret;

    /* We generated a response asynchronously, queue that */
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Returning asynchronously generated response with HTTP status %u\n",
                bc->response_code);
    ret = MHD_queue_response (connection,
                              bc->response_code,
                              bc->resp);
    GNUNET_break (MHD_YES == ret);
    MHD_destroy_response (bc->resp);
    bc->resp = NULL;
    return ret;
  }

  /* finished with upload, check hash */
  {
    struct GNUNET_HashCode our_hash;

    GNUNET_CRYPTO_hash_context_finish (bc->hash_ctx,
                                       &our_hash);
    bc->hash_ctx = NULL;
    if (0 != GNUNET_memcmp (&our_hash,
                            &bc->new_backup_hash))
    {
      GNUNET_break_op (0);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_BAD_REQUEST,
                                         TALER_EC_SYNC_INVALID_UPLOAD,
                                         NULL);
    }
  }

  /* store backup to database */
  {
    enum SYNC_DB_QueryStatus qs;

    if (GNUNET_YES == GNUNET_is_zero (&bc->old_backup_hash))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Uploading first backup to account\n");
      qs = db->store_backup_TR (db->cls,
                                account,
                                &bc->account_sig,
                                &bc->new_backup_hash,
                                bc->upload_size,
                                bc->upload);
    }
    else
    {
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Uploading existing backup of account\n");
      qs = db->update_backup_TR (db->cls,
                                 account,
                                 &bc->old_backup_hash,
                                 &bc->account_sig,
                                 &bc->new_backup_hash,
                                 bc->upload_size,
                                 bc->upload);
    }
    if (qs < 0)
      return handle_database_error (bc,
                                    qs);
    if (0 == qs)
    {
      /* database says nothing actually changed, 304 (could
         theoretically happen if another equivalent upload succeeded
         since we last checked!) */
      struct MHD_Response *resp;
      MHD_RESULT ret;

      resp = MHD_create_response_from_buffer (0,
                                              NULL,
                                              MHD_RESPMEM_PERSISTENT);
      TALER_MHD_add_global_headers (resp);
      ret = MHD_queue_response (connection,
                                MHD_HTTP_NOT_MODIFIED,
                                resp);
      GNUNET_break (MHD_YES == ret);
      MHD_destroy_response (resp);
      return ret;
    }
  }

  /* generate main (204) standard success reply */
  {
    struct MHD_Response *resp;
    MHD_RESULT ret;

    resp = MHD_create_response_from_buffer (0,
                                            NULL,
                                            MHD_RESPMEM_PERSISTENT);
    TALER_MHD_add_global_headers (resp);
    ret = MHD_queue_response (connection,
                              MHD_HTTP_NO_CONTENT,
                              resp);
    GNUNET_break (MHD_YES == ret);
    MHD_destroy_response (resp);
    return ret;
  }
}
