/*
  This file is part of TALER
  Copyright (C) 2014-2023 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 merchant_api_get_orders.c
 * @brief Implementation of the GET /private/orders request of the merchant's HTTP API
 * @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_service.h"
#include "merchant_api_curl_defaults.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_signatures.h>

/**
 * Maximum number of orders we return.
 */
#define MAX_ORDERS 1024

/**
 * Handle for a GET /orders operation.
 */
struct TALER_MERCHANT_OrdersGetHandle
{
  /**
   * The url for this request.
   */
  char *url;

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

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

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

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

};


/**
 * Parse order information from @a ia.
 *
 * @param ia JSON array (or NULL!) with order data
 * @param[in] ogr response to fill
 * @param ogh operation handle
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
parse_orders (const json_t *ia,
              struct TALER_MERCHANT_OrdersGetResponse *ogr,
              struct TALER_MERCHANT_OrdersGetHandle *ogh)
{
  unsigned int oes_len = (unsigned int) json_array_size (ia);

  if ( (json_array_size (ia) != (size_t)  oes_len) ||
       (oes_len > MAX_ORDERS) )
  {
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  {
    struct TALER_MERCHANT_OrderEntry oes[GNUNET_NZL (oes_len)];
    size_t index;
    json_t *value;

    memset (oes,
            0,
            sizeof (oes));
    json_array_foreach (ia, index, value) {
      struct TALER_MERCHANT_OrderEntry *ie = &oes[index];
      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_string ("order_id",
                                 &ie->order_id),
        GNUNET_JSON_spec_timestamp ("timestamp",
                                    &ie->timestamp),
        GNUNET_JSON_spec_uint64 ("row_id",
                                 &ie->order_serial),
        TALER_JSON_spec_amount_any ("amount",
                                    &ie->amount),
        GNUNET_JSON_spec_mark_optional (
          TALER_JSON_spec_amount_any ("refund_amount",
                                      &ie->refund_amount),
          NULL),
        GNUNET_JSON_spec_mark_optional (
          TALER_JSON_spec_amount_any ("pending_refund_amount",
                                      &ie->pending_refund_amount),
          NULL),
        GNUNET_JSON_spec_string ("summary",
                                 &ie->summary),
        GNUNET_JSON_spec_bool ("refundable",
                               &ie->refundable),
        GNUNET_JSON_spec_bool ("paid",
                               &ie->paid),
        GNUNET_JSON_spec_end ()
      };

      if (GNUNET_OK !=
          GNUNET_JSON_parse (value,
                             spec,
                             NULL, NULL))
      {
        GNUNET_break_op (0);
        return GNUNET_SYSERR;
      }
    }
    ogr->details.ok.orders_length = oes_len;
    ogr->details.ok.orders = oes;
    ogh->cb (ogh->cb_cls,
             ogr);
    ogh->cb = NULL; /* just to be sure */
  }
  return GNUNET_OK;
}


/**
 * Function called when we're done processing the
 * HTTP /orders request.
 *
 * @param cls the `struct TALER_MERCHANT_OrdersGetHandle`
 * @param response_code HTTP response code, 0 on error
 * @param response response body, NULL if not in JSON
 */
