/*
  This file is part of TALER
  Copyright (C) 2020-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_common.c
 * @brief Implementation of common logic for libtalermerchant
 * @author Christian Grothoff
 * @author Priscilla Huang
 */
#include "platform.h"
#include "microhttpd.h"
#include <curl/curl.h>
#include "taler_merchant_service.h"
#include "merchant_api_common.h"
#include <gnunet/gnunet_uri_lib.h>
#include <taler/taler_json_lib.h>
#include "taler_merchant_util.h"


static void
TALER_MERCHANT_format_fractional_string (uint64_t integer,
                                         uint32_t fractional,
                                         char *buffer,
                                         size_t buffer_length)
{
  GNUNET_assert (NULL != buffer);
  GNUNET_assert (0 < buffer_length);
  GNUNET_assert (fractional < MERCHANT_UNIT_FRAC_BASE);

  if (0 == fractional)
  {
    GNUNET_snprintf (buffer,
                     buffer_length,
                     "%lu",
                     integer);
    return;
  }
  {
    char frac_buf[MERCHANT_UNIT_FRAC_MAX_DIGITS + 1];
    size_t idx;

    GNUNET_snprintf (frac_buf,
                     sizeof (frac_buf),
                     "%0*u",
                     MERCHANT_UNIT_FRAC_MAX_DIGITS,
                     (unsigned int) fractional);
    for (idx = strlen (frac_buf); idx > 0; idx--)
    {
      if ('0' != frac_buf[idx - 1])
        break;
      frac_buf[idx - 1] = '\0';
    }
    if ('\0' == frac_buf[0])
      GNUNET_strlcpy (frac_buf,
                      "0",
                      sizeof (frac_buf));
    GNUNET_snprintf (buffer,
                     buffer_length,
                     "%lu.%s",
                     integer,
                     frac_buf);
  }
}


void
TALER_MERCHANT_format_quantity_string (uint64_t quantity,
                                       uint32_t quantity_frac,
                                       char *buffer,
                                       size_t buffer_length)
{
  TALER_MERCHANT_format_fractional_string (quantity,
                                           quantity_frac,
                                           buffer,
                                           buffer_length);
}


void
TALER_MERCHANT_format_stock_string (uint64_t total_stock,
                                    uint32_t total_stock_frac,
                                    char *buffer,
                                    size_t buffer_length)
{
  if ( (INT64_MAX == (int64_t) total_stock) &&
       (INT32_MAX == (int32_t) total_stock_frac) )
  {
    GNUNET_snprintf (buffer,
                     buffer_length,
                     "-1");
    return;
  }
  TALER_MERCHANT_format_fractional_string (total_stock,
                                           total_stock_frac,
                                           buffer,
                                           buffer_length);
}


void
TALER_MERCHANT_parse_error_details_ (const json_t *response,
                                     unsigned int http_status,
                                     struct TALER_MERCHANT_HttpResponse *hr)
{
  const json_t *jc;

  memset (hr, 0, sizeof (*hr));
  hr->reply = response;
  hr->http_status = http_status;
  if (NULL == response)
  {
    hr->ec = TALER_EC_GENERIC_INVALID_RESPONSE;
    return;
  }
  hr->ec = TALER_JSON_get_error_code (response);
  hr->hint = TALER_JSON_get_error_hint (response);

  /* handle 'exchange_http_status' */
  jc = json_object_get (response,
                        "exchange_http_status");
  /* The caller already knows that the JSON represents an error,
     so we are dealing with a missing error code here.  */
  if (NULL == jc)
    return; /* no need to bother with exchange_code/hint if we had no status */
  if (! json_is_integer (jc))
  {
    GNUNET_break_op (0);
    return;
  }
  hr->exchange_http_status = (unsigned int) json_integer_value (jc);

  /* handle 'exchange_reply' */
  jc = json_object_get (response,
                        "exchange_reply");
  if (! json_is_object (jc))
  {
    GNUNET_break_op (0);
  }
  else
  {
    hr->exchange_reply = jc;
  }

  /* handle 'exchange_code' */
  jc = json_object_get (response,
                        "exchange_code");
  /* The caller already knows that the JSON represents an error,
     so we are dealing with a missing error code here.  */
  if (NULL == jc)
    return; /* no need to bother with exchange-hint if we had no code */
  if (! json_is_integer (jc))
  {
    GNUNET_break_op (0);
    hr->exchange_code = TALER_EC_INVALID;
  }
  else
  {
    hr->exchange_code = (enum TALER_ErrorCode) (int) json_integer_value (jc);
  }

