/*
  This file is part of TALER
  Copyright (C) 2018, 2019, 2020 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_merchant_get_order.c
 * @brief Implementation of the GET /private/orders/$ORDER request
 * @author Christian Grothoff
 * @author Marcello Stanisci
 * @author Florian Dold
 */
#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 refund details we return.
 */
#define MAX_REFUND_DETAILS 1024

/**
 * Maximum number of wire details we return.
 */
#define MAX_WIRE_DETAILS 1024


/**
 * @brief A GET /private/orders/$ORDER handle
 */
struct TALER_MERCHANT_OrderMerchantGetHandle
{

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

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

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

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

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


/**
 * Function called when we're done processing the GET /private/orders/$ORDER
 * request and we got an HTTP status of OK and the order was unpaid. Parse
 * the response and call the callback.
 *
 * @param omgh handle for the request
 * @param[in,out] osr HTTP response we got
 */
static void
handle_unpaid (struct TALER_MERCHANT_OrderMerchantGetHandle *omgh,
               struct TALER_MERCHANT_OrderStatusResponse *osr)
{
  struct GNUNET_JSON_Specification spec[] = {
    TALER_JSON_spec_amount_any (
      "total_amount",
      &osr->details.ok.details.unpaid.contract_amount),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string (
        "already_paid_order_id",
        &osr->details.ok.details.unpaid.already_paid_order_id),
      NULL),
    GNUNET_JSON_spec_string (
      "taler_pay_uri",
      &osr->details.ok.details.unpaid.taler_pay_uri),
    GNUNET_JSON_spec_string (
      "summary",
      &osr->details.ok.details.unpaid.summary),
    GNUNET_JSON_spec_timestamp (
      "creation_time",
      &osr->details.ok.details.unpaid.creation_time),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (osr->hr.reply,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    osr->hr.http_status = 0;
    osr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    omgh->cb (omgh->cb_cls,
              osr);
    return;
  }
  osr->details.ok.status = TALER_MERCHANT_OSC_UNPAID;
  omgh->cb (omgh->cb_cls,
            osr);
}


/**
 * Function called when we're done processing the GET /private/orders/$ORDER
 * request and we got an HTTP status of OK and the order was claimed but not
 * paid. Parse the response and call the callback.
 *
 * @param omgh handle for the request
 * @param[in,out] osr HTTP response we got
 */
static void
handle_claimed (struct TALER_MERCHANT_OrderMerchantGetHandle *omgh,
                struct TALER_MERCHANT_OrderStatusResponse *osr)
{
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_object_const (
      "contract_terms",
      &osr->details.ok.details.claimed.contract_terms),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (osr->hr.reply,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    osr->hr.http_status = 0;
    osr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    omgh->cb (omgh->cb_cls,
              osr);
    return;
  }
  osr->details.ok.status = TALER_MERCHANT_OSC_CLAIMED;
  omgh->cb (omgh->cb_cls,
            osr);
}


/**
 * Function called when we're done processing the GET /private/orders/$ORDER
 * request and we got an HTTP status of OK and the order was paid. Parse
 * the response and call the callback.
 *
 * @param omgh handle for the request
 * @param[in,out] osr HTTP response we got
 */
static void
handle_paid (struct TALER_MERCHANT_OrderMerchantGetHandle *omgh,
             struct TALER_MERCHANT_OrderStatusResponse *osr)
{
  uint32_t hc32;
  const json_t *wire_details;
  const json_t *refund_details;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_bool ("refunded",
                           &osr->details.ok.details.paid.refunded),
    GNUNET_JSON_spec_bool ("refund_pending",
                           &osr->details.ok.details.paid.refund_pending),
    GNUNET_JSON_spec_bool ("wired",
                           &osr->details.ok.details.paid.wired),
    TALER_JSON_spec_amount_any ("deposit_total",
                                &osr->details.ok.details.paid.deposit_total),
    TALER_JSON_spec_ec ("exchange_code",
                        &osr->details.ok.details.paid.exchange_ec),
    GNUNET_JSON_spec_uint32 ("exchange_http_status",
                             &hc32),
    TALER_JSON_spec_amount_any ("refund_amount",
                                &osr->details.ok.details.paid.refund_amount),
    GNUNET_JSON_spec_object_const (
      "contract_terms",
      &osr->details.ok.details.paid.contract_terms),
    GNUNET_JSON_spec_array_const ("wire_details",
                                  &wire_details),
    GNUNET_JSON_spec_array_const ("refund_details",
                                  &refund_details),
    /* Only available since **v14** */
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_timestamp ("last_payment",
                                  &osr->details.ok.details.paid.last_payment),
      NULL),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (osr->hr.reply,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    osr->hr.http_status = 0;
    osr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    omgh->cb (omgh->cb_cls,
              osr);
    return;
  }
  osr->details.ok.status = TALER_MERCHANT_OSC_PAID;

