/*
  This file is part of TALER
  (C) 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 3, 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 General Public License for more details.

  You should have received a copy of the GNU General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file src/util/template_parse.c
 * @brief shared logic for template contract parsing
 * @author Bohdan Potuzhnyi
 */
#include "platform.h"
#include <gnunet/gnunet_common.h>
#include <gnunet/gnunet_json_lib.h>
#include <jansson.h>
#include <string.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_util.h>
#include "taler/taler_merchant_util.h"
#include <regex.h>


enum TALER_MERCHANT_TemplateType
TALER_MERCHANT_template_type_from_contract (const json_t *template_contract)
{
  const json_t *type_val;

  if (NULL == template_contract)
    return TALER_MERCHANT_TEMPLATE_TYPE_INVALID;

  type_val = json_object_get (template_contract,
                              "template_type");

  if (NULL == type_val)
  {
    /* missing => fixed order */
    return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER;
  }
  if (! json_is_string (type_val))
  {
    GNUNET_break_op (0);
    return TALER_MERCHANT_TEMPLATE_TYPE_INVALID;
  }

  return TALER_MERCHANT_template_type_from_string (
    json_string_value (type_val));
}


enum TALER_MERCHANT_TemplateType
TALER_MERCHANT_template_type_from_string (const char *template_type)
{
  if (NULL == template_type)
    return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER;
  if (0 == strcmp (template_type,
                   "fixed-order"))
    return TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER;
  if (0 == strcmp (template_type,
                   "inventory-cart"))
    return TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART;
  if (0 == strcmp (template_type,
                   "paivana"))
    return TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA;
  return TALER_MERCHANT_TEMPLATE_TYPE_INVALID;
}


const char *
TALER_MERCHANT_template_type_to_string (
  enum TALER_MERCHANT_TemplateType template_type)
{
  switch (template_type)
  {
  case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER:
    return "fixed-order";
  case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART:
    return "inventory-cart";
  case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA:
    return "paivana";
  case TALER_MERCHANT_TEMPLATE_TYPE_INVALID:
    break;
  }
  return NULL;
}


/**
 * Parse inventory-specific fields from a template contract.
 *
 * @param template_contract json
 * @param[out] out where to write parsed fields
 * @param[out] error_name error description
 * @return #GNUNET_OK on success, #GNUNET_SYSERR on parse/validation failure
 */
static enum GNUNET_GenericReturnValue
parse_template_inventory (const json_t *template_contract,
                          struct TALER_MERCHANT_TemplateContract *out,
                          const char **error_name)
{
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_bool (
        "selected_all",
        &out->details.inventory.selected_all),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_array_const (
        "selected_categories",
        &out->details.inventory.selected_categories)
      ,
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_array_const (
        "selected_products",
        &out->details.inventory.selected_products),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_bool (
        "choose_one",
        &out->details.inventory.choose_one),
      NULL),
    GNUNET_JSON_spec_end ()
  };
  const char *en;

  if (GNUNET_OK !=
      GNUNET_JSON_parse ((json_t *) template_contract,
                         spec,
                         &en,
                         NULL))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Invalid inventory template_contract for field %s\n",
                en);
    if (NULL != error_name)
      *error_name = en;
    return GNUNET_SYSERR;
  }

  if (NULL != out->details.inventory.selected_categories)
  {
    const json_t *entry;
    size_t idx;

    json_array_foreach ((json_t *) out->details.inventory.selected_categories,
                        idx,
                        entry)
    {
      if ( (! json_is_integer (entry)) ||
           (0 > json_integer_value (entry)) )
      {
        GNUNET_break_op (0);
        if (NULL != error_name)
          *error_name = "selected_categories";
        return GNUNET_SYSERR;
      }
    }
  }

  if (NULL != out->details.inventory.selected_products)
  {
    const json_t *entry;
    size_t idx;

    json_array_foreach ((json_t *) out->details.inventory.selected_products,
                        idx,
                        entry)
    {
      if (! json_is_string (entry))
      {
        GNUNET_break_op (0);
        if (NULL != error_name)
          *error_name = "selected_products";
        return GNUNET_SYSERR;
      }
    }
  }
  return GNUNET_OK;
}


