/*
  This file is part of TALER
  (C) 2022-2025 Taler Systems SA

  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU Affero 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 taler-merchant-httpd_get-templates-TEMPLATE_ID.c
 * @brief implement GET /templates/$ID
 * @author Priscilla HUANG
 */
#include "taler/platform.h"
#include "taler-merchant-httpd_get-templates-TEMPLATE_ID.h"
#include "taler-merchant-httpd_helper.h"
#include <taler/taler_json_lib.h>


/**
 * Context for building inventory template payloads.
 */
struct InventoryPayloadContext
{
  /**
   * Selected category IDs (as JSON array).
   */
  const json_t *selected_categories;

  /**
   * Selected product IDs (as JSON array) from contract_template.
   */
  const json_t *selected_products;

  /**
   * Whether all products are selected.
   */
  bool selected_all;

  /**
   * JSON array of products to build.
   */
  json_t *products;

  /**
   * JSON array of categories to build.
   */
  json_t *category_payload;

  /**
   * JSON array of units to build.
   */
  json_t *unit_payload;

  /**
   * Set of categories referenced by the products.
   */
  struct TMH_CategorySet category_set;

  /**
   * Set of unit identifiers referenced by the products.
   */
  struct TMH_UnitSet unit_set;
};


/**
 * Release resources associated with an inventory payload context.
 *
 * @param ipc inventory payload context
 */
static void
inventory_payload_cleanup (struct InventoryPayloadContext *ipc)
{
  if (NULL != ipc->products)
    json_decref (ipc->products);
  if (NULL != ipc->category_payload)
    json_decref (ipc->category_payload);
  if (NULL != ipc->unit_payload)
    json_decref (ipc->unit_payload);
  GNUNET_free (ipc->category_set.ids);
  TMH_unit_set_clear (&ipc->unit_set);
  ipc->products = NULL;
  ipc->category_payload = NULL;
  ipc->unit_payload = NULL;
  ipc->category_set.ids = NULL;
  ipc->category_set.len = 0;
}


/**
 * Add inventory product to JSON payload.
 *
 * @param cls inventory payload context
 * @param product_id product identifier
 * @param pd product details
 * @param num_categories number of categories
 * @param categories category IDs
 */
static void
add_inventory_product (
  void *cls,
  const char *product_id,
  const struct TALER_MERCHANTDB_InventoryProductDetails *pd,
  size_t num_categories,
  const uint64_t *categories)
{
  struct InventoryPayloadContext *ipc = cls;
  json_t *jcategories;
  json_t *product;
  char remaining_stock_buf[64];

  jcategories = json_array ();
  GNUNET_assert (NULL != jcategories);
  for (size_t i = 0; i < num_categories; i++)
  {
    /* Adding per product category */
    TMH_category_set_add (&ipc->category_set,
                          categories[i]);
    GNUNET_assert (0 ==
                   json_array_append_new (jcategories,
                                          json_integer (categories[i])));
  }
  GNUNET_assert (0 < pd->price_array_length);
  TALER_MERCHANT_vk_format_fractional_string (
    TALER_MERCHANT_VK_STOCK,
    pd->remaining_stock,
    pd->remaining_stock_frac,
    sizeof (remaining_stock_buf),
    remaining_stock_buf);
  product = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_string ("product_id",
                             product_id),
    GNUNET_JSON_pack_string ("product_name",
                             pd->product_name),
    GNUNET_JSON_pack_string ("description",
                             pd->description),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_object_incref ("description_i18n",
                                      pd->description_i18n)),
    GNUNET_JSON_pack_string ("unit",
                             pd->unit),
    TALER_JSON_pack_amount_array ("unit_prices",
                                  pd->price_array_length,
                                  pd->price_array),
    GNUNET_JSON_pack_bool ("unit_allow_fraction",
                           pd->allow_fractional_quantity),
    GNUNET_JSON_pack_uint64 ("unit_precision_level",
                             pd->fractional_precision_level),
    GNUNET_JSON_pack_string ("remaining_stock",
                             remaining_stock_buf),
    GNUNET_JSON_pack_array_steal ("categories",
                                  jcategories),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_array_incref ("taxes",
                                     pd->taxes)),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_string ("image_hash",
                               pd->image_hash)));

  /* Adding per product unit */
  TMH_unit_set_add (&ipc->unit_set,
                    pd->unit);

  GNUNET_assert (0 ==
                 json_array_append_new (ipc->products,
                                        product));
}


/**
 * Add an inventory category to the payload if referenced.
 *
 * @param cls category payload context
 * @param category_id category identifier
 * @param category_name category name
 * @param category_name_i18n translated names
 * @param product_count number of products (unused)
 */
static void
add_inventory_category (void *cls,
                        uint64_t category_id,
                        const char *category_name,
                        const json_t *category_name_i18n,
                        uint64_t product_count)
{
  struct InventoryPayloadContext *ipc = cls;
  json_t *category;

  (void) product_count;
  category = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_uint64 ("category_id",
                             category_id),
    GNUNET_JSON_pack_string ("category_name",
                             category_name),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_object_incref ("category_name_i18n",
                                      (json_t *) category_name_i18n)));
  GNUNET_assert (0 ==
                 json_array_append_new (ipc->category_payload,
                                        category));
}