  osr->details.ok.details.paid.exchange_hc = (unsigned int) hc32;
  {
    unsigned int wts_len = (unsigned int) json_array_size (wire_details);
    unsigned int ref_len = (unsigned int) json_array_size (refund_details);

    if ( (json_array_size (wire_details) != (size_t)  wts_len) ||
         (wts_len > MAX_WIRE_DETAILS) )
    {
      GNUNET_break (0);
      osr->hr.http_status = 0;
      osr->hr.ec = TALER_EC_GENERIC_ALLOCATION_FAILURE;
      omgh->cb (omgh->cb_cls,
                osr);
      return;
    }
    if ( (json_array_size (refund_details) != (size_t)  ref_len) ||
         (ref_len > MAX_REFUND_DETAILS) )
    {
      GNUNET_break (0);
      osr->hr.http_status = 0;
      osr->hr.ec = TALER_EC_GENERIC_ALLOCATION_FAILURE;
      omgh->cb (omgh->cb_cls,
                osr);
      return;
    }
    {
      struct TALER_MERCHANT_WireTransfer wts[GNUNET_NZL (wts_len)];
      struct TALER_MERCHANT_RefundOrderDetail ref[GNUNET_NZL (ref_len)];

      for (unsigned int i = 0; i<wts_len; i++)
      {
        struct TALER_MERCHANT_WireTransfer *wt = &wts[i];
        const json_t *w = json_array_get (wire_details,
                                          i);
        struct GNUNET_JSON_Specification ispec[] = {
          TALER_JSON_spec_web_url ("exchange_url",
                                   &wt->exchange_url),
          GNUNET_JSON_spec_fixed_auto ("wtid",
                                       &wt->wtid),
          GNUNET_JSON_spec_timestamp ("execution_time",
                                      &wt->execution_time),
          TALER_JSON_spec_amount_any ("amount",
                                      &wt->total_amount),
          GNUNET_JSON_spec_bool ("confirmed",
                                 &wt->confirmed),
          GNUNET_JSON_spec_end ()
        };

        if (GNUNET_OK !=
            GNUNET_JSON_parse (w,
                               ispec,
                               NULL, NULL))
        {
          GNUNET_break_op (0);
          osr->hr.http_status = 0;
          osr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
          omgh->cb (omgh->cb_cls,
                    osr);
          return;
        }
      }

      for (unsigned int i = 0; i<ref_len; i++)
      {
        struct TALER_MERCHANT_RefundOrderDetail *ro = &ref[i];
        const json_t *w = json_array_get (refund_details,
                                          i);
        struct GNUNET_JSON_Specification ispec[] = {
          TALER_JSON_spec_amount_any ("amount",
                                      &ro->refund_amount),
          GNUNET_JSON_spec_string ("reason",
                                   &ro->reason),
          GNUNET_JSON_spec_timestamp ("timestamp",
                                      &ro->refund_time),
          GNUNET_JSON_spec_end ()
        };

        if (GNUNET_OK !=
            GNUNET_JSON_parse (w,
                               ispec,
                               NULL, NULL))
        {
          GNUNET_break_op (0);
          osr->hr.http_status = 0;
          osr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
          omgh->cb (omgh->cb_cls,
                    osr);
          return;
        }
      }

      osr->details.ok.details.paid.wts = wts;
      osr->details.ok.details.paid.wts_len = wts_len;
      osr->details.ok.details.paid.refunds = ref;
      osr->details.ok.details.paid.refunds_len = ref_len;
      omgh->cb (omgh->cb_cls,
                osr);
    }
  }
}


/**
 * Function called when we're done processing the GET /private/orders/$ORDER
 * request.
 *
 * @param cls the `struct TALER_MERCHANT_OrderMerchantGetHandle`
 * @param response_code HTTP response code, 0 on error
 * @param response response body, NULL if not in JSON
 */
