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

  You should have received a copy of the GNU Lesser General Public License along with
  TALER; see the file COPYING.LIB.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file taler_merchant_pay_service.c
 * @brief Implementation of the the ideology
 *        from the pay_service as copy of
 *        merchant_api_post_order_pay.c
 * @author Bohdan Potuzhnyi
 */
#include "platform.h"
#include <curl/curl.h>
#include <gnunet/gnunet_common.h>
#include <gnunet/gnunet_json_lib.h>
#include <jansson.h>
#include <microhttpd.h>
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_curl_lib.h>
#include "taler_merchant_service.h"
#include "taler_merchant_pay_service.h"
#include "merchant_api_common.h"
#include "merchant_api_curl_defaults.h"
#include <stdio.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_signatures.h>
#include <taler/taler_exchange_service.h>
#include <taler/taler_curl_lib.h>

/**
 * @brief A Pay Handle
 */
struct TALER_MERCHANT_OrderPayHandle
{
  /**
   * Reference to the GNUNET CURL execution context.
   */
  struct GNUNET_CURL_Context   *ctx;

  /**
   * Callback to invoke with the payment result ("pay" mode).
   */
  TALER_MERCHANT_OrderPayCallback cb;

  /**
   * Closure data for @a cb.
   */
  TALER_MERCHANT_ORDER_PAY_CALLBACK_CLOSURE_TYPE *cb_cls;

  /* Mandatory scalars: */

  /**
   * Base URL of the merchant service.
   */
  char *merchant_url;

  /**
   * Identifier of the order being paid.
   */
  char *order_id;

  /**
   * Session identifier for this payment attempt.
   */
  char *session_id;

  /**
   * Timestamp when the payment request was created.
   */
  struct GNUNET_TIME_Timestamp timestamp;

  /**
   * Deadline after which refunds are no longer allowed.
   */
  struct GNUNET_TIME_Timestamp refund_deadline;

  /**
   * Wire hash for communicating payment details.
   */
  struct TALER_MerchantWireHashP h_wire;

  /**
   * Indicates whether @a h_wire has been set.
   */
  bool has_h_wire;

  /* Wallet mode fields: */

  /**
   * Indicates whether a contract hash was provided.
   */
  bool has_h_contract;

  /**
   * Hash of the private contract terms (wallet mode only).
   */
  struct TALER_PrivateContractHashP h_contract_terms;

  /**
   * Indicates whether the merchant public key was provided.
   */
  bool has_merchant_pub;

  /**
   * Merchant’s public key for verifying signatures (wallet mode).
   */
  struct TALER_MerchantPublicKeyP merchant_pub;

  /**
   * Indicates whether a choice index was provided.
   */
  bool has_choice_index;

  /**
   * Selected index of the contract choice (for token operations).
   */
  int choice_index;

  /**
   * Legacy: pointer to the amount structure for strcmp checks.
   */
  const struct TALER_Amount *amount;

  /**
   * Legacy: pointer to the maximum fee structure for strcmp checks.
   */
  const struct TALER_Amount *max_fee;

  /* Raw arrays as passed in via set_options(): */

  /**
   * Coins used for payment.
   */
  struct
  {
    /**
     * Number of coins provided.
     */
    unsigned int num_coins;
    /**
     * Array of coins to spend.
     */
    const struct TALER_MERCHANT_PayCoin *coins;
  } coins;

  /**
   * Input tokens to use (wallet mode).
   */
  struct
  {
    /**
     * Number of tokens provided.
     */
    unsigned int num_tokens;
    /**
     * Array of tokens to redeem.
     */
    const struct TALER_MERCHANT_UseToken *tokens;
  } input_tokens;

  /**
   * Output tokens expected from the merchant.
   */
  struct
  {
    /**
     * Number of output tokens expected.
     */
    unsigned int num_output_tokens;
    /**
     * Array of expected output tokens.
     */
    const struct TALER_MERCHANT_OutputToken *output_tokens;
  } output_tokens;

  /**
   * JSON array of token envelope events (from Donau).
   */
  json_t *tokens_evs;

  /* Computed once both choice_index and tokens_evs are available: */

  /**
   * JSON object containing wallet-specific data payload.
   */
  json_t *wallet_data;

  /**
   * Hash code of @a wallet_data for integrity checks.
   */
  struct GNUNET_HashCode wallet_data_hash;