/**
 * Add an inventory unit to the payload if referenced and non-builtin.
 *
 * @param cls unit payload context
 * @param unit_serial unit identifier
 * @param ud unit details
 */
static void
add_inventory_unit (void *cls,
                    uint64_t unit_serial,
                    const struct TALER_MERCHANTDB_UnitDetails *ud)
{
  struct InventoryPayloadContext *ipc = cls;
  json_t *unit;

  (void) unit_serial;

  unit = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_string ("unit",
                             ud->unit),
    GNUNET_JSON_pack_string ("unit_name_long",
                             ud->unit_name_long),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_object_incref ("unit_name_long_i18n",
                                      ud->unit_name_long_i18n)),
    GNUNET_JSON_pack_string ("unit_name_short",
                             ud->unit_name_short),
    GNUNET_JSON_pack_allow_null (
      GNUNET_JSON_pack_object_incref ("unit_name_short_i18n",
                                      ud->unit_name_short_i18n)),
    GNUNET_JSON_pack_bool ("unit_allow_fraction",
                           ud->unit_allow_fraction),
    GNUNET_JSON_pack_uint64 ("unit_precision_level",
                             ud->unit_precision_level));
  GNUNET_assert (0 ==
                 json_array_append_new (ipc->unit_payload,
                                        unit));
}


/**
 * Build wallet-facing payload for inventory templates.
 *
 * @param connection HTTP connection
 * @param mi merchant instance
 * @param tp template details
 * @return MHD result
 */
static MHD_RESULT
handle_get_templates_inventory (
  struct MHD_Connection *connection,
  const struct TMH_MerchantInstance *mi,
  const struct TALER_MERCHANTDB_TemplateDetails *tp)
{
  struct InventoryPayloadContext ipc;
  const char **product_ids = NULL;
  uint64_t *category_ids = NULL;
  size_t num_product_ids = 0;
  size_t num_category_ids = 0;
  json_t *inventory_payload;
  json_t *template_contract;

  memset (&ipc,
          0,
          sizeof (ipc));
  ipc.products = json_array ();
  {
    struct GNUNET_JSON_Specification spec[] = {
      GNUNET_JSON_spec_mark_optional (
        GNUNET_JSON_spec_array_const ("selected_categories",
                                      &ipc.selected_categories),
        NULL),
      GNUNET_JSON_spec_mark_optional (
        GNUNET_JSON_spec_array_const ("selected_products",
                                      &ipc.selected_products),
        NULL),
      GNUNET_JSON_spec_mark_optional (
        GNUNET_JSON_spec_bool ("selected_all",
                               &ipc.selected_all),
        NULL),
      GNUNET_JSON_spec_end ()
    };
    const char *err_name;
    unsigned int err_line;

    if (GNUNET_OK !=
        GNUNET_JSON_parse (tp->template_contract,
                           spec,
                           &err_name,
                           &err_line))
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Invalid inventory template_contract for field %s\n",
                  err_name);
      inventory_payload_cleanup (&ipc);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
        err_name);
    }
  }

  if (! ipc.selected_all)
  {
    if (NULL != ipc.selected_products)
    {
      size_t max_ids;

      max_ids = json_array_size (ipc.selected_products);
      if (0 < max_ids)
        product_ids = GNUNET_new_array (max_ids,
                                        const char *);
      for (size_t i = 0; i < max_ids; i++)
      {
        const json_t *entry = json_array_get (ipc.selected_products,
                                              i);

        if (json_is_string (entry))
          product_ids[num_product_ids++] = json_string_value (entry);
        else
          GNUNET_break (0);
      }
    }
    if (NULL != ipc.selected_categories)
    {
      size_t max_categories;

      max_categories = json_array_size (ipc.selected_categories);
      if (0 < max_categories)
        category_ids = GNUNET_new_array (max_categories,
                                         uint64_t);
      for (size_t i = 0; i < max_categories; i++)
      {
        const json_t *entry = json_array_get (ipc.selected_categories,
                                              i);

        if (json_is_integer (entry) &&
            (0 < json_integer_value (entry)))
          category_ids[num_category_ids++]
            = (uint64_t) json_integer_value (entry);
        else
          GNUNET_break (0);
      }
    }
  }

  if (ipc.selected_all)
  {
    enum GNUNET_DB_QueryStatus qs;

    qs = TMH_db->lookup_inventory_products (TMH_db->cls,
                                            mi->settings.id,
                                            &add_inventory_product,
                                            &ipc);
    if (0 > qs)
    {
      GNUNET_break (0);
      inventory_payload_cleanup (&ipc);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "lookup_inventory_products");
    }
  }
  else if ( (0 < num_product_ids) ||
            (0 < num_category_ids) )
  {
    enum GNUNET_DB_QueryStatus qs;

    qs = TMH_db->lookup_inventory_products_filtered (
      TMH_db->cls,
      mi->settings.id,
      product_ids,
      num_product_ids,
      category_ids,
      num_category_ids,
      &add_inventory_product,
      &ipc);
    GNUNET_free (product_ids);
    GNUNET_free (category_ids);
    if (0 > qs)
    {
      GNUNET_break (0);
      inventory_payload_cleanup (&ipc);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "lookup_inventory_products_filtered");
    }
  }

  ipc.category_payload = json_array ();
  GNUNET_assert (NULL != ipc.category_payload);
  if (0 < ipc.category_set.len)
  {
    enum GNUNET_DB_QueryStatus qs;

    qs = TMH_db->lookup_categories_by_ids (
      TMH_db->cls,
      mi->settings.id,
      ipc.category_set.ids,
      ipc.category_set.len,
      &add_inventory_category,
      &ipc);
    if (0 > qs)
    {
      GNUNET_break (0);
      inventory_payload_cleanup (&ipc);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "lookup_categories_by_ids");
    }
  }

  ipc.unit_payload = json_array ();
  GNUNET_assert (NULL != ipc.unit_payload);
  if (0 < ipc.unit_set.len)
  {
    enum GNUNET_DB_QueryStatus qs;

    qs = TMH_db->lookup_custom_units_by_names (
      TMH_db->cls,
      mi->settings.id,
      (const char *const *) ipc.unit_set.units,
      ipc.unit_set.len,
      &add_inventory_unit,
      &ipc);
    if (0 > qs)
    {
      GNUNET_break (0);
      inventory_payload_cleanup (&ipc);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_FETCH_FAILED,
        "lookup_custom_units_by_names");
    }
  }

  inventory_payload = GNUNET_JSON_PACK (
    GNUNET_JSON_pack_array_steal ("products",
                                  ipc.products),
    GNUNET_JSON_pack_array_steal ("categories",
                                  ipc.category_payload),
    GNUNET_JSON_pack_array_steal ("units",
                                  ipc.unit_payload));
  ipc.products = NULL;
  ipc.category_payload = NULL;
  ipc.unit_payload = NULL;

  template_contract = json_deep_copy (tp->template_contract);
  GNUNET_assert (NULL != template_contract);
  /* remove internal fields */
  (void) json_object_del (template_contract,
                          "selected_categories");
  (void) json_object_del (template_contract,
                          "selected_products");
  (void) json_object_del (template_contract,
                          "selected_all");
  /* add inventory data */
  GNUNET_assert (0 ==
                 json_object_set_new (template_contract,
                                      "inventory_payload",
                                      inventory_payload));
  {
    MHD_RESULT ret;

    ret = TALER_MHD_REPLY_JSON_PACK (
      connection,
      MHD_HTTP_OK,
      GNUNET_JSON_pack_allow_null (
        GNUNET_JSON_pack_object_incref ("editable_defaults",
                                        tp->editable_defaults)),
      GNUNET_JSON_pack_object_steal ("template_contract",
                                     template_contract));
    inventory_payload_cleanup (&ipc);
    return ret;
  }
}