static void
handle_merchant_order_get_finished (void *cls,
                                    long response_code,
                                    const void *response)
{
  struct TALER_MERCHANT_OrderMerchantGetHandle *omgh = cls;
  const json_t *json = response;
  const char *order_status;
  struct TALER_MERCHANT_OrderStatusResponse osr = {
    .hr.http_status = (unsigned int) response_code,
    .hr.reply = json
  };

  omgh->job = NULL;
  switch (response_code)
  {
  case MHD_HTTP_OK:
    /* see below */
    break;
  case MHD_HTTP_ACCEPTED:
    /* see below */
    omgh->cb (omgh->cb_cls,
              &osr);
    TALER_MERCHANT_merchant_order_get_cancel (omgh);
    return;
  case MHD_HTTP_UNAUTHORIZED:
    osr.hr.ec = TALER_JSON_get_error_code (json);
    osr.hr.hint = TALER_JSON_get_error_hint (json);
    omgh->cb (omgh->cb_cls,
              &osr);
    TALER_MERCHANT_merchant_order_get_cancel (omgh);
    return;
  case MHD_HTTP_NOT_FOUND:
    osr.hr.ec = TALER_JSON_get_error_code (json);
    osr.hr.hint = TALER_JSON_get_error_hint (json);
    omgh->cb (omgh->cb_cls,
              &osr);
    TALER_MERCHANT_merchant_order_get_cancel (omgh);
    return;
  case MHD_HTTP_GATEWAY_TIMEOUT:
    osr.hr.ec = TALER_JSON_get_error_code (json);
    osr.hr.hint = TALER_JSON_get_error_hint (json);
    omgh->cb (omgh->cb_cls,
              &osr);
    TALER_MERCHANT_merchant_order_get_cancel (omgh);
    return;
  default:
    osr.hr.ec = TALER_JSON_get_error_code (json);
    osr.hr.hint = TALER_JSON_get_error_hint (json);
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Polling payment failed with HTTP status code %u/%d\n",
                (unsigned int) response_code,
                (int) osr.hr.ec);
    GNUNET_break_op (0);
    omgh->cb (omgh->cb_cls,
              &osr);
    TALER_MERCHANT_merchant_order_get_cancel (omgh);
    return;
  }

  order_status = json_string_value (json_object_get (json,
                                                     "order_status"));

  if (NULL == order_status)
  {
    GNUNET_break_op (0);
    osr.hr.http_status = 0;
    osr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    omgh->cb (omgh->cb_cls,
              &osr);
    TALER_MERCHANT_merchant_order_get_cancel (omgh);
    return;
  }

  if (0 == strcmp ("paid",
                   order_status))
  {
    handle_paid (omgh,
                 &osr);
  }
  else if (0 == strcmp ("claimed",
                        order_status))
  {
    handle_claimed (omgh,
                    &osr);
  }
  else if (0 == strcmp ("unpaid",
                        order_status))
  {
    handle_unpaid (omgh,
                   &osr);
  }
  else
  {
    GNUNET_break_op (0);
    osr.hr.http_status = 0;
    osr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    omgh->cb (omgh->cb_cls,
              &osr);
  }
  TALER_MERCHANT_merchant_order_get_cancel (omgh);
}


struct TALER_MERCHANT_OrderMerchantGetHandle *
TALER_MERCHANT_merchant_order_get (
  struct GNUNET_CURL_Context *ctx,
  const char *backend_url,
  const char *order_id,
  const char *session_id,
  struct GNUNET_TIME_Relative timeout,
  TALER_MERCHANT_OrderMerchantGetCallback cb,
  void *cb_cls)
{
  struct TALER_MERCHANT_OrderMerchantGetHandle *omgh;
  unsigned int tms;

  tms = (unsigned int) (timeout.rel_value_us
                        / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us);
  omgh = GNUNET_new (struct TALER_MERCHANT_OrderMerchantGetHandle);
  omgh->ctx = ctx;
  omgh->cb = cb;
  omgh->cb_cls = cb_cls;
  {
    char *path;
    char timeout_ms[32];

    GNUNET_snprintf (timeout_ms,
                     sizeof (timeout_ms),
                     "%u",
                     tms);
    GNUNET_asprintf (&path,
                     "private/orders/%s",
                     order_id);
    omgh->url = TALER_url_join (backend_url,
                                path,
                                "session_id", session_id,
                                "timeout_ms", (0 != tms) ? timeout_ms : NULL,
                                NULL);
    GNUNET_free (path);
  }
  if (NULL == omgh->url)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Could not construct request URL.\n");
    GNUNET_free (omgh);
    return NULL;
  }

  {
    CURL *eh;

    eh = TALER_MERCHANT_curl_easy_get_ (omgh->url);
    if (NULL == eh)
    {
      GNUNET_break (0);
      GNUNET_free (omgh->url);
      GNUNET_free (omgh);
      return NULL;
    }
    if (0 != tms)
    {
      GNUNET_break (CURLE_OK ==
                    curl_easy_setopt (eh,
                                      CURLOPT_TIMEOUT_MS,
                                      (long) (tms + 100L)));
    }

    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Getting order status from %s\n",
                omgh->url);
    if (NULL == (omgh->job =
                   GNUNET_CURL_job_add (ctx,
                                        eh,
                                        &handle_merchant_order_get_finished,
                                        omgh)))
    {
      GNUNET_break (0);
      GNUNET_free (omgh->url);
      GNUNET_free (omgh);
      return NULL;
    }
  }
  return omgh;
}


void
TALER_MERCHANT_merchant_order_get_cancel (
  struct TALER_MERCHANT_OrderMerchantGetHandle *omgh)
{
  if (NULL != omgh->job)
  {
    GNUNET_CURL_job_cancel (omgh->job);
    omgh->job = NULL;
  }
  GNUNET_free (omgh->url);
  GNUNET_free (omgh);
}


/* end of merchant_api_merchant_get_order.c */
