/*
  This file is part of TALER
  (C) 2023, 2024 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_private-post-token-families.c
 * @brief implementing POST /tokenfamilies request handling
 * @author Christian Blättler
 */
#include "platform.h"
#include "taler-merchant-httpd_private-post-token-families.h"
#include "taler-merchant-httpd_helper.h"
#include <gnunet/gnunet_time_lib.h>
#include <taler/taler_json_lib.h>


/**
 * How often do we retry the simple INSERT database transaction?
 */
#define MAX_RETRIES 3


/**
 * Check if the two token families are identical.
 *
 * @param tf1 token family to compare
 * @param tf2 other token family to compare
 * @return true if they are 'equal', false if not
 */
static bool
token_families_equal (const struct TALER_MERCHANTDB_TokenFamilyDetails *tf1,
                      const struct TALER_MERCHANTDB_TokenFamilyDetails *tf2)
{
  /* Note: we're not comparing 'cipher', as that is selected
     in the database to some default value and we currently
     do not allow the SPA to change it. As a result, it should
     always be "NULL" in tf1 and the DB-default in tf2. */
  return ( (0 == strcmp (tf1->slug,
                         tf2->slug)) &&
           (0 == strcmp (tf1->name,
                         tf2->name)) &&
           (0 == strcmp (tf1->description,
                         tf2->description)) &&
           (1 == json_equal (tf1->description_i18n,
                             tf2->description_i18n)) &&
           ( (tf1->extra_data == tf2->extra_data) ||
             (1 == json_equal (tf1->extra_data,
                               tf2->extra_data)) ) &&
           (GNUNET_TIME_timestamp_cmp (tf1->valid_after,
                                       ==,
                                       tf2->valid_after)) &&
           (GNUNET_TIME_timestamp_cmp (tf1->valid_before,
                                       ==,
                                       tf2->valid_before)) &&
           (GNUNET_TIME_relative_cmp (tf1->duration,
                                      ==,
                                      tf2->duration)) &&
           (GNUNET_TIME_relative_cmp (tf1->validity_granularity,
                                      ==,
                                      tf2->validity_granularity)) &&
           (GNUNET_TIME_relative_cmp (tf1->start_offset,
                                      ==,
                                      tf2->start_offset)) &&
           (tf1->kind == tf2->kind) );
}