static void
handle_get_orders_finished (void *cls,
                            long response_code,
                            const void *response)
{
  struct TALER_MERCHANT_OrdersGetHandle *ogh = cls;
  const json_t *json = response;
  struct TALER_MERCHANT_OrdersGetResponse ogr = {
    .hr.http_status = (unsigned int) response_code,
    .hr.reply = json
  };

  ogh->job = NULL;
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Got /orders response with status code %u\n",
              (unsigned int) response_code);
  switch (response_code)
  {
  case MHD_HTTP_OK:
    {
      const json_t *orders;
      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_array_const ("orders",
                                      &orders),
        GNUNET_JSON_spec_end ()
      };

      if (GNUNET_OK !=
          GNUNET_JSON_parse (json,
                             spec,
                             NULL, NULL))
      {
        ogr.hr.http_status = 0;
        ogr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
        break;
      }
      if (GNUNET_OK ==
          parse_orders (orders,
                        &ogr,
                        ogh))
      {
        TALER_MERCHANT_orders_get_cancel (ogh);
        return;
      }
      ogr.hr.http_status = 0;
      ogr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
      break;
    }
  case MHD_HTTP_UNAUTHORIZED:
    ogr.hr.ec = TALER_JSON_get_error_code (json);
    ogr.hr.hint = TALER_JSON_get_error_hint (json);
    /* Nothing really to verify, merchant says we need to authenticate. */
    break;
  case MHD_HTTP_NOT_FOUND:
    ogr.hr.ec = TALER_JSON_get_error_code (json);
    ogr.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  default:
    /* unexpected response code */
    ogr.hr.ec = TALER_JSON_get_error_code (json);
    ogr.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) ogr.hr.ec);
    break;
  }
  ogh->cb (ogh->cb_cls,
           &ogr);
  TALER_MERCHANT_orders_get_cancel (ogh);
}


struct TALER_MERCHANT_OrdersGetHandle *
TALER_MERCHANT_orders_get (
  struct GNUNET_CURL_Context *ctx,
  const char *backend_url,
  TALER_MERCHANT_OrdersGetCallback cb,
  void *cb_cls)
{
  return TALER_MERCHANT_orders_get2 (ctx,
                                     backend_url,
                                     TALER_EXCHANGE_YNA_ALL,
                                     TALER_EXCHANGE_YNA_ALL,
                                     TALER_EXCHANGE_YNA_ALL,
                                     GNUNET_TIME_UNIT_FOREVER_TS,
                                     UINT64_MAX,
                                     -20, /* default is most recent 20 entries */
                                     GNUNET_TIME_UNIT_ZERO,
                                     cb,
                                     cb_cls);
}


struct TALER_MERCHANT_OrdersGetHandle *
TALER_MERCHANT_orders_get2 (
  struct GNUNET_CURL_Context *ctx,
  const char *backend_url,
  enum TALER_EXCHANGE_YesNoAll paid,
  enum TALER_EXCHANGE_YesNoAll refunded,
  enum TALER_EXCHANGE_YesNoAll wired,
  struct GNUNET_TIME_Timestamp date,
  uint64_t start_row,
  int64_t delta,
  struct GNUNET_TIME_Relative timeout,
  TALER_MERCHANT_OrdersGetCallback cb,
  void *cb_cls)
{
  return TALER_MERCHANT_orders_get3 (
    ctx,
    backend_url,
    paid,
    refunded,
    wired,
    NULL,
    NULL,
    date,
    start_row,
    delta,
    timeout,
    cb,
    cb_cls);
}