  /**
   * JSON body being constructed for the HTTP POST.
   */
  json_t *body;

  /* Final URL and CURL plumbing: */

  /**
   * Fully formed URL for the POST /order/$ID/pay request.
   */
  char *url;

  /**
   * CURL post context managing headers and body.
   */
  struct TALER_CURL_PostContext post_ctx;

  /**
   * Handle for the asynchronous CURL job.
   */
  struct GNUNET_CURL_Job *job;

  /**
   * Flags indicating which payment options have been set.
   */
  bool field_seen[TALER_MERCHANT_OrderPayOptionType_LENGTH];

  /**
   * True if operating in wallet mode (using tokens/contracts).
   */
  bool am_wallet;

  /**
   * Raw JSON data of `donau` for `wallet_data`.
   */
  json_t *donau_data;
};

/**
 * Parse blindly signed output tokens from response.
 *
 * @param token_sigs the JSON array with the token signatures. Can be NULL.
 * @param tokens where to store the parsed tokens.
 * @param num_tokens where to store the length of the @a tokens array.
 */
static enum GNUNET_GenericReturnValue
parse_tokens (const json_t *token_sigs,
              struct TALER_MERCHANT_OutputToken **tokens,
              unsigned int *num_tokens)
{
  GNUNET_array_grow (*tokens,
                     *num_tokens,
                     json_array_size (token_sigs));

  for (unsigned int i = 0; i<(*num_tokens); i++)
  {
    struct TALER_MERCHANT_OutputToken *token = &(*tokens)[i];
    struct GNUNET_JSON_Specification spec[] = {
      TALER_JSON_spec_blinded_token_issue_sig ("blind_sig",
                                               &token->blinded_sig),
      GNUNET_JSON_spec_end ()
    };
    const json_t *jtoken
      = json_array_get (token_sigs,
                        i);

    if (NULL == jtoken)
    {
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
    if (GNUNET_OK !=
        GNUNET_JSON_parse (jtoken,
                           spec,
                           NULL, NULL))
    {
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
  }

  return GNUNET_YES;
}


/**
 * Function called when we're done processing the
 * HTTP /pay request.
 *
 * @param cls the `struct TALER_MERCHANT_Pay`
 * @param response_code HTTP response code, 0 on error
 * @param resp response body, NULL if not in JSON
 */
static void
handle_finished (void *cls,
                 long response_code,
                 const void *resp)
{
  struct TALER_MERCHANT_OrderPayHandle *oph = cls;
  const json_t *json = resp;
  struct TALER_MERCHANT_PayResponse pr = {
    .hr.http_status = (unsigned int) response_code,
    .hr.reply = json
  };

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Received /pay response with status code %u\n",
              (unsigned int) response_code);

  json_dumpf (json,
              stderr,
              JSON_INDENT (2));

  oph->job = NULL;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "/pay completed with response code %u\n",
              (unsigned int) response_code);
  switch (response_code)
  {
  case 0:
    pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
    break;
  case MHD_HTTP_OK:
    if (oph->am_wallet)
    {
      const json_t *token_sigs = NULL;
      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_fixed_auto ("sig",
                                     &pr.details.ok.merchant_sig),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_string ("pos_confirmation",
                                   &pr.details.ok.pos_confirmation),
          NULL),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_array_const ("token_sigs",
                                        &token_sigs),
          NULL),
        GNUNET_JSON_spec_end ()
      };

      if (GNUNET_OK !=
          GNUNET_JSON_parse (json,
                             spec,
                             NULL, NULL))
      {
        GNUNET_break_op (0);
        pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
        pr.hr.http_status = 0;
        pr.hr.hint = "sig field missing in response";
        break;
      }

      if (GNUNET_OK !=
          parse_tokens (token_sigs,
                        &pr.details.ok.tokens,
                        &pr.details.ok.num_tokens))
      {
        GNUNET_break_op (0);
        pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
        pr.hr.http_status = 0;
        pr.hr.hint = "failed to parse token_sigs field in response";
        break;
      }

      if (GNUNET_OK !=
          TALER_merchant_pay_verify (&oph->h_contract_terms,
                                     &oph->merchant_pub,
                                     &pr.details.ok.merchant_sig))
      {
        GNUNET_break_op (0);
        pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
        pr.hr.http_status = 0;
        pr.hr.hint = "signature invalid";
      }
    }
    break;
  /* Tolerating Not Acceptable because sometimes
     * - especially in tests - we might want to POST
     * coins one at a time.  */
  case MHD_HTTP_NOT_ACCEPTABLE:
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_BAD_REQUEST:
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    /* This should never happen, either us
     * or the merchant is buggy (or API version conflict);
     * just pass JSON reply to the application */
    break;
  case MHD_HTTP_PAYMENT_REQUIRED:
    /* was originally paid, but then refunded */
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_FORBIDDEN:
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_NOT_FOUND:
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    /* Nothing really to verify, this should never
       happen, we should pass the JSON reply to the
       application */
    break;
  case MHD_HTTP_REQUEST_TIMEOUT:
    pr.hr.ec = TALER_JSON_get_error_code (json);
    pr.hr.hint = TALER_JSON_get_error_hint (json);
    /* The merchant couldn't generate a timely response, likely because
       it itself waited too long on the exchange.
       Pass on to application. */
    break;
  case MHD_HTTP_CONFLICT:
    TALER_MERCHANT_parse_error_details_ (json,
                                         MHD_HTTP_CONFLICT,
                                         &pr.hr);
    break;
  case MHD_HTTP_GONE:
    TALER_MERCHANT_parse_error_details_ (json,
                                         response_code,
                                         &pr.hr);
    /* The merchant says we are too late, the offer has expired or some
       denomination key of a coin involved has expired.
       Might be a disagreement in timestamps? Still, pass on to application. */
    break;
  case MHD_HTTP_PRECONDITION_FAILED:
    TALER_MERCHANT_parse_error_details_ (json,
                                         response_code,
                                         &pr.hr);
    /* Nothing really to verify, the merchant is blaming us for failing to
       satisfy some constraint (likely it does not like our exchange because
       of some disagreement on the PKI).  We should pass the JSON reply to the
       application */
    break;
  case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
    {
      json_t *ebus = json_object_get (json,
                                      "exchange_base_urls");
      if (NULL == ebus)
      {
        GNUNET_break_op (0);
        pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
        pr.hr.http_status = 0;
        pr.hr.hint = "failed to parse exchange_base_urls field in response";
        break;
      }
      {
        size_t alen = json_array_size (ebus);
        const char *ebua[GNUNET_NZL (alen)];
        size_t idx;
        json_t *jebu;
        bool ok = true;

        GNUNET_assert (alen <= UINT_MAX);
        json_array_foreach (ebus, idx, jebu)
        {
          ebua[idx] = json_string_value (jebu);
          if (NULL == ebua[idx])
          {
            GNUNET_break_op (0);
            pr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
            pr.hr.http_status = 0;
            pr.hr.hint = "non-string value in exchange_base_urls in response";
            ok = false;
            break;
          }
        }
        if (! ok)
          break;
        pr.details.unavailable_for_legal_reasons.num_exchanges
          = (unsigned int) alen;
        pr.details.unavailable_for_legal_reasons.exchanges
          = ebua;
        oph->cb (oph->cb_cls,
                 &pr);
        TALER_MERCHANT_order_pay_cancel1 (oph);
        return;
      }
    }
    break;
  case MHD_HTTP_INTERNAL_SERVER_ERROR:
    TALER_MERCHANT_parse_error_details_ (json,
                                         response_code,
                                         &pr.hr);
    /* Server had an internal issue; we should retry,
       but this API leaves this to the application */
    break;
  case MHD_HTTP_BAD_GATEWAY:
    /* Nothing really to verify, the merchant is blaming the exchange.
       We should pass the JSON reply to the application */
    TALER_MERCHANT_parse_error_details_ (json,
                                         response_code,
                                         &pr.hr);
    break;
  case MHD_HTTP_SERVICE_UNAVAILABLE:
    TALER_MERCHANT_parse_error_details_ (json,
                                         response_code,
                                         &pr.hr);
    /* Exchange couldn't respond properly; the retry is
       left to the application */
    break;
  case MHD_HTTP_GATEWAY_TIMEOUT:
    TALER_MERCHANT_parse_error_details_ (json,
                                         response_code,
                                         &pr.hr);
    /* Exchange couldn't respond in a timely fashion;
       the retry is left to the application */
    break;
  default:
    TALER_MERCHANT_parse_error_details_ (json,
                                         response_code,
                                         &pr.hr);
    /* unexpected response code */
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Unexpected response code %u/%d\n",
                (unsigned int) response_code,
                (int) pr.hr.ec);
    GNUNET_break_op (0);
    break;
  }
  oph->cb (oph->cb_cls,
           &pr);

  if (pr.details.ok.tokens)
  {
    GNUNET_free (pr.details.ok.tokens);
    pr.details.ok.tokens = NULL;
    pr.details.ok.num_tokens = 0;
  }

  TALER_MERCHANT_order_pay_cancel1 (oph);
}


