/*
  This file is part of TALER
  Copyright (C) 2014-2025 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_config.c
 * @brief Implementation of the /config 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>

/**
 * Which version of the Taler protocol is implemented
 * by this library?  Used to determine compatibility.
 */
#define MERCHANT_PROTOCOL_CURRENT 24

/**
 * How many configs are we backwards-compatible with?
 */
#define MERCHANT_PROTOCOL_AGE 0

/**
 * How many exchanges do we allow at most per merchant?
 */
#define MAX_EXCHANGES 1024

/**
 * How many currency specs do we allow at most per merchant?
 */
#define MAX_CURRENCIES 1024

/**
 * @brief A handle for /config operations
 */
struct TALER_MERCHANT_ConfigGetHandle
{
  /**
   * The url for this request.
   */
  char *url;

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

  /**
   * Function to call with the result.
   */
  TALER_MERCHANT_ConfigCallback 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
 * HTTP /config request.
 *
 * @param cls the `struct TALER_MERCHANT_ConfigGetHandle`
 * @param response_code HTTP response code, 0 on error
 * @param response response body, NULL if not in JSON
 */
static void
handle_config_finished (void *cls,
                        long response_code,
                        const void *response)
{
  struct TALER_MERCHANT_ConfigGetHandle *vgh = cls;
  const json_t *json = response;
  struct TALER_MERCHANT_ConfigResponse cr = {
    .hr.http_status = (unsigned int) response_code,
    .hr.reply = json
  };

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

  vgh->job = NULL;
  switch (response_code)
  {
  case MHD_HTTP_OK:
    {
      const json_t *jcs;
      const json_t *exchanges = NULL;
      struct TALER_MERCHANT_ExchangeConfigInfo *eci = NULL;
      unsigned int num_eci = 0;
      unsigned int nspec;
      struct TALER_JSON_ProtocolVersion pv;
      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_object_const ("currencies",
                                       &jcs),
        GNUNET_JSON_spec_array_const ("exchanges",
                                      &exchanges),
        GNUNET_JSON_spec_string ("currency",
                                 &cr.details.ok.ci.currency),
        TALER_JSON_spec_version ("version",
                                 &pv),
        GNUNET_JSON_spec_string ("version",
                                 &cr.details.ok.ci.version),
        GNUNET_JSON_spec_end ()
      };

      cr.details.ok.compat = TALER_MERCHANT_VC_PROTOCOL_ERROR;
      if (GNUNET_OK !=
          GNUNET_JSON_parse (json,
                             spec,
                             NULL, NULL))
      {
        GNUNET_break_op (0);
        cr.hr.http_status = 0;
        cr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
        break;
      }
      cr.details.ok.compat = TALER_MERCHANT_VC_MATCH;
      if (MERCHANT_PROTOCOL_CURRENT < pv.current)
      {
        cr.details.ok.compat |= TALER_MERCHANT_VC_NEWER;
        if (MERCHANT_PROTOCOL_CURRENT < pv.current - pv.age)
          cr.details.ok.compat |= TALER_MERCHANT_VC_INCOMPATIBLE;
      }
      if (MERCHANT_PROTOCOL_CURRENT > pv.current)
      {
        cr.details.ok.compat |= TALER_MERCHANT_VC_OLDER;
        if (MERCHANT_PROTOCOL_CURRENT - MERCHANT_PROTOCOL_AGE > pv.current)
          cr.details.ok.compat |= TALER_MERCHANT_VC_INCOMPATIBLE;
      }

      nspec = (unsigned int) json_object_size (jcs);
      if ( (nspec > MAX_CURRENCIES) ||
           (json_object_size (jcs) != (size_t) nspec) )
      {
        GNUNET_break_op (0);
        cr.hr.http_status = 0;
        cr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
        break;
      }
      if (NULL != exchanges)
      {
        num_eci = (unsigned int) json_object_size (exchanges);
        if ( (num_eci > MAX_EXCHANGES) ||
             (json_object_size (exchanges) != (size_t) num_eci) )
        {
          GNUNET_break_op (0);
          cr.hr.http_status = 0;
          cr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
          break;
        }
        eci = GNUNET_new_array (num_eci,
                                struct TALER_MERCHANT_ExchangeConfigInfo);
        for (unsigned int i = 0; i<num_eci; i++)
        {
          struct TALER_MERCHANT_ExchangeConfigInfo *ei = &eci[i];
          const json_t *ej = json_array_get (exchanges,
                                             i);
          struct GNUNET_JSON_Specification ispec[] = {
            GNUNET_JSON_spec_string ("currency",
                                     &ei->currency),
            GNUNET_JSON_spec_string ("base_url",
                                     &ei->base_url),
            GNUNET_JSON_spec_fixed_auto ("master_pub",
                                         &ei->master_pub),
            GNUNET_JSON_spec_end ()
          };

          if (GNUNET_OK !=
              GNUNET_JSON_parse (ej,
                                 ispec,
                                 NULL, NULL))
          {
            GNUNET_break_op (0);
            cr.hr.http_status = 0;
            cr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
            GNUNET_free (eci);
            break;
          }
        }
      }
      {
        struct TALER_CurrencySpecification *cspecs;
        unsigned int off = 0;
        json_t *obj;
        const char *curr;

        cspecs = GNUNET_new_array (nspec,
                                   struct TALER_CurrencySpecification);
        cr.details.ok.num_cspecs = nspec;
        cr.details.ok.cspecs = cspecs;
        cr.details.ok.num_exchanges = (unsigned int) num_eci;
        cr.details.ok.exchanges = eci;
        json_object_foreach ((json_t *) jcs, curr, obj)
        {
          struct TALER_CurrencySpecification *cs = &cspecs[off++];
          struct GNUNET_JSON_Specification cspec[] = {
            TALER_JSON_spec_currency_specification (curr,
                                                    curr,
                                                    cs),
            GNUNET_JSON_spec_end ()
          };

          if (GNUNET_OK !=
              GNUNET_JSON_parse (jcs,
                                 cspec,
                                 NULL, NULL))
          {
            GNUNET_break_op (0);
            cr.hr.http_status = 0;
            cr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
            GNUNET_free (eci);
            TALER_CONFIG_free_currencies (off - 1,
                                          cspecs);
            break;
          }
        }
        vgh->cb (vgh->cb_cls,
                 &cr);
        GNUNET_free (eci);
        TALER_CONFIG_free_currencies (nspec,
                                      cspecs);
      }
      TALER_MERCHANT_config_get_cancel (vgh);
      return;
    }
  default:
    /* unexpected response code */
    cr.hr.ec = TALER_JSON_get_error_code (json);
    cr.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) cr.hr.ec);
    break;
  }
  vgh->cb (vgh->cb_cls,
           &cr);
  TALER_MERCHANT_config_get_cancel (vgh);
}