/**
 * Parse paivana-specific fields from a template contract.
 *
 * @param template_contract json
 * @param[out] out where to write parsed fields
 * @param[out] error_name error description
 * @return #GNUNET_OK on success, #GNUNET_SYSERR on parse/validation failure
 */
static enum GNUNET_GenericReturnValue
parse_template_paivana (const json_t *template_contract,
                        struct TALER_MERCHANT_TemplateContract *out,
                        const char **error_name)
{
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("website_regex",
                               &out->details.paivana.website_regex),
      NULL),
    TALER_MERCHANT_spec_order_choices ("choices",
                                       &out->details.paivana.choices,
                                       &out->details.paivana.choices_len),
    GNUNET_JSON_spec_end ()
  };
  const char *en;

  if (GNUNET_OK !=
      GNUNET_JSON_parse ((json_t *) template_contract,
                         spec,
                         &en,
                         NULL))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Invalid paivana template_contract for field %s\n",
                en);
    if (NULL != error_name)
      *error_name = en;
    return GNUNET_SYSERR;
  }
  if (NULL != out->details.paivana.website_regex)
  {
    regex_t ex;

    if (0 != regcomp (&ex,
                      out->details.paivana.website_regex,
                      REG_NOSUB | REG_EXTENDED))
    {
      GNUNET_break_op (0);
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Invalid paivana website_regex given\n");
      if (NULL != error_name)
        *error_name = "Invalid website_regex given";
      return GNUNET_SYSERR;
    }
    regfree (&ex);
  }
  return GNUNET_OK;
}


/**
 * Check that the (optional) @e min_amount and @e max_amount of @a tc are
 * usable: they must be denominated in the same currency, which must be
 * the currency of the template (which then must be given, as otherwise we
 * could not tell in which currency to enforce the limits), and they must
 * not exclude each other.
 *
 * @param tc template contract to check
 * @param[out] error_name error description
 * @return #GNUNET_OK on success, #GNUNET_SYSERR on validation failure
 */
static enum GNUNET_GenericReturnValue
check_amount_limits (const struct TALER_MERCHANT_TemplateContract *tc,
                     const char **error_name)
{
  if (tc->no_min_amount &&
      tc->no_max_amount)
    return GNUNET_OK;
  if ( (! tc->no_min_amount) &&
       (! tc->no_max_amount) )
  {
    /* Must be checked before the two are compared, as comparing
       amounts of different currencies fails an assertion. Note that
       we deliberately do not rely on both having been checked against
       the currency of the template here. */
    if (GNUNET_YES !=
        TALER_amount_cmp_currency (&tc->min_amount,
                                   &tc->max_amount))
    {
      GNUNET_break_op (0);
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "min_amount and max_amount must use the same currency\n");
      if (NULL != error_name)
        *error_name = "max_amount";
      return GNUNET_SYSERR;
    }
    if (0 < TALER_amount_cmp (&tc->min_amount,
                              &tc->max_amount))
    {
      GNUNET_break_op (0);
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "min_amount exceeds max_amount\n");
      if (NULL != error_name)
        *error_name = "min_amount";
      return GNUNET_SYSERR;
    }
  }
  if (NULL == tc->currency)
  {
    GNUNET_break_op (0);
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Template with min_amount or max_amount must specify a currency\n");
    if (NULL != error_name)
      *error_name = "currency";
    return GNUNET_SYSERR;
  }
  if ( (! tc->no_min_amount) &&
       (0 != strcasecmp (tc->currency,
                         tc->min_amount.currency)) )
  {
    GNUNET_break_op (0);
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "min_amount must use the currency of the template (%s)\n",
                tc->currency);
    if (NULL != error_name)
      *error_name = "min_amount";
    return GNUNET_SYSERR;
  }
  if ( (! tc->no_max_amount) &&
       (0 != strcasecmp (tc->currency,
                         tc->max_amount.currency)) )
  {
    GNUNET_break_op (0);
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "max_amount must use the currency of the template (%s)\n",
                tc->currency);
    if (NULL != error_name)
      *error_name = "max_amount";
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