MHD_RESULT
TMH_private_post_token_families (const struct TMH_RequestHandler *rh,
                                 struct MHD_Connection *connection,
                                 struct TMH_HandlerContext *hc)
{
  struct TMH_MerchantInstance *mi = hc->instance;
  struct TALER_MERCHANTDB_TokenFamilyDetails details = { 0 };
  const char *kind = NULL;
  bool no_valid_after = false;
  enum GNUNET_DB_QueryStatus qs;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_string ("slug",
                             (const char **) &details.slug),
    GNUNET_JSON_spec_string ("name",
                             (const char **) &details.name),
    GNUNET_JSON_spec_string ("description",
                             (const char **) &details.description),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_json ("description_i18n",
                             &details.description_i18n),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_json ("extra_data",
                             &details.extra_data),
      NULL),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_timestamp ("valid_after",
                                  &details.valid_after),
      &no_valid_after),
    GNUNET_JSON_spec_timestamp ("valid_before",
                                &details.valid_before),
    GNUNET_JSON_spec_relative_time ("duration",
                                    &details.duration),
    GNUNET_JSON_spec_relative_time ("validity_granularity",
                                    &details.validity_granularity),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_relative_time ("start_offset",
                                      &details.start_offset),
      NULL),
    GNUNET_JSON_spec_string ("kind",
                             &kind),
    GNUNET_JSON_spec_end ()
  };
  struct GNUNET_TIME_Timestamp now
    = GNUNET_TIME_timestamp_get ();

  GNUNET_assert (NULL != mi);
  {
    enum GNUNET_GenericReturnValue res;

    res = TALER_MHD_parse_json_data (connection,
                                     hc->request_body,
                                     spec);
    if (GNUNET_OK != res)
    {
      GNUNET_break_op (0);
      return (GNUNET_NO == res)
             ? MHD_YES
             : MHD_NO;
    }
  }
  if (no_valid_after)
    details.valid_after = now;

  /* Ensure that valid_after is before valid_before */
  if (GNUNET_TIME_timestamp_cmp (details.valid_after,
                                 >=,
                                 details.valid_before))
  {
    GNUNET_break (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "valid_after >= valid_before");
  }

  /* Ensure that duration exceeds rounding plus start_offset */
  if (GNUNET_TIME_relative_cmp (details.duration,
                                <,
                                GNUNET_TIME_relative_add (details.
                                                          validity_granularity,
                                                          details.start_offset))
      )
  {
    GNUNET_break (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "duration below validity_granularity plus start_offset");
  }

  if (0 ==
      strcmp (kind,
              "discount"))
    details.kind = TALER_MERCHANTDB_TFK_Discount;
  else if (0 ==
           strcmp (kind,
                   "subscription"))
    details.kind = TALER_MERCHANTDB_TFK_Subscription;
  else
  {
    GNUNET_break (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "kind");
  }

  if (NULL == details.description_i18n)
    details.description_i18n = json_object ();

  if (! TALER_JSON_check_i18n (details.description_i18n))
  {
    GNUNET_break_op (0);
    GNUNET_JSON_parse_free (spec);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "description_i18n");
  }

  if (GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_YEARS,
                                !=,
                                details.validity_granularity) &&
      GNUNET_TIME_relative_cmp (GNUNET_TIME_relative_multiply (
                                  GNUNET_TIME_UNIT_DAYS,
                                  90),
                                !=,
                                details.validity_granularity) &&
      GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MONTHS,
                                !=,
                                details.validity_granularity) &&
      GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_WEEKS,
                                !=,
                                details.validity_granularity) &&
      GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_DAYS,
                                !=,
                                details.validity_granularity) &&
      GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_HOURS,
                                !=,
                                details.validity_granularity) &&
      GNUNET_TIME_relative_cmp (GNUNET_TIME_UNIT_MINUTES,
                                !=,
                                details.validity_granularity)
      )
  {
    GNUNET_break (0);
    GNUNET_JSON_parse_free (spec);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Received invalid validity_granularity value: %s\n",
                GNUNET_STRINGS_relative_time_to_string (details.
                                                        validity_granularity,
                                                        false));
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "validity_granularity");
  }

  /* finally, interact with DB until no serialization error */
  for (unsigned int i = 0; i<MAX_RETRIES; i++)
  {
    /* Test if a token family of this id is known */
    struct TALER_MERCHANTDB_TokenFamilyDetails existing;

    TMH_db->preflight (TMH_db->cls);
    if (GNUNET_OK !=
        TMH_db->start (TMH_db->cls,
                       "/post tokenfamilies"))
    {
      GNUNET_break (0);
      GNUNET_JSON_parse_free (spec);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_START_FAILED,
                                         NULL);
    }
    qs = TMH_db->insert_token_family (TMH_db->cls,
                                      mi->settings.id,
                                      details.slug,
                                      &details);
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "insert_token_family returned %d\n",
                (int) qs);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
      GNUNET_break (0);
      TMH_db->rollback (TMH_db->cls);
      GNUNET_JSON_parse_free (spec);
      return TALER_MHD_reply_with_error (
        connection,
        MHD_HTTP_INTERNAL_SERVER_ERROR,
        TALER_EC_GENERIC_DB_STORE_FAILED,
        "insert_token_family");
    case GNUNET_DB_STATUS_SOFT_ERROR:
      GNUNET_break (0);
      TMH_db->rollback (TMH_db->cls);
      break;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      qs = TMH_db->lookup_token_family (TMH_db->cls,
                                        mi->settings.id,
                                        details.slug,
                                        &existing);
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "lookup_token_family returned %d\n",
                  (int) qs);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        /* Clean up and fail hard */
        GNUNET_break (0);
        TMH_db->rollback (TMH_db->cls);
        GNUNET_JSON_parse_free (spec);
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_FETCH_FAILED,
          "lookup_token_family");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        TMH_db->rollback (TMH_db->cls);
        break;
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
          "lookup_token_family failed after insert_token_family failed");
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        {
          bool eq;

          eq = token_families_equal (&details,
                                     &existing);
          TALER_MERCHANTDB_token_family_details_free (&existing);
          TMH_db->rollback (TMH_db->cls);
          GNUNET_JSON_parse_free (spec);
          return eq
          ? TALER_MHD_reply_static (
            connection,
            MHD_HTTP_NO_CONTENT,
            NULL,
            NULL,
            0)
          : TALER_MHD_reply_with_error (
            connection,
            MHD_HTTP_CONFLICT,
            TALER_EC_MERCHANT_POST_TOKEN_FAMILY_CONFLICT,
            details.slug);
        }
      }
      break;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      qs = TMH_db->commit (TMH_db->cls);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        /* Clean up and fail hard */
        GNUNET_break (0);
        TMH_db->rollback (TMH_db->cls);
        GNUNET_JSON_parse_free (spec);
        return TALER_MHD_reply_with_error (
          connection,
          MHD_HTTP_INTERNAL_SERVER_ERROR,
          TALER_EC_GENERIC_DB_COMMIT_FAILED,
          "insert_token_family");
      case GNUNET_DB_STATUS_SOFT_ERROR:
        break;
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        break;
      }
      break;
    }
    if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
      TMH_db->rollback (TMH_db->cls);
    else
      break;
  } /* for(i... MAX_RETRIES) */

  GNUNET_JSON_parse_free (spec);
  if (qs < 0)
  {
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (
      connection,
      MHD_HTTP_INTERNAL_SERVER_ERROR,
      TALER_EC_GENERIC_DB_SOFT_FAILURE,
      NULL);
  }
  return TALER_MHD_reply_static (connection,
                                 MHD_HTTP_NO_CONTENT,
                                 NULL,
                                 NULL,
                                 0);
}


/* end of taler-merchant-httpd_private-post-token-families.c */
