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

  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU Lesser General Public License as published by the Free Software
  Foundation; either version 2.1, 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.LGPL.  If not, see
  <http://www.gnu.org/licenses/>
*/
/**
 * @file src/lib/merchant_api_post-private-orders.c
 * @brief Implementation of the POST /private/orders request
 * @author Christian Grothoff
 */
#include "platform.h"
#include <curl/curl.h>
#include <jansson.h>
#include <microhttpd.h> /* just for HTTP status codes */
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_curl_lib.h>
#include <taler/merchant/post-private-orders.h>
#include "merchant_api_curl_defaults.h"
#include "merchant_api_common.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_curl_lib.h>


/**
 * Maximum number of exchange rejections we allow in a response before
 * we consider the (untrusted) reply malformed.  Bounds the stack VLA
 * used to parse the array.
 */
#define MAX_EXCHANGE_REJECTIONS 1024


/**
 * Handle for a POST /private/orders operation.
 */
struct TALER_MERCHANT_PostPrivateOrdersHandle
{
  /**
   * Base URL of the merchant backend.
   */
  char *base_url;

  /**
   * The full URL for this request.
   */
  char *url;

  /**
   * Handle for the request.
   */
  struct GNUNET_CURL_Job *job;

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

  /**
   * Closure for @a cb.
   */
  TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE *cb_cls;

  /**
   * Reference to the execution context.
   */
  struct GNUNET_CURL_Context *ctx;

  /**
   * Minor context that holds body and headers.
   */
  struct TALER_CURL_PostContext post_ctx;

  /**
   * Order contract terms (JSON).
   */
  json_t *order;

  /**
   * Optional refund delay.
   */
  struct GNUNET_TIME_Relative refund_delay;

  /**
   * Whether refund_delay was set.
   */
  bool refund_delay_set;

  /**
   * Optional payment target.
   */
  const char *payment_target;

  /**
   * Optional session ID.
   */
  const char *session_id;

  /**
   * Whether to create a claim token (default: true).
   */
  bool create_token;

  /**
   * Optional OTP device ID.
   */
  const char *otp_id;

  /**
   * Optional inventory products.
   */
  const struct TALER_MERCHANT_PostPrivateOrdersInventoryProduct *
    inventory_products;

  /**
   * Number of inventory products.
   */
  unsigned int num_inventory_products;

  /**
   * Optional lock UUIDs.
   */
  const char **lock_uuids;

  /**
   * Number of lock UUIDs.
   */
  unsigned int num_lock_uuids;
};


/**
 * Function called when we're done processing the
 * HTTP POST /private/orders request.
 *
 * @param cls the `struct TALER_MERCHANT_PostPrivateOrdersHandle`
 * @param response_code HTTP response code, 0 on error
 * @param response response body, NULL if not in JSON
 */