enum GNUNET_GenericReturnValue
TALER_MERCHANT_template_contract_parse (
  const json_t *template_contract,
  struct TALER_MERCHANT_TemplateContract *out,
  const char **error_name)
{
  const char *template_type_str = NULL;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("template_type",
                               &template_type_str),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("summary",
                               &out->summary),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_string ("currency",
                               &out->currency),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      TALER_JSON_spec_amount_any ("amount",
                                  &out->amount),
      &out->no_amount),
    GNUNET_JSON_spec_mark_optional (
      TALER_JSON_spec_amount_any ("min_amount",
                                  &out->min_amount),
      &out->no_min_amount),
    GNUNET_JSON_spec_mark_optional (
      TALER_JSON_spec_amount_any ("max_amount",
                                  &out->max_amount),
      &out->no_max_amount),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_uint32 ("minimum_age",
                               &out->minimum_age),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("pay_duration",
                                      &out->pay_duration),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("max_pickup_duration",
                                      &out->max_pickup_duration),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_bool ("request_tip",
                             &out->request_tip),
      NULL),
    GNUNET_JSON_spec_end ()
  };
  const char *en;

  if (NULL == template_contract)
  {
    if (NULL != error_name)
      *error_name = "template_contract is NULL";
    return GNUNET_SYSERR;
  }
  out->max_pickup_duration = GNUNET_TIME_UNIT_FOREVER_REL;
  out->no_min_amount = true;
  out->no_max_amount = true;
  if (GNUNET_OK !=
      GNUNET_JSON_parse ((json_t *) template_contract,
                         spec,
                         &en,
                         NULL))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Invalid input for field %s\n",
                en);
    if (NULL != error_name)
      *error_name = en;
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      check_amount_limits (out,
                           error_name))
    return GNUNET_SYSERR;

  out->type = TALER_MERCHANT_template_type_from_string (template_type_str);
  if (TALER_MERCHANT_TEMPLATE_TYPE_INVALID == out->type)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Invalid template_type used '%s'\n",
                template_type_str);
    if (NULL != error_name)
      *error_name = "Invalid template_type used";
    return GNUNET_SYSERR;
  }

  /* Parse additional fields for each specific type */
  switch (out->type)
  {
  case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER:
    return GNUNET_OK;
  case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART:
    return parse_template_inventory (template_contract,
                                     out,
                                     error_name);
  case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA:
    return parse_template_paivana (template_contract,
                                   out,
                                   error_name);
  case TALER_MERCHANT_TEMPLATE_TYPE_INVALID:
    break;
  }

  /* I think we are never supposed to reach it */
  GNUNET_break_op (0);
  if (NULL != error_name)
    *error_name = "template_type";
  return GNUNET_SYSERR;
}


void
TALER_MERCHANT_template_contract_free (
  struct TALER_MERCHANT_TemplateContract *tc)
{
  switch (tc->type)
  {
  case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER:
    return;
  case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART:
    return;
  case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA:
    for (unsigned int i = 0; i<tc->details.paivana.choices_len; i++)
      TALER_MERCHANT_order_choice_free (&tc->details.paivana.choices[i]);
    GNUNET_array_grow (tc->details.paivana.choices,
                       tc->details.paivana.choices_len,
                       0);
    return;
  case TALER_MERCHANT_TEMPLATE_TYPE_INVALID:
    return;
  }
}


bool
TALER_MERCHANT_template_contract_valid (const json_t *template_contract)
{
  struct TALER_MERCHANT_TemplateContract tmp;
  bool ret;

  memset (&tmp,
          0,
          sizeof (tmp));
  ret = (GNUNET_OK ==
         TALER_MERCHANT_template_contract_parse (template_contract,
                                                 &tmp,
                                                 NULL));
  TALER_MERCHANT_template_contract_free (&tmp);
  return ret;
}
