/*
  This file is part of TALER
  (C) 2020-2022 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_post-orders-ID-refund.c
 * @brief handling of POST /orders/$ID/refund requests
 * @author Jonathan Buchanan
 */
#include "platform.h"
#include <taler/taler_dbevents.h>
#include <taler/taler_signatures.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_exchange_service.h>
#include "taler-merchant-httpd.h"
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_post-orders-ID-refund.h"


/**
 * Information we keep for each coin to be refunded.
 */
struct CoinRefund
{

  /**
   * Kept in a DLL.
   */
  struct CoinRefund *next;

  /**
   * Kept in a DLL.
   */
  struct CoinRefund *prev;

  /**
   * Request to connect to the target exchange.
   */
  struct TMH_EXCHANGES_KeysOperation *fo;

  /**
   * Handle for the refund operation with the exchange.
   */
  struct TALER_EXCHANGE_RefundHandle *rh;

  /**
   * Request this operation is part of.
   */
  struct PostRefundData *prd;

  /**
   * URL of the exchange for this @e coin_pub.
   */
  char *exchange_url;

  /**
   * Fully reply from the exchange, only possibly set if
   * we got a JSON reply and a non-#MHD_HTTP_OK error code
   */
  json_t *exchange_reply;

  /**
   * When did the merchant grant the refund. To be used to group events
   * in the wallet.
   */
  struct GNUNET_TIME_Timestamp execution_time;

  /**
   * Coin to refund.
   */
  struct TALER_CoinSpendPublicKeyP coin_pub;

  /**
   * Refund transaction ID to use.
   */
  uint64_t rtransaction_id;

  /**
   * Unique serial number identifying the refund.
   */
  uint64_t refund_serial;

  /**
   * Amount to refund.
   */
  struct TALER_Amount refund_amount;

  /**
   * Public key of the exchange affirming the refund.
   */
  struct TALER_ExchangePublicKeyP exchange_pub;

  /**
   * Signature of the exchange affirming the refund.
   */
  struct TALER_ExchangeSignatureP exchange_sig;

  /**
   * HTTP status from the exchange, #MHD_HTTP_OK if
   * @a exchange_pub and @a exchange_sig are valid.
   */
  unsigned int exchange_status;

  /**
   * HTTP error code from the exchange.
   */
  enum TALER_ErrorCode exchange_code;

};


/**
 * Context for the operation.
 */
struct PostRefundData
{

  /**
   * Hashed version of contract terms. All zeros if not provided.
   */
  struct TALER_PrivateContractHashP h_contract_terms;

  /**
   * DLL of (suspended) requests.
   */
  struct PostRefundData *next;

  /**
   * DLL of (suspended) requests.
   */
  struct PostRefundData *prev;

  /**
   * Refunds for this order. Head of DLL.
   */
  struct CoinRefund *cr_head;

  /**
   * Refunds for this order. Tail of DLL.
   */
  struct CoinRefund *cr_tail;

  /**
   * Context of the request.
   */
  struct TMH_HandlerContext *hc;

  /**
   * Entry in the #resume_timeout_heap for this check payment, if we are
   * suspended.
   */
  struct TMH_SuspendedConnection sc;

  /**
   * order ID for the payment
   */
  const char *order_id;

  /**
   * Where to get the contract
   */
  const char *contract_url;

  /**
   * fulfillment URL of the contract (valid as long as
   * @e contract_terms is valid).
   */
  const char *fulfillment_url;

  /**
   * session of the client
   */
  const char *session_id;

  /**
   * Contract terms of the payment we are checking. NULL when they
   * are not (yet) known.
   */
  json_t *contract_terms;

  /**
   * Total refunds granted for this payment. Only initialized
   * if @e refunded is set to true.
   */
  struct TALER_Amount refund_amount;

  /**
   * Did we suspend @a connection and are thus in
   * the #prd_head DLL (#GNUNET_YES). Set to
   * #GNUNET_NO if we are not suspended, and to
   * #GNUNET_SYSERR if we should close the connection
   * without a response due to shutdown.
   */
  enum GNUNET_GenericReturnValue suspended;

  /**
   * Return code: #TALER_EC_NONE if successful.
   */
  enum TALER_ErrorCode ec;

  /**
   * HTTP status to use for the reply, 0 if not yet known.
   */
  unsigned int http_status;

  /**
   * Set to true if we are dealing with an unclaimed order
   * (and thus @e h_contract_terms is not set, and certain
   * DB queries will not work).
   */
  bool unclaimed;