static void
handle_post_orders_finished (void *cls,
                             long response_code,
                             const void *response)
{
  struct TALER_MERCHANT_PostPrivateOrdersHandle *ppoh = cls;
  const json_t *json = response;
  struct TALER_MERCHANT_PostPrivateOrdersResponse por = {
    .hr.http_status = (unsigned int) response_code,
    .hr.reply = json
  };
  struct TALER_ClaimTokenP token;

  ppoh->job = NULL;
  switch (response_code)
  {
  case 0:
    por.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
    break;
  case MHD_HTTP_OK:
    {
      bool no_token;
      bool no_pay_deadline;
      struct GNUNET_JSON_Specification spec[] = {
        TALER_JSON_spec_slug ("order_id",
                              &por.details.ok.order_id),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_fixed_auto ("token",
                                       &token),
          &no_token),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_timestamp ("pay_deadline",
                                      &por.details.ok.pay_deadline),
          &no_pay_deadline),
        GNUNET_JSON_spec_end ()
      };

      if (GNUNET_OK !=
          GNUNET_JSON_parse (json,
                             spec,
                             NULL, NULL))
      {
        GNUNET_break_op (0);
        por.hr.http_status = 0;
        por.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
        break;
      }
      if (! no_token)
        por.details.ok.token = &token;
      if (no_pay_deadline)
        por.details.ok.pay_deadline = GNUNET_TIME_UNIT_ZERO_TS;
      break;
    }
  case MHD_HTTP_BAD_REQUEST:
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_UNAUTHORIZED:
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_FORBIDDEN:
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_NOT_FOUND:
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_CONFLICT:
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_GONE:
    {
      bool rq_frac_missing;
      bool aq_frac_missing;
      struct GNUNET_JSON_Specification spec[] = {
        TALER_JSON_spec_slug (
          "product_id",
          &por.details.gone.product_id),
        GNUNET_JSON_spec_uint64 (
          "requested_quantity",
          &por.details.gone.requested_quantity),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_uint32 (
            "requested_quantity_frac",
            &por.details.gone.requested_quantity_frac),
          &rq_frac_missing),
        GNUNET_JSON_spec_uint64 (
          "available_quantity",
          &por.details.gone.available_quantity),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_uint32 (
            "available_quantity_frac",
            &por.details.gone.available_quantity_frac),
          &aq_frac_missing),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_string (
            "unit_requested_quantity",
            &por.details.gone.unit_requested_quantity),
          NULL),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_string (
            "unit_available_quantity",
            &por.details.gone.unit_available_quantity),
          NULL),
        GNUNET_JSON_spec_mark_optional (
          GNUNET_JSON_spec_timestamp (
            "restock_expected",
            &por.details.gone.restock_expected),
          NULL),
        GNUNET_JSON_spec_end ()
      };

      if (GNUNET_OK !=
          GNUNET_JSON_parse (json,
                             spec,
                             NULL, NULL))
      {
        GNUNET_break_op (0);
        por.hr.http_status = 0;
        por.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
      }
      else
      {
        if (rq_frac_missing)
          por.details.gone.requested_quantity_frac = 0;
        if (aq_frac_missing)
          por.details.gone.available_quantity_frac = 0;
      }
      break;
    }
  case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
    {
      const json_t *jer = NULL;

      por.hr.ec = TALER_JSON_get_error_code (json);
      por.hr.hint = TALER_JSON_get_error_hint (json);
      jer = json_object_get (json,
                             "exchange_rejections");
      if ( (NULL != jer) &&
           json_is_array (jer) )
      {
        unsigned int rej_len = (unsigned int) json_array_size (jer);

        if ( (json_array_size (jer) == (size_t) rej_len) &&
             (rej_len <= MAX_EXCHANGE_REJECTIONS) )
        {
          struct TALER_MERCHANT_ExchangeRejectionDetail rejs[
            GNUNET_NZL (rej_len)];
          bool ok = true;

          memset (rejs, 0, sizeof (rejs));
          for (unsigned int i = 0; i < rej_len; i++)
          {
            struct GNUNET_JSON_Specification rspec[] = {
              TALER_JSON_spec_web_url (
                "exchange_url",
                &rejs[i].exchange_url),
              TALER_JSON_spec_ec (
                "code",
                &rejs[i].code),
              GNUNET_JSON_spec_mark_optional (
                GNUNET_JSON_spec_string (
                  "hint",
                  &rejs[i].hint),
                NULL),
              GNUNET_JSON_spec_end ()
            };

            if (GNUNET_OK !=
                GNUNET_JSON_parse (json_array_get (jer, i),
                                   rspec,
                                   NULL, NULL))
            {
              GNUNET_break_op (0);
              ok = false;
              break;
            }
          }
          if (ok)
          {
            por.details.unavailable_for_legal_reasons
            .num_exchange_rejections = rej_len;
            por.details.unavailable_for_legal_reasons
            .exchange_rejections = rejs;
            ppoh->cb (ppoh->cb_cls,
                      &por);
            TALER_MERCHANT_post_private_orders_cancel (ppoh);
            return;
          }
        }
      }
      break;
    }
  case MHD_HTTP_INTERNAL_SERVER_ERROR:
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  default:
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Unexpected response code %u/%d\n",
                (unsigned int) response_code,
                (int) por.hr.ec);
    GNUNET_break_op (0);
    break;
  }
  ppoh->cb (ppoh->cb_cls,
            &por);
  TALER_MERCHANT_post_private_orders_cancel (ppoh);
}