/**
 * @brief Create and initialize a new payment handle
 *
 * Allocates a TALER_MERCHANT_OrderPayHandle, sets up its context and
 * prepares an empty JSON body for the /orders/$ID/pay request.
 */
struct TALER_MERCHANT_OrderPayHandle *
TALER_MERCHANT_order_pay_create (struct GNUNET_CURL_Context *ctx,
                                 TALER_MERCHANT_OrderPayCallback cb,
                                 TALER_MERCHANT_ORDER_PAY_CALLBACK_CLOSURE_TYPE
                                 *cb_cls)
{
  struct TALER_MERCHANT_OrderPayHandle *ph =
    GNUNET_new (struct TALER_MERCHANT_OrderPayHandle);
  ph->ctx             = ctx;
  ph->cb              = cb;
  ph->cb_cls          = cb_cls;
  ph->body            = json_object ();
  GNUNET_assert (ph->body);
  return ph;
}


/**
 * @brief Cancel and free a payment handle
 *
 * Aborts any in-flight CURL job, releases all JSON objects and internal
 * buffers, and frees the handle structure itself.
 */
void
TALER_MERCHANT_order_pay_cancel1 (struct TALER_MERCHANT_OrderPayHandle *ph)
{
  if (ph->job)
    GNUNET_CURL_job_cancel (ph->job);
  ph->job = NULL;

  TALER_curl_easy_post_finished (&ph->post_ctx);

  if (ph->body)
    json_decref (ph->body);
  ph->body = NULL;

  if (ph->wallet_data)
    json_decref (ph->wallet_data);
  ph->wallet_data = NULL;

  if (ph->tokens_evs)
    json_decref (ph->tokens_evs);
  ph->tokens_evs = NULL;

  if (ph->donau_data)
    json_decref (ph->donau_data);

  GNUNET_free (ph->url);
  GNUNET_free (ph->merchant_url);
  GNUNET_free (ph->session_id);
  GNUNET_free (ph->order_id);
  GNUNET_free (ph);
}