  /**
   * Set to true if this payment has been refunded and
   * @e refund_amount is initialized.
   */
  bool refunded;

  /**
   * Set to true if a refund is still available for the
   * wallet for this payment.
   */
  bool refund_available;

  /**
   * Set to true if the client requested HTML, otherwise
   * we generate JSON.
   */
  bool generate_html;

};


/**
 * Head of DLL of (suspended) requests.
 */
static struct PostRefundData *prd_head;

/**
 * Tail of DLL of (suspended) requests.
 */
static struct PostRefundData *prd_tail;


/**
 * Function called when we are done processing a refund request.
 * Frees memory associated with @a ctx.
 *
 * @param ctx a `struct PostRefundData`
 */
static void
refund_cleanup (void *ctx)
{
  struct PostRefundData *prd = ctx;
  struct CoinRefund *cr;

  while (NULL != (cr = prd->cr_head))
  {
    GNUNET_CONTAINER_DLL_remove (prd->cr_head,
                                 prd->cr_tail,
                                 cr);
    json_decref (cr->exchange_reply);
    GNUNET_free (cr->exchange_url);
    if (NULL != cr->fo)
    {
      TMH_EXCHANGES_keys4exchange_cancel (cr->fo);
      cr->fo = NULL;
    }
    if (NULL != cr->rh)
    {
      TALER_EXCHANGE_refund_cancel (cr->rh);
      cr->rh = NULL;
    }
    GNUNET_free (cr);
  }
  json_decref (prd->contract_terms);
  GNUNET_free (prd);
}


/**
 * Force resuming all suspended order lookups, needed during shutdown.
 */
void
TMH_force_wallet_refund_order_resume (void)
{
  struct PostRefundData *prd;

  while (NULL != (prd = prd_head))
  {
    GNUNET_CONTAINER_DLL_remove (prd_head,
                                 prd_tail,
                                 prd);
    GNUNET_assert (GNUNET_YES == prd->suspended);
    prd->suspended = GNUNET_SYSERR;
    MHD_resume_connection (prd->sc.con);
  }
}


/**
 * Check if @a prd has exchange requests still pending.
 *
 * @param prd state to check
 * @return true if activities are still pending
 */
static bool
exchange_operations_pending (struct PostRefundData *prd)
{
  for (struct CoinRefund *cr = prd->cr_head;
       NULL != cr;
       cr = cr->next)
  {
    if ( (NULL != cr->fo) ||
         (NULL != cr->rh) )
      return true;
  }
  return false;
}


/**
 * Check if @a prd is ready to be resumed, and if so, do it.
 *
 * @param prd refund request to be possibly ready
 */
static void
check_resume_prd (struct PostRefundData *prd)
{
  if ( (TALER_EC_NONE == prd->ec) &&
       exchange_operations_pending (prd) )
    return;
  GNUNET_CONTAINER_DLL_remove (prd_head,
                               prd_tail,
                               prd);
  GNUNET_assert (prd->suspended);
  prd->suspended = GNUNET_NO;
  MHD_resume_connection (prd->sc.con);
  TALER_MHD_daemon_trigger ();
}


/**
 * Notify applications waiting for a client to obtain
 * a refund.
 *
 * @param prd refund request with the change
 */
static void
notify_refund_obtained (struct PostRefundData *prd)
{
  struct TMH_OrderPayEventP refund_eh = {
    .header.size = htons (sizeof (refund_eh)),
    .header.type = htons (TALER_DBEVENT_MERCHANT_REFUND_OBTAINED),
    .merchant_pub = prd->hc->instance->merchant_pub
  };

  GNUNET_CRYPTO_hash (prd->order_id,
                      strlen (prd->order_id),
                      &refund_eh.h_order_id);
  TMH_db->event_notify (TMH_db->cls,
                        &refund_eh.header,
                        NULL,
                        0);
}


/**
 * Callbacks of this type are used to serve the result of submitting a
 * refund request to an exchange.
 *
 * @param cls a `struct CoinRefund`
 * @param rr response data
 */