struct TALER_MERCHANT_ConfigGetHandle *
TALER_MERCHANT_config_get (struct GNUNET_CURL_Context *ctx,
                           const char *backend_url,
                           TALER_MERCHANT_ConfigCallback config_cb,
                           void *config_cb_cls)
{
  struct TALER_MERCHANT_ConfigGetHandle *vgh;
  CURL *eh;

  vgh = GNUNET_new (struct TALER_MERCHANT_ConfigGetHandle);
  vgh->ctx = ctx;
  vgh->cb = config_cb;
  vgh->cb_cls = config_cb_cls;
  vgh->url = TALER_url_join (backend_url,
                             "config",
                             NULL);
  if (NULL == vgh->url)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Could not construct request URL.\n");
    GNUNET_free (vgh);
    return NULL;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Requesting URL '%s'\n",
              vgh->url);
  eh = TALER_MERCHANT_curl_easy_get_ (vgh->url);
  vgh->job = GNUNET_CURL_job_add (ctx,
                                  eh,
                                  &handle_config_finished,
                                  vgh);
  return vgh;
}


void
TALER_MERCHANT_config_get_cancel (struct TALER_MERCHANT_ConfigGetHandle *vgh)
{
  if (NULL != vgh->job)
  {
    GNUNET_CURL_job_cancel (vgh->job);
    vgh->job = NULL;
  }
  GNUNET_free (vgh->url);
  GNUNET_free (vgh);
}


/* end of merchant_api_config_get.c */