MHD_RESULT
TMH_get_templates_ID (
  const struct TMH_RequestHandler *rh,
  struct MHD_Connection *connection,
  struct TMH_HandlerContext *hc)
{
  struct TMH_MerchantInstance *mi = hc->instance;
  struct TALER_MERCHANTDB_TemplateDetails tp = { 0 };
  enum GNUNET_DB_QueryStatus qs;

  GNUNET_assert (NULL != mi);
  qs = TMH_db->lookup_template (TMH_db->cls,
                                mi->settings.id,
                                hc->infix,
                                &tp);
  if (0 > qs)
  {
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_DB_FETCH_FAILED,
      "lookup_template");
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_NOT_FOUND,
      TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN,
      hc->infix);
  }
  {
    MHD_RESULT ret;

    switch (TALER_MERCHANT_template_type_from_contract (tp.template_contract))
    {
    case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART:
      ret = handle_get_templates_inventory (connection,
                                            mi,
                                            &tp);
      TALER_MERCHANTDB_template_details_free (&tp);
      return ret;
    case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER:
    case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA:
      ret = TALER_MHD_REPLY_JSON_PACK (
        connection,
        MHD_HTTP_OK,
        GNUNET_JSON_pack_allow_null (
          GNUNET_JSON_pack_object_incref ("editable_defaults",
                                          tp.editable_defaults)),
        GNUNET_JSON_pack_object_incref ("template_contract",
                                        tp.template_contract));
      TALER_MERCHANTDB_template_details_free (&tp);
      return ret;
    case TALER_MERCHANT_TEMPLATE_TYPE_INVALID:
      break;
    }
    GNUNET_break_op (0);
    ret = TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
      "template_type");
    TALER_MERCHANTDB_template_details_free (&tp);
    return ret;
  }
}


/* end of taler-merchant-httpd_get-templates-TEMPLATE_ID.c */