static void
refund_cb (void *cls,
           const struct TALER_EXCHANGE_RefundResponse *rr)
{
  struct CoinRefund *cr = cls;
  const struct TALER_EXCHANGE_HttpResponse *hr = &rr->hr;

  cr->rh = NULL;
  cr->exchange_status = hr->http_status;
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Exchange refund status for coin %s is %u\n",
              TALER_B2S (&cr->coin_pub),
              hr->http_status);
  switch (hr->http_status)
  {
  case MHD_HTTP_OK:
    {
      enum GNUNET_DB_QueryStatus qs;

      cr->exchange_pub = rr->details.ok.exchange_pub;
      cr->exchange_sig = rr->details.ok.exchange_sig;
      qs = TMH_db->insert_refund_proof (TMH_db->cls,
                                        cr->refund_serial,
                                        &rr->details.ok.exchange_sig,
                                        &rr->details.ok.exchange_pub);
      if (0 >= qs)
      {
        /* generally, this is relatively harmless for the merchant, but let's at
           least log this. */
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Failed to persist exchange response to /refund in database: %d\n",
                    qs);
      }
      else
      {
        notify_refund_obtained (cr->prd);
      }
    }
    break;
  default:
    cr->exchange_code = hr->ec;
    cr->exchange_reply = json_incref ((json_t*) hr->reply);
    break;
  }
  check_resume_prd (cr->prd);
}


/**
 * Function called with the result of a
 * #TMH_EXCHANGES_keys4exchange()
 * operation.
 *
 * @param cls a `struct CoinRefund *`
 * @param keys keys of exchange, NULL on error
 * @param exchange representation of the exchange
 */
static void
exchange_found_cb (void *cls,
                   struct TALER_EXCHANGE_Keys *keys,
                   struct TMH_Exchange *exchange)
{
  struct CoinRefund *cr = cls;
  struct PostRefundData *prd = cr->prd;

  (void) exchange;
  cr->fo = NULL;
  if (NULL == keys)
  {
    prd->http_status = MHD_HTTP_GATEWAY_TIMEOUT;
    prd->ec = TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT;
    check_resume_prd (prd);
    return;
  }
  cr->rh = TALER_EXCHANGE_refund (
    TMH_curl_ctx,
    cr->exchange_url,
    keys,
    &cr->refund_amount,
    &prd->h_contract_terms,
    &cr->coin_pub,
    cr->rtransaction_id,
    &prd->hc->instance->merchant_priv,
    &refund_cb,
    cr);
}


/**
 * Function called with information about a refund.
 * It is responsible for summing up the refund amount.
 *
 * @param cls closure
 * @param refund_serial unique serial number of the refund
 * @param timestamp time of the refund (for grouping of refunds in the wallet UI)
 * @param coin_pub public coin from which the refund comes from
 * @param exchange_url URL of the exchange that issued @a coin_pub
 * @param rtransaction_id identificator of the refund
 * @param reason human-readable explanation of the refund
 * @param refund_amount refund amount which is being taken from @a coin_pub
 * @param pending true if the this refund was not yet processed by the wallet/exchange
 */