struct TALER_MERCHANT_PostPrivateOrdersHandle *
TALER_MERCHANT_post_private_orders_create (
  struct GNUNET_CURL_Context *ctx,
  const char *url,
  const json_t *order)
{
  struct TALER_MERCHANT_PostPrivateOrdersHandle *ppoh;

  ppoh = GNUNET_new (struct TALER_MERCHANT_PostPrivateOrdersHandle);
  ppoh->ctx = ctx;
  ppoh->base_url = GNUNET_strdup (url);
  ppoh->order = json_incref ((json_t *) order);
  ppoh->create_token = true;
  return ppoh;
}


enum GNUNET_GenericReturnValue
TALER_MERCHANT_post_private_orders_set_options_ (
  struct TALER_MERCHANT_PostPrivateOrdersHandle *ppoh,
  unsigned int num_options,
  const struct TALER_MERCHANT_PostPrivateOrdersOptionValue *options)
{
  for (unsigned int i = 0; i < num_options; i++)
  {
    switch (options[i].option)
    {
    case TALER_MERCHANT_POST_PRIVATE_ORDERS_OPTION_END:
      return GNUNET_OK;
    case TALER_MERCHANT_POST_PRIVATE_ORDERS_OPTION_REFUND_DELAY:
      ppoh->refund_delay = options[i].details.refund_delay;
      ppoh->refund_delay_set = true;
      break;
    case TALER_MERCHANT_POST_PRIVATE_ORDERS_OPTION_PAYMENT_TARGET:
      ppoh->payment_target = options[i].details.payment_target;
      break;
    case TALER_MERCHANT_POST_PRIVATE_ORDERS_OPTION_SESSION_ID:
      ppoh->session_id = options[i].details.session_id;
      break;
    case TALER_MERCHANT_POST_PRIVATE_ORDERS_OPTION_CREATE_TOKEN:
      ppoh->create_token = options[i].details.create_token;
      break;
    case TALER_MERCHANT_POST_PRIVATE_ORDERS_OPTION_OTP_ID:
      ppoh->otp_id = options[i].details.otp_id;
      break;
    case TALER_MERCHANT_POST_PRIVATE_ORDERS_OPTION_INVENTORY_PRODUCTS:
      ppoh->num_inventory_products
        = options[i].details.inventory_products.num;
      ppoh->inventory_products
        = options[i].details.inventory_products.products;
      break;
    case TALER_MERCHANT_POST_PRIVATE_ORDERS_OPTION_LOCK_UUIDS:
      ppoh->num_lock_uuids = options[i].details.lock_uuids.num;
      ppoh->lock_uuids = options[i].details.lock_uuids.uuids;
      break;
    default:
      GNUNET_break (0);
      return GNUNET_SYSERR;
    }
  }
  return GNUNET_OK;
}


