/*
  This file is part of TALER
  (C) 2020 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-patch-orders-ID-forget.c
 * @brief implementing PATCH /orders/$ORDER_ID/forget request handling
 * @author Jonathan Buchanan
 */
#include "platform.h"
#include "taler-merchant-httpd_private-patch-orders-ID-forget.h"
#include <taler/taler_json_lib.h>


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


/**
 * Forget part of the contract terms.
 *
 * @param cls pointer to the result of the forget operation.
 * @param object_id name of the object to forget.
 * @param parent parent of the object at @e object_id.
 */
static void
forget (void *cls,
        const char *object_id,
        json_t *parent)
{
  int *res = cls;
  int ret;

  ret = TALER_JSON_contract_part_forget (parent,
                                         object_id);
  if (GNUNET_SYSERR == ret)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Matching path `%s' not forgettable!\n",
                object_id);
    *res = GNUNET_SYSERR;
  }
  if (GNUNET_NO == ret)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Matching path `%s' already forgotten!\n",
                object_id);
  }
  else
  {
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Forgot `%s'\n",
                object_id);
    if (GNUNET_NO == *res)
      *res = GNUNET_OK;
  }
}


/**
 * Forget fields of an order's contract terms.
 *
 * @param rh context of the handler
 * @param connection the MHD connection to handle
 * @param[in,out] hc context with further information about the request
 * @return MHD result code
 */
MHD_RESULT
TMH_private_patch_orders_ID_forget (const struct TMH_RequestHandler *rh,
                                    struct MHD_Connection *connection,
                                    struct TMH_HandlerContext *hc)
{
  const char *order_id = hc->infix;
  enum GNUNET_DB_QueryStatus qs;
  uint64_t order_serial;

  for (unsigned int i = 0; i<MAX_RETRIES; i++)
  {
    const json_t *fields;
    json_t *contract_terms;
    bool changed = false;

    if (GNUNET_OK !=
        TMH_db->start (TMH_db->cls,
                       "forget order"))
    {
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_START_FAILED,
                                         NULL);
    }
    qs = TMH_db->lookup_contract_terms (TMH_db->cls,
                                        hc->instance->settings.id,
                                        order_id,
                                        &contract_terms,
                                        &order_serial,
                                        NULL);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
      TMH_db->rollback (TMH_db->cls);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
                                         "contract terms");
    case GNUNET_DB_STATUS_SOFT_ERROR:
      TMH_db->rollback (TMH_db->cls);
      continue;
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      TMH_db->rollback (TMH_db->cls);
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_NOT_FOUND,
                                         TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
                                         order_id);
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      GNUNET_assert (NULL != contract_terms);
      break;
    }

    {
      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_array_const ("fields",
                                      &fields),
        GNUNET_JSON_spec_end ()
      };
      enum GNUNET_GenericReturnValue res;

      res = TALER_MHD_parse_json_data (connection,
                                       hc->request_body,
                                       spec);
      if (GNUNET_OK != res)
      {
        TMH_db->rollback (TMH_db->cls);
        json_decref (contract_terms);
        return (GNUNET_NO == res)
               ? MHD_YES
               : MHD_NO;
      }
    }
    {
      size_t index;
      json_t *value;

      json_array_foreach (fields, index, value) {
        int forget_status = GNUNET_NO;
        int expand_status;

        if (! (json_is_string (value)))
        {
          TMH_db->rollback (TMH_db->cls);
          json_decref (contract_terms);
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_BAD_REQUEST,
                                             TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT,
                                             "field is not a string");
        }
        expand_status = TALER_JSON_expand_path (contract_terms,
                                                json_string_value (value),
                                                &forget,
                                                &forget_status);
        if (GNUNET_SYSERR == forget_status)
        {
          /* We tried to forget a field that isn't forgettable */
          TMH_db->rollback (TMH_db->cls);
          json_decref (contract_terms);
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_CONFLICT,
                                             TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE,
                                             json_string_value (value));
        }
        if (GNUNET_OK == forget_status)
          changed = true;
        if (GNUNET_SYSERR == expand_status)
        {
          /* One of the paths was malformed and couldn't be expanded */
          TMH_db->rollback (TMH_db->cls);
          json_decref (contract_terms);
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_BAD_REQUEST,
                                             TALER_EC_MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT,
                                             json_string_value (value));
        }
      }
    }

    if (! changed)
    {
      TMH_db->rollback (TMH_db->cls);
      json_decref (contract_terms);
      return TALER_MHD_reply_static (connection,
                                     MHD_HTTP_NO_CONTENT,
                                     NULL,
                                     NULL,
                                     0);
    }
    qs = TMH_db->update_contract_terms (TMH_db->cls,
                                        hc->instance->settings.id,
                                        order_id,
                                        contract_terms);
    json_decref (contract_terms);
    if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
    {
      TMH_db->rollback (TMH_db->cls);
      if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
        break;
    }
    else
    {
      qs = TMH_db->commit (TMH_db->cls);
      if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
        break;
    }
  }
  if (0 > qs)
  {
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
                                       TALER_EC_GENERIC_DB_COMMIT_FAILED,
                                       NULL);
  }

  return TALER_MHD_reply_static (connection,
                                 MHD_HTTP_OK,
                                 NULL,
                                 NULL,
                                 0);
}