static void
process_refunds_cb (void *cls,
                    uint64_t refund_serial,
                    struct GNUNET_TIME_Timestamp timestamp,
                    const struct TALER_CoinSpendPublicKeyP *coin_pub,
                    const char *exchange_url,
                    uint64_t rtransaction_id,
                    const char *reason,
                    const struct TALER_Amount *refund_amount,
                    bool pending)
{
  struct PostRefundData *prd = cls;
  struct CoinRefund *cr;

  for (cr = prd->cr_head;
       NULL != cr;
       cr = cr->next)
    if (cr->refund_serial == refund_serial)
      return;
  /* already known */

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Found refund of %s for coin %s with reason `%s' in database\n",
              TALER_amount2s (refund_amount),
              TALER_B2S (coin_pub),
              reason);
  cr = GNUNET_new (struct CoinRefund);
  cr->refund_serial = refund_serial;
  cr->exchange_url = GNUNET_strdup (exchange_url);
  cr->prd = prd;
  cr->coin_pub = *coin_pub;
  cr->rtransaction_id = rtransaction_id;
  cr->refund_amount = *refund_amount;
  cr->execution_time = timestamp;
  GNUNET_CONTAINER_DLL_insert (prd->cr_head,
                               prd->cr_tail,
                               cr);
  if (prd->refunded)
  {
    GNUNET_assert (0 <=
                   TALER_amount_add (&prd->refund_amount,
                                     &prd->refund_amount,
                                     refund_amount));
    return;
  }
  prd->refund_amount = *refund_amount;
  prd->refunded = true;
  prd->refund_available |= pending;
}


/**
 * Obtain refunds for an order.
 *
 * @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_post_orders_ID_refund (const struct TMH_RequestHandler *rh,
                           struct MHD_Connection *connection,
                           struct TMH_HandlerContext *hc)
{
  struct PostRefundData *prd = hc->ctx;
  enum GNUNET_DB_QueryStatus qs;

  if (NULL == prd)
  {
    prd = GNUNET_new (struct PostRefundData);
    prd->sc.con = connection;
    prd->hc = hc;
    prd->order_id = hc->infix;
    hc->ctx = prd;
    hc->cc = &refund_cleanup;
    {
      enum GNUNET_GenericReturnValue res;

      struct GNUNET_JSON_Specification spec[] = {
        GNUNET_JSON_spec_fixed_auto ("h_contract",
                                     &prd->h_contract_terms),
        GNUNET_JSON_spec_end ()
      };
      res = TALER_MHD_parse_json_data (connection,
                                       hc->request_body,
                                       spec);
      if (GNUNET_OK != res)
        return (GNUNET_NO == res)
               ? MHD_YES
               : MHD_NO;
    }

    TMH_db->preflight (TMH_db->cls);
    {
      json_t *contract_terms;
      uint64_t order_serial;

      qs = TMH_db->lookup_contract_terms (TMH_db->cls,
                                          hc->instance->settings.id,
                                          hc->infix,
                                          &contract_terms,
                                          &order_serial,
                                          NULL);
      if (0 > qs)
      {
        /* single, read-only SQL statements should never cause
           serialization problems */
        GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
        /* Always report on hard error as well to enable diagnostics */
        GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_INTERNAL_SERVER_ERROR,
                                           TALER_EC_GENERIC_DB_FETCH_FAILED,
                                           "contract terms");
      }
      if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
      {
        json_decref (contract_terms);
        return TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_NOT_FOUND,
                                           TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
                                           hc->infix);
      }
      {
        struct TALER_PrivateContractHashP h_contract_terms;

        if (GNUNET_OK !=
            TALER_JSON_contract_hash (contract_terms,
                                      &h_contract_terms))
        {
          GNUNET_break (0);
          json_decref (contract_terms);
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                                             TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
                                             NULL);
        }
        json_decref (contract_terms);
        if (0 != GNUNET_memcmp (&h_contract_terms,
                                &prd->h_contract_terms))
        {
          return TALER_MHD_reply_with_error (
            connection,
            MHD_HTTP_FORBIDDEN,
            TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER,
            NULL);
        }
      }
    }
  }
  if (GNUNET_SYSERR == prd->suspended)
    return MHD_NO; /* we are in shutdown */

  if (TALER_EC_NONE != prd->ec)
  {
    GNUNET_break (0 != prd->http_status);
    /* kill pending coin refund operations immediately, just to be
       extra sure they don't modify 'prd' after we already created
       a reply (this might not be needed, but feels safer). */
    for (struct CoinRefund *cr = prd->cr_head;
         NULL != cr;
         cr = cr->next)
    {
      if (NULL != cr->fo)
      {
        TMH_EXCHANGES_keys4exchange_cancel (cr->fo);
        cr->fo = NULL;
      }
      if (NULL != cr->rh)
      {
        TALER_EXCHANGE_refund_cancel (cr->rh);
        cr->rh = NULL;
      }
    }
    return TALER_MHD_reply_with_error (connection,
                                       prd->http_status,
                                       prd->ec,
                                       NULL);
  }

  qs = TMH_db->lookup_refunds_detailed (TMH_db->cls,
                                        hc->instance->settings.id,
                                        &prd->h_contract_terms,
                                        &process_refunds_cb,
                                        prd);
  if (0 > qs)
  {
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
                                       TALER_EC_GENERIC_DB_FETCH_FAILED,
                                       "detailed refunds");
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
  {
    GNUNET_break (0);
    return TALER_MHD_reply_with_error (connection,
                                       MHD_HTTP_INTERNAL_SERVER_ERROR,
                                       TALER_EC_GENERIC_DB_FETCH_FAILED,
                                       "no coins found that could be refunded");
  }

  /* Now launch exchange interactions, unless we already have the
     response in the database! */
  for (struct CoinRefund *cr = prd->cr_head;
       NULL != cr;
       cr = cr->next)
  {
    qs = TMH_db->lookup_refund_proof (TMH_db->cls,
                                      cr->refund_serial,
                                      &cr->exchange_sig,
                                      &cr->exchange_pub);
    switch (qs)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
    case GNUNET_DB_STATUS_SOFT_ERROR:
      return TALER_MHD_reply_with_error (connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
                                         "refund proof");
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      if (NULL == cr->exchange_reply)
      {
        /* We need to talk to the exchange */
        cr->fo = TMH_EXCHANGES_keys4exchange (cr->exchange_url,
                                              false,
                                              &exchange_found_cb,
                                              cr);
        if (NULL == cr->fo)
        {
          GNUNET_break (0);
          return TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                                             TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED,
                                             cr->exchange_url);
        }
      }
      break;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      /* We got a reply earlier, set status accordingly */
      cr->exchange_status = MHD_HTTP_OK;
      break;
    }
  }

  /* Check if there are still exchange operations pending */
  if (exchange_operations_pending (prd))
  {
    if (GNUNET_NO == prd->suspended)
    {
      prd->suspended = GNUNET_YES;
      MHD_suspend_connection (connection);
      GNUNET_CONTAINER_DLL_insert (prd_head,
                                   prd_tail,
                                   prd);
    }
    return MHD_YES;   /* we're still talking to the exchange */
  }

  {
    json_t *ra;

    ra = json_array ();
    GNUNET_assert (NULL != ra);
    for (struct CoinRefund *cr = prd->cr_head;
         NULL != cr;
         cr = cr->next)
    {
      json_t *refund;

      if (MHD_HTTP_OK != cr->exchange_status)
      {
        if (NULL == cr->exchange_reply)
        {
          refund = GNUNET_JSON_PACK (
            GNUNET_JSON_pack_string ("type",
                                     "failure"),
            GNUNET_JSON_pack_uint64 ("exchange_status",
                                     cr->exchange_status),
            GNUNET_JSON_pack_uint64 ("rtransaction_id",
                                     cr->rtransaction_id),
            GNUNET_JSON_pack_data_auto ("coin_pub",
                                        &cr->coin_pub),
            TALER_JSON_pack_amount ("refund_amount",
                                    &cr->refund_amount),
            GNUNET_JSON_pack_timestamp ("execution_time",
                                        cr->execution_time));
        }
        else
        {
          refund = GNUNET_JSON_PACK (
            GNUNET_JSON_pack_string ("type",
                                     "failure"),
            GNUNET_JSON_pack_uint64 ("exchange_status",
                                     cr->exchange_status),
            GNUNET_JSON_pack_uint64 ("exchange_code",
                                     cr->exchange_code),
            GNUNET_JSON_pack_object_incref ("exchange_reply",
                                            cr->exchange_reply),
            GNUNET_JSON_pack_uint64 ("rtransaction_id",
                                     cr->rtransaction_id),
            GNUNET_JSON_pack_data_auto ("coin_pub",
                                        &cr->coin_pub),
            TALER_JSON_pack_amount ("refund_amount",
                                    &cr->refund_amount),
            GNUNET_JSON_pack_timestamp ("execution_time",
                                        cr->execution_time));
        }
      }
      else
      {
        refund = GNUNET_JSON_PACK (
          GNUNET_JSON_pack_string ("type",
                                   "success"),
          GNUNET_JSON_pack_uint64 ("exchange_status",
                                   cr->exchange_status),
          GNUNET_JSON_pack_data_auto ("exchange_sig",
                                      &cr->exchange_sig),
          GNUNET_JSON_pack_data_auto ("exchange_pub",
                                      &cr->exchange_pub),
          GNUNET_JSON_pack_uint64 ("rtransaction_id",
                                   cr->rtransaction_id),
          GNUNET_JSON_pack_data_auto ("coin_pub",
                                      &cr->coin_pub),
          TALER_JSON_pack_amount ("refund_amount",
                                  &cr->refund_amount),
          GNUNET_JSON_pack_timestamp ("execution_time",
                                      cr->execution_time));
      }
      GNUNET_assert (
        0 ==
        json_array_append_new (ra,
                               refund));
    }

    return TALER_MHD_REPLY_JSON_PACK (
      connection,
      MHD_HTTP_OK,
      TALER_JSON_pack_amount ("refund_amount",
                              &prd->refund_amount),
      GNUNET_JSON_pack_array_steal ("refunds",
                                    ra),
      GNUNET_JSON_pack_data_auto ("merchant_pub",
                                  &hc->instance->merchant_pub));
  }

  return MHD_YES;
}


/* end of taler-merchant-httpd_post-orders-ID-refund.c */