/**
 * @brief Store a JSON snippet under a payment option key
 *
 * Ensures that an option of type @a ot has not already been set,
 * then merges @a snippet into the handle's JSON @c body. Marks the
 * field_seen flag and frees @a snippet.
 *
 * @param ph   payment handle receiving the snippet
 * @param ot   option type under which to store @a snippet
 * @param snippet JSON object representing the option payload
 * @return #TALER_MERCHANT_OPOEC_OK if stored; appropriate error code otherwise
 */
static enum TALER_MERCHANT_OrderPayErrorCode
store_json_option (struct TALER_MERCHANT_OrderPayHandle *ph,
                   enum TALER_MERCHANT_OrderPayOptionType ot,
                   json_t *snippet)
{
  if (ph->field_seen[ot])
  {
    json_decref (snippet);
    return TALER_MERCHANT_OPOEC_DUPLICATE_OPTION;
  }
  ph->field_seen[ot] = true;
  GNUNET_assert (0 == json_object_update (ph->body,
                                          snippet));
  json_decref (snippet);
  return TALER_MERCHANT_OPOEC_OK;
}


/**
 * @brief Apply user-supplied options to a payment handle
 *
 * Iterates through a NULL-terminated array of #TALER_MERCHANT_OrderPayOption
 * entries, validates and stores each into @a ph. Handles JSON packing and
 * internal state updates for coins, tokens, deadlines, Donau data, etc.
 */