  /* handle 'exchange-hint' */
  jc = json_object_get (response,
                        "exchange-hint");
  /* The caller already knows that the JSON represents an error,
     so we are dealing with a missing error code here.  */
  if (NULL == jc)
    return;
  if (! json_is_string (jc))
  {
    GNUNET_break_op (0);
  }
  else
  {
    hr->exchange_hint = json_string_value (jc);
  }
}


enum GNUNET_GenericReturnValue
TALER_MERCHANT_parse_pay_uri (const char *pay_uri,
                              struct TALER_MERCHANT_PayUriData *parse_data)
{
  char *cp = GNUNET_strdup (pay_uri);
  struct GNUNET_Uri u;

  if (0 !=
      GNUNET_uri_parse (&u,
                        cp))
  {
    GNUNET_free (cp);
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if ((0 != strcasecmp ("taler",
                        u.scheme)) &&
      (0 != strcasecmp ("taler+http",
                        u.scheme)))
  {
    fprintf (stderr,
             "Bad schema %s\n",
             u.scheme);
    GNUNET_free (cp);
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  parse_data->use_http = (0 == strcasecmp ("taler+http",
                                           u.scheme));
  if (0 != strcasecmp ("pay",
                       u.host))
  {
    GNUNET_break_op (0);
    GNUNET_free (cp);
    return GNUNET_SYSERR;
  }

  {
    char *order_id;
    char *mpp;
    char *session_id = strrchr (u.path,
                                '/');
    struct TALER_ClaimTokenP *claim_token = NULL;

    if (NULL == session_id)
    {
      GNUNET_break_op (0);
      GNUNET_free (cp);
      return GNUNET_SYSERR;
    }
    *session_id = '\0';
    ++session_id;

    order_id = strrchr (u.path,
                        '/');
    if (NULL == order_id)
    {
      GNUNET_break_op (0);
      GNUNET_free (cp);
      return GNUNET_SYSERR;
    }
    *order_id = '\0';
    ++order_id;
    mpp = strchr (u.path,
                  '/');
    if (NULL != mpp)
    {
      *mpp = '\0';
      ++mpp;
    }

    {
      char *ct_str = u.query;
      char *ct_data;

      if (NULL != ct_str)
      {
        ct_data = strchr (u.query,
                          '=');
        if (NULL == ct_data)
        {
          GNUNET_break_op (0);
          GNUNET_free (cp);
          return GNUNET_SYSERR;
        }
        *ct_data = '\0';
        ++ct_data;
        claim_token = GNUNET_new (struct TALER_ClaimTokenP);
        if ( (0 != strcmp ("c",
                           u.query)) ||
             (GNUNET_OK !=
              GNUNET_STRINGS_string_to_data (ct_data,
                                             strlen (ct_data),
                                             claim_token,
                                             sizeof (*claim_token))) )
        {
          GNUNET_break_op (0);
          GNUNET_free (claim_token);
          GNUNET_free (cp);
          return GNUNET_SYSERR;
        }
      }
    }

    parse_data->merchant_prefix_path
      = (NULL == mpp)
        ? NULL
        : GNUNET_strdup (mpp);
    parse_data->merchant_host = GNUNET_strdup (u.path);
    parse_data->order_id = GNUNET_strdup (order_id);
    parse_data->session_id
      = (0 < strlen (session_id))
        ? GNUNET_strdup (session_id)
        : NULL;
    parse_data->claim_token = claim_token;
    parse_data->ssid
      = (NULL == u.fragment)
        ? NULL
        : GNUNET_strdup (u.fragment);
  }
  GNUNET_free (cp);
  return GNUNET_OK;
}


void
TALER_MERCHANT_parse_pay_uri_free (
  struct TALER_MERCHANT_PayUriData *parse_data)
{
  GNUNET_free (parse_data->merchant_host);
  GNUNET_free (parse_data->merchant_prefix_path);
  GNUNET_free (parse_data->order_id);
  GNUNET_free (parse_data->session_id);
  GNUNET_free (parse_data->claim_token);
  GNUNET_free (parse_data->ssid);
}


enum GNUNET_GenericReturnValue
TALER_MERCHANT_parse_refund_uri (
  const char *refund_uri,
  struct TALER_MERCHANT_RefundUriData *parse_data)
{
  char *cp = GNUNET_strdup (refund_uri);
  struct GNUNET_Uri u;

  if (0 !=
      GNUNET_uri_parse (&u,
                        cp))
  {
    GNUNET_free (cp);
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if ((0 != strcasecmp ("taler",
                        u.scheme)) &&
      (0 != strcasecmp ("taler+http",
                        u.scheme)))
  {
    GNUNET_free (cp);
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  parse_data->use_http = (0 == strcasecmp ("taler+http",
                                           u.scheme));

  if (0 != strcasecmp ("refund",
                       u.host))
  {
    GNUNET_break_op (0);
    GNUNET_free (cp);
    return GNUNET_SYSERR;
  }

  {
    char *order_id;
    char *last_seg = strrchr (u.path,
                              '/');

    if (NULL == last_seg)
    {
      GNUNET_break_op (0);
      GNUNET_free (cp);
      return GNUNET_SYSERR;
    }
    *last_seg = '\0';
    ++last_seg;

    order_id = strrchr (u.path,
                        '/');
    if (NULL == order_id)
    {
      GNUNET_break_op (0);
      GNUNET_free (cp);
      return GNUNET_SYSERR;
    }
    *order_id = '\0';
    ++order_id;
    if (0 != strlen (last_seg))
    {
      GNUNET_break_op (0);
      GNUNET_free (cp);
      return GNUNET_SYSERR;
    }

    {
      char *mpp;

      mpp = strchr (u.path,
                    '/');
      if (NULL != mpp)
      {
        *mpp = '\0';
        ++mpp;
      }

      parse_data->merchant_prefix_path
        = (NULL == mpp)
          ? NULL
          : GNUNET_strdup (mpp);
    }
    parse_data->merchant_host = GNUNET_strdup (u.path);
    parse_data->order_id = GNUNET_strdup (order_id);
    parse_data->ssid
      = (NULL == u.fragment)
        ? NULL
        : GNUNET_strdup (u.fragment);
  }
  GNUNET_free (cp);
  return GNUNET_OK;
}


void
TALER_MERCHANT_parse_refund_uri_free (
  struct TALER_MERCHANT_RefundUriData *parse_data)
{
  GNUNET_free (parse_data->merchant_host);
  GNUNET_free (parse_data->merchant_prefix_path);
  GNUNET_free (parse_data->order_id);
  GNUNET_free (parse_data->ssid);
}


void
TALER_MERCHANT_handle_order_creation_response_ (
  TALER_MERCHANT_PostOrdersCallback cb,
  void *cb_cls,
  long response_code,
  const json_t *json)
{
  struct TALER_MERCHANT_PostOrdersReply por = {
    .hr.http_status = (unsigned int) response_code,
    .hr.reply = json
  };
  struct TALER_ClaimTokenP token;

  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[] = {
        GNUNET_JSON_spec_string ("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 (
          /* optional for pre-v21 compatibility */
          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;
      }
      else
      {
        if (! no_token)
          por.details.ok.token = &token;
        if (no_pay_deadline)
        {
          GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                      "Talking to outdated merchant backend, payment deadline unavailable.\n");
          por.details.ok.pay_deadline = GNUNET_TIME_UNIT_ZERO_TS;
        }
      }
    }
    break;
  case MHD_HTTP_BAD_REQUEST:
    json_dumpf (json,
                stderr,
                JSON_INDENT (2));
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.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_UNAUTHORIZED:
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    /* Nothing really to verify, merchant says we need to authenticate. */
    break;
  case MHD_HTTP_FORBIDDEN:
    /* Nothing really to verify, merchant says one
       of the signatures is invalid; as we checked them,
       this should never happen, we should pass the JSON
       reply to the application */
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_NOT_FOUND:
    /* Nothing really to verify, this should never
       happen, we should pass the JSON reply to the application */
    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:
    /* The quantity of some product requested was not available. */
    {
      bool rq_frac_missing;
      bool aq_frac_missing;

      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_string (
          "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_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:
    /* Order total too high for legal reasons */
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  case MHD_HTTP_INTERNAL_SERVER_ERROR:
    /* Server had an internal issue; we should retry,
       but this API leaves this to the application */
    por.hr.ec = TALER_JSON_get_error_code (json);
    por.hr.hint = TALER_JSON_get_error_hint (json);
    break;
  default:
    /* unexpected response code */
    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;
  } // end of switch
  cb (cb_cls,
      &por);
}