struct TALER_MERCHANT_OrdersGetHandle *
TALER_MERCHANT_orders_get3 (
  struct GNUNET_CURL_Context *ctx,
  const char *backend_url,
  enum TALER_EXCHANGE_YesNoAll paid,
  enum TALER_EXCHANGE_YesNoAll refunded,
  enum TALER_EXCHANGE_YesNoAll wired,
  const char *session_id,
  const char *fulfillment_url,
  struct GNUNET_TIME_Timestamp date,
  uint64_t start_row,
  int64_t delta,
  struct GNUNET_TIME_Relative timeout,
  TALER_MERCHANT_OrdersGetCallback cb,
  void *cb_cls)
{
  struct TALER_MERCHANT_OrdersGetHandle *ogh;
  CURL *eh;
  unsigned int tms = timeout.rel_value_us
                     / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us;

  GNUNET_assert (NULL != backend_url);
  if ( (delta > MAX_ORDERS) ||
       (delta < -MAX_ORDERS) )
  {
    GNUNET_break (0);
    return NULL;
  }
  if (0 == delta)
  {
    GNUNET_break (0);
    return NULL;
  }
  ogh = GNUNET_new (struct TALER_MERCHANT_OrdersGetHandle);
  ogh->ctx = ctx;
  ogh->cb = cb;
  ogh->cb_cls = cb_cls;

  /* build ogh->url with the various optional arguments */
  {
    char dstr[30];
    char *fec = NULL;
    char *sid = NULL;
    bool have_date;
    bool have_srow;
    char cbuf[30];
    char dbuf[30];
    char tbuf[30];

    GNUNET_snprintf (tbuf,
                     sizeof (tbuf),
                     "%u",
                     tms);
    GNUNET_snprintf (dbuf,
                     sizeof (dbuf),
                     "%lld",
                     (long long) delta);
    GNUNET_snprintf (cbuf,
                     sizeof (cbuf),
                     "%llu",
                     (unsigned long long) start_row);
    if (NULL != session_id)
      (void) GNUNET_STRINGS_urlencode (strlen (session_id),
                                       session_id,
                                       &sid);
    if (NULL != fulfillment_url)
      (void) GNUNET_STRINGS_urlencode (strlen (fulfillment_url),
                                       fulfillment_url,
                                       &fec);
    GNUNET_snprintf (dstr,
                     sizeof (dstr),
                     "%llu",
                     (unsigned long long) GNUNET_TIME_timestamp_to_s (date));
    if (delta > 0)
    {
      have_date = ! GNUNET_TIME_absolute_is_zero (date.abs_time);
      have_srow = (0 != start_row);
    }
    else
    {
      have_date = ! GNUNET_TIME_absolute_is_never (date.abs_time);
      have_srow = (UINT64_MAX != start_row);
    }
    ogh->url = TALER_url_join (backend_url,
                               "private/orders",
                               "paid",
                               (TALER_EXCHANGE_YNA_ALL != paid)
                               ? TALER_yna_to_string (paid)
                               : NULL,
                               "refunded",
                               (TALER_EXCHANGE_YNA_ALL != refunded)
                               ? TALER_yna_to_string (refunded)
                               : NULL,
                               "wired",
                               (TALER_EXCHANGE_YNA_ALL != wired)
                               ? TALER_yna_to_string (wired)
                               : NULL,
                               "date_s",
                               (have_date)
                               ? dstr
                               : NULL,
                               "start",
                               (have_srow)
                               ? cbuf
                               : NULL,
                               "delta",
                               (-20 != delta)
                               ? dbuf
                               : NULL,
                               "timeout_ms",
                               (0 != tms)
                               ? tbuf
                               : NULL,
                               "session_id",
                               sid,
                               "fulfillment_url",
                               fec,
                               NULL);
    GNUNET_free (sid);
    GNUNET_free (fec);
  }
  if (NULL == ogh->url)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Could not construct request URL.\n");
    GNUNET_free (ogh);
    return NULL;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Requesting URL '%s'\n",
              ogh->url);
  eh = TALER_MERCHANT_curl_easy_get_ (ogh->url);
  if (NULL == eh)
  {
    GNUNET_break (0);
    GNUNET_free (ogh->url);
    GNUNET_free (ogh);
    return NULL;
  }
  if (0 != tms)
  {
    GNUNET_break (CURLE_OK ==
                  curl_easy_setopt (eh,
                                    CURLOPT_TIMEOUT_MS,
                                    (long) (tms + 100L)));
  }
  ogh->job = GNUNET_CURL_job_add (ctx,
                                  eh,
                                  &handle_get_orders_finished,
                                  ogh);
  return ogh;
}


void
TALER_MERCHANT_orders_get_cancel (
  struct TALER_MERCHANT_OrdersGetHandle *ogh)
{
  if (NULL != ogh->job)
    GNUNET_CURL_job_cancel (ogh->job);
  GNUNET_free (ogh->url);
  GNUNET_free (ogh);
}