enum TALER_MERCHANT_OrderPayErrorCode
TALER_MERCHANT_order_pay_set_options (
  struct TALER_MERCHANT_OrderPayHandle *ph,
  const struct TALER_MERCHANT_OrderPayOption options[],
  size_t max_options)
{
  for (size_t i = 0; i < max_options
       && options[i].ot != TALER_MERCHANT_OrderPayOptionType_END; i++)
  {
    const struct TALER_MERCHANT_OrderPayOption *o = &options[i];

    switch (o->ot)
    {
    case TALER_MERCHANT_OrderPayOptionType_MERCHANT_URL:
      ph->merchant_url = GNUNET_strdup (o->details.merchant_url);
      break;

    case TALER_MERCHANT_OrderPayOptionType_SESSION_ID:
      ph->session_id = GNUNET_strdup (o->details.session_id);
      /* add straight into JSON body */
      {
        json_t *js = GNUNET_JSON_PACK (GNUNET_JSON_pack_string ("session_id",
                                                                o->details.
                                                                session_id));
        enum TALER_MERCHANT_OrderPayErrorCode ec =
          store_json_option (ph,
                             o->ot,
                             js);
        if (TALER_MERCHANT_OPOEC_OK != ec)
          return ec;
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_ORDER_ID:
      {
        ph->order_id = GNUNET_strdup (o->details.order_id);
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_H_CONTRACT:
      {
        ph->h_contract_terms = *o->details.h_contract;
        ph->has_h_contract = true;

        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_CHOICE_INDEX:
      ph->choice_index = o->details.choice_index;
      ph->has_choice_index = true;
      break;

    case TALER_MERCHANT_OrderPayOptionType_AMOUNT:
      {
        ph->amount = &o->details.amount;
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_MAX_FEE:
      {
        ph->max_fee = &o->details.max_fee;
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_MERCHANT_PUB:
      {
        ph->merchant_pub = o->details.merchant_pub;
        ph->has_merchant_pub = true;

        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_TIMESTAMP:
      {
        ph->timestamp = o->details.timestamp;
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_REFUND_DEADLINE:
      {
        ph->refund_deadline = o->details.refund_deadline;
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_PAY_DEADLINE:
      {
        /* FIXME: This one comes from the merchant_api_post_order_pay
         no idea do we still need it or not? */
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_H_WIRE:
      {
        ph->h_wire = o->details.h_wire;
        ph->has_h_wire = true;
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_COINS:
      /* stash for later signing */
      ph->coins.num_coins = o->details.coins.num_coins;
      ph->coins.coins = o->details.coins.coins;
      break;

    case TALER_MERCHANT_OrderPayOptionType_INPUT_TOKENS:
      /* stash for later signing */
      ph->input_tokens.num_tokens = o->details.input_tokens.num_tokens;
      ph->input_tokens.tokens = o->details.input_tokens.tokens;
      break;

    case TALER_MERCHANT_OrderPayOptionType_OUTPUT_TOKENS:
      /* store JSON array directly, *and* stash for hash */
      ph->output_tokens.num_output_tokens =
        o->details.output_tokens.num_output_tokens;
      ph->output_tokens.output_tokens = o->details.output_tokens.output_tokens;
      {
        /* build and store tokens_evs */
        json_t *arr = json_array ();

        GNUNET_assert (NULL != arr);
        for (unsigned j = 0; j < ph->output_tokens.num_output_tokens; j++)
        {
          const struct TALER_MERCHANT_OutputToken *otk =
            &ph->output_tokens.output_tokens[j];
          json_t *je = GNUNET_JSON_PACK (TALER_JSON_pack_token_envelope (NULL,
                                                                         &otk->
                                                                         envelope));
          GNUNET_assert (0 ==
                         json_array_append_new (arr,
                                                je));
        }

        ph->tokens_evs = arr;

        ph->field_seen[o->ot] = true;
      }
      break;

    case TALER_MERCHANT_OrderPayOptionType_DONAU_URL:
      if (NULL == ph->donau_data)
        ph->donau_data = json_object ();
      GNUNET_assert (0 ==
                     json_object_set_new (
                       ph->donau_data,
                       "url",
                       json_string (o->details.donau_url)));
      break;

    case TALER_MERCHANT_OrderPayOptionType_DONAU_YEAR:
      {
        if (ph->donau_data == NULL)
          ph->donau_data = json_object ();
        GNUNET_assert (0 == json_object_set_new (
                         ph->donau_data,
                         "year",
                         json_integer ((json_int_t) o->details.donau_year)));
        break;
      }

    case TALER_MERCHANT_OrderPayOptionType_DONAU_BUDIS:
      {
        if (ph->donau_data == NULL)
          ph->donau_data = json_object ();
        GNUNET_assert (0 == json_object_set_new (
                         ph->donau_data,
                         "budikeypairs",
                         json_incref (o->details.donau_budis_json)));
        break;
      }

    default:
      return TALER_MERCHANT_OPOEC_UNKNOWN_OPTION;
    }
  }
  return TALER_MERCHANT_OPOEC_OK;
}


/**
 * @brief Dispatch the /orders/$ID/pay request
 *
 * Validates that all mandatory parameters (merchant_url, order_id, coins)
 * have been set, builds the final JSON payload, constructs the URL,
 * and issues an asynchronous HTTP POST. The payment handle's callback
 * will receive completion notifications.
 */
enum TALER_MERCHANT_OrderPayErrorCode
TALER_MERCHANT_order_pay_start (struct TALER_MERCHANT_OrderPayHandle *ph)
{
  /* all the old mandatory checks */
  if ( (! ph->merchant_url) ||
       (! ph->order_id) )
  {
    GNUNET_break (0);
    return TALER_MERCHANT_OPOEC_MISSING_MANDATORY;
  }
  if (GNUNET_YES !=
      TALER_amount_cmp_currency (ph->amount,
                                 ph->max_fee))
  {
    GNUNET_break (0);
    return TALER_MERCHANT_OPOEC_INVALID_VALUE;
  }

  /* build wallet_data hash for signing coins & tokens */
  if (ph->has_choice_index)
  {
    /* base fields */
    json_t *wd = GNUNET_JSON_PACK (
      GNUNET_JSON_pack_int64 ("choice_index",
                              ph->choice_index),
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_array_incref ("tokens_evs",
                                       ph->tokens_evs))
      );

    /* Putting prepared donau_data into the wallet_data */
    if (ph->donau_data)
      GNUNET_assert (0 == json_object_set_new (
                       wd,
                       "donau",
                       json_incref (ph->donau_data)));

    ph->wallet_data = wd;

    TALER_json_hash (ph->wallet_data,
                     &ph->wallet_data_hash);

    store_json_option (ph,
                       TALER_MERCHANT_OrderPayOptionType_WALLET_DATA,
                       GNUNET_JSON_PACK (
                         GNUNET_JSON_pack_object_incref ("wallet_data",
                                                         ph->wallet_data)));
  }

  /* sign coins AND build the “coins” JSON in one pass */
  {
    struct TALER_Amount total_fee;
    struct TALER_Amount total_amount;
    json_t *arr = json_array ();

    GNUNET_assert (NULL != arr);
    for (unsigned i = 0; i < ph->coins.num_coins; i++)
    {
      const struct TALER_MERCHANT_PayCoin *c = &ph->coins.coins[i];
      struct TALER_MERCHANT_PaidCoin pc;
      json_t *je;

      /* sign  */
      struct TALER_Amount fee;
      struct TALER_DenominationHashP h_denom_pub;

      TALER_denom_pub_hash (&c->denom_pub,
                            &h_denom_pub);
      if (0 > TALER_amount_subtract (&fee,
                                     &c->amount_with_fee,
                                     &c->amount_without_fee))
        return TALER_MERCHANT_OPOEC_INVALID_VALUE;


      TALER_wallet_deposit_sign (&c->amount_with_fee,
                                 &fee,
                                 &ph->h_wire,
                                 &ph->h_contract_terms,
                                 (NULL != ph->wallet_data)
                                ? &ph->wallet_data_hash
                                : NULL,
                                 c->h_age_commitment,
                                 NULL,
                                 &h_denom_pub,
                                 ph->timestamp,
                                 &ph->merchant_pub,
                                 ph->refund_deadline,
                                 &c->coin_priv,
                                 &pc.coin_sig);

      pc.denom_pub = c->denom_pub;
      pc.denom_sig = c->denom_sig;
      pc.denom_value = c->denom_value;
      pc.amount_with_fee = c->amount_with_fee;
      pc.amount_without_fee = c->amount_without_fee;
      pc.exchange_url = c->exchange_url;
      GNUNET_CRYPTO_eddsa_key_get_public (&c->coin_priv.eddsa_priv,
                                          &pc.coin_pub.eddsa_pub);

      /* JSON  */
      je = GNUNET_JSON_PACK (TALER_JSON_pack_amount ("contribution",
                                                     &pc.amount_with_fee),
                             GNUNET_JSON_pack_data_auto ("coin_pub",
                                                         &pc.coin_pub),
                             GNUNET_JSON_pack_string ("exchange_url",
                                                      pc.exchange_url),
                             GNUNET_JSON_pack_data_auto ("h_denom",
                                                         &h_denom_pub),
                             TALER_JSON_pack_denom_sig ("ub_sig",
                                                        &pc.denom_sig),
                             GNUNET_JSON_pack_data_auto ("coin_sig",
                                                         &pc.coin_sig));
      GNUNET_assert (0 ==
                     json_array_append_new (arr,
                                            je));

      /* optional totals if you need them later
       (kept here because they existed in the legacy code) */
      if (0 == i)
      {
        total_fee = fee;
        total_amount = pc.amount_with_fee;
      }
      else
      {
        if ( (0 >
              TALER_amount_add (&total_fee,
                                &total_fee,
                                &fee)) ||
             (0 >
              TALER_amount_add (&total_amount,
                                &total_amount,
                                &pc.amount_with_fee)) )
        {
          return TALER_MERCHANT_OPOEC_INVALID_VALUE;
        }
      }
    }

    /* Putting coins to the body*/
    {
      enum TALER_MERCHANT_OrderPayErrorCode ec =
        store_json_option (ph,
                           TALER_MERCHANT_OrderPayOptionType_COINS,
                           GNUNET_JSON_PACK (
                             GNUNET_JSON_pack_array_steal ("coins",
                                                           arr)
                             ));
      if (TALER_MERCHANT_OPOEC_OK != ec)
      {
        return ec;
      }
    }
  }

  /* sign & pack input_tokens into used_tokens array in body */
  if (ph->input_tokens.num_tokens > 0)
  {
    struct TALER_MERCHANT_UsedToken ut[ph->input_tokens.num_tokens];
    json_t *arr = json_array ();

    GNUNET_assert (NULL != arr);
    for (unsigned i = 0; i < ph->input_tokens.num_tokens; i++)
    {
      json_t *je;
      const struct TALER_MERCHANT_UseToken *in = &ph->input_tokens.tokens[i];
      struct TALER_MERCHANT_UsedToken *t = &ut[i];

      TALER_wallet_token_use_sign (&ph->h_contract_terms,
                                   &ph->wallet_data_hash,
                                   &in->token_priv,
                                   &t->token_sig);

      t->ub_sig = in->ub_sig;
      t->issue_pub = in->issue_pub;

      GNUNET_CRYPTO_eddsa_key_get_public (&in->token_priv.private_key,
                                          &t->token_pub.public_key);

      je = GNUNET_JSON_PACK (
        GNUNET_JSON_pack_data_auto ("token_sig",
                                    &t->token_sig),
        TALER_JSON_pack_token_issue_sig ("ub_sig",
                                         &t->ub_sig),
        GNUNET_JSON_pack_data_auto ("h_issue",
                                    &t->issue_pub.public_key->pub_key_hash),
        GNUNET_JSON_pack_data_auto ("token_pub",
                                    &t->token_pub)
        );
      GNUNET_assert (0 ==
                     json_array_append_new (arr,
                                            je));
    }

    store_json_option (ph,
                       TALER_MERCHANT_OrderPayOptionType_INPUT_TOKENS,
                       GNUNET_JSON_PACK (
                         GNUNET_JSON_pack_array_steal ("tokens",
                                                       arr)
                         )
                       );
  }


  /* post the request */
  {
    char *path;
    CURL *eh;
    GNUNET_asprintf (&path,
                     "orders/%s/pay",
                     ph->order_id);
    ph->url = TALER_url_join (ph->merchant_url,
                              path,
                              NULL);
    GNUNET_free (path);

    if (NULL == ph->url)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Could not construct request URL.\n");
      json_decref (ph->body);
      GNUNET_free (ph);
      return TALER_MERCHANT_OPOEC_URL_FAILURE;
    }

    eh = TALER_MERCHANT_curl_easy_get_ (ph->url);
    if (GNUNET_OK !=
        TALER_curl_easy_post (&ph->post_ctx,
                              eh,
                              ph->body))
    {
      GNUNET_break (0);
      curl_easy_cleanup (eh);
      GNUNET_free (ph->url);
      GNUNET_free (ph);
      return TALER_MERCHANT_OPOEC_CURL_FAILURE;
    }

    ph->job = GNUNET_CURL_job_add2 (ph->ctx,
                                    eh,
                                    ph->post_ctx.headers,
                                    &handle_finished,
                                    ph);

    ph->am_wallet = true;
    return TALER_MERCHANT_OPOEC_OK;
  }
}