enum TALER_ErrorCode
TALER_MERCHANT_post_private_orders_start (
  struct TALER_MERCHANT_PostPrivateOrdersHandle *ppoh,
  TALER_MERCHANT_PostPrivateOrdersCallback cb,
  TALER_MERCHANT_POST_PRIVATE_ORDERS_RESULT_CLOSURE *cb_cls)
{
  json_t *req;
  CURL *eh;

  ppoh->cb = cb;
  ppoh->cb_cls = cb_cls;
  ppoh->url = TALER_url_join (ppoh->base_url,
                              "private/orders",
                              NULL);
  if (NULL == ppoh->url)
    return TALER_EC_GENERIC_CONFIGURATION_INVALID;
  req = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_object_incref ("order",
                                    ppoh->order),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("session_id",
                               ppoh->session_id)),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("payment_target",
                               ppoh->payment_target)),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("otp_id",
                               ppoh->otp_id)));
  if (ppoh->refund_delay_set &&
      (0 != ppoh->refund_delay.rel_value_us))
  {
    GNUNET_assert (0 ==
                   json_object_set_new (req,
                                        "refund_delay",
                                        GNUNET_JSON_from_time_rel (
                                          ppoh->refund_delay)));
  }
  if (0 != ppoh->num_inventory_products)
  {
    json_t *ipa = json_array ();

    GNUNET_assert (NULL != ipa);
    for (unsigned int i = 0; i < ppoh->num_inventory_products; i++)
    {
      json_t *ip;

      {
        char unit_quantity_buf[64];

        TALER_MERCHANT_format_quantity_string (
          ppoh->inventory_products[i].quantity,
          ppoh->inventory_products[i].use_fractional_quantity
          ? ppoh->inventory_products[i].quantity_frac
          : 0,
          unit_quantity_buf,
          sizeof (unit_quantity_buf));
        ip = GNUNET_JSON_PACK (
          GNUNET_JSON_pack_string ("product_id",
                                   ppoh->inventory_products[i].product_id),
          GNUNET_JSON_pack_string ("unit_quantity",
                                   unit_quantity_buf));
      }
      if (ppoh->inventory_products[i].product_money_pot > 0)
      {
        GNUNET_assert (
          0 ==
          json_object_set_new (
            ip,
            "product_money_pot",
            json_integer (
              ppoh->inventory_products[i].product_money_pot)));
      }
      GNUNET_assert (0 ==
                     json_array_append_new (ipa,
                                            ip));
    }
    GNUNET_assert (0 ==
                   json_object_set_new (req,
                                        "inventory_products",
                                        ipa));
  }
  if (0 != ppoh->num_lock_uuids)
  {
    json_t *ua = json_array ();

    GNUNET_assert (NULL != ua);
    for (unsigned int i = 0; i < ppoh->num_lock_uuids; i++)
    {
      GNUNET_assert (0 ==
                     json_array_append_new (ua,
                                            json_string (
                                              ppoh->lock_uuids[i])));
    }
    GNUNET_assert (0 ==
                   json_object_set_new (req,
                                        "lock_uuids",
                                        ua));
  }
  if (! ppoh->create_token)
  {
    GNUNET_assert (0 ==
                   json_object_set_new (req,
                                        "create_token",
                                        json_boolean (ppoh->create_token)));
  }
  eh = TALER_MERCHANT_curl_easy_get_ (ppoh->url);
  if ( (NULL == eh) ||
       (GNUNET_OK !=
        TALER_curl_easy_post (&ppoh->post_ctx,
                              eh,
                              req)) )
  {
    GNUNET_break (0);
    json_decref (req);
    if (NULL != eh)
      curl_easy_cleanup (eh);
    return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
  }
  json_decref (req);
  ppoh->job = GNUNET_CURL_job_add2 (ppoh->ctx,
                                    eh,
                                    ppoh->post_ctx.headers,
                                    &handle_post_orders_finished,
                                    ppoh);
  if (NULL == ppoh->job)
    return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
  return TALER_EC_NONE;
}


void
TALER_MERCHANT_post_private_orders_cancel (
  struct TALER_MERCHANT_PostPrivateOrdersHandle *ppoh)
{
  if (NULL != ppoh->job)
  {
    GNUNET_CURL_job_cancel (ppoh->job);
    ppoh->job = NULL;
  }
  TALER_curl_easy_post_finished (&ppoh->post_ctx);
  json_decref (ppoh->order);
  GNUNET_free (ppoh->url);
  GNUNET_free (ppoh->base_url);
  GNUNET_free (ppoh);
}


/* end of merchant_api_post-private-orders-new.c */
