/*
  This file is part of TALER
  Copyright (C) 2023 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 Affero General Public License for more details.
  You should have received a copy of the GNU Affero General
  Public License along with TALER; see the file COPYING.  If not,
  see 
*/
/**
 * @file taler-exchange-httpd_age-withdraw.c
 * @brief Handle /reserves/$RESERVE_PUB/age-withdraw requests
 * @author Özgür Kesim
 */
#include "platform.h"
#include 
#include 
#include "taler_json_lib.h"
#include "taler_kyclogic_lib.h"
#include "taler_mhd_lib.h"
#include "taler-exchange-httpd_age-withdraw.h"
#include "taler-exchange-httpd_responses.h"
#include "taler-exchange-httpd_keys.h"
/**
 * Send a response to a "age-withdraw" request.
 *
 * @param connection the connection to send the response to
 * @param ach value the client committed to
 * @param noreveal_index which index will the client not have to reveal
 * @return a MHD status code
 */
static MHD_RESULT
reply_age_withdraw_success (
  struct MHD_Connection *connection,
  const struct TALER_AgeWithdrawCommitmentHashP *ach,
  uint32_t noreveal_index)
{
  struct TALER_ExchangePublicKeyP pub;
  struct TALER_ExchangeSignatureP sig;
  enum TALER_ErrorCode ec =
    TALER_exchange_online_age_withdraw_confirmation_sign (
      &TEH_keys_exchange_sign_,
      ach,
      noreveal_index,
      &pub,
      &sig);
  if (TALER_EC_NONE != ec)
    return TALER_MHD_reply_with_ec (connection,
                                    ec,
                                    NULL);
  return TALER_MHD_REPLY_JSON_PACK (connection,
                                    MHD_HTTP_OK,
                                    GNUNET_JSON_pack_uint64 ("noreveal_index",
                                                             noreveal_index),
                                    GNUNET_JSON_pack_data_auto ("exchange_sig",
                                                                &sig),
                                    GNUNET_JSON_pack_data_auto ("exchange_pub",
                                                                &pub));
}
/**
 * Context for #age_withdraw_transaction.
 */
struct AgeWithdrawContext
{
  /**
   * KYC status for the operation.
   */
  struct TALER_EXCHANGEDB_KycStatus kyc;
  /**
   * Hash of the wire source URL, needed when kyc is needed.
   */
  struct TALER_PaytoHashP h_payto;
  /**
   * The data from the age-withdraw request
   */
  struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment;
  /**
   * Current time for the DB transaction.
   */
  struct GNUNET_TIME_Timestamp now;
};
/**
 * Function called to iterate over KYC-relevant
 * transaction amounts for a particular time range.
 * Called within a database transaction, so must
 * not start a new one.
 *
 * @param cls closure, identifies the event type and
 *        account to iterate over events for
 * @param limit maximum time-range for which events
 *        should be fetched (timestamp in the past)
 * @param cb function to call on each event found,
 *        events must be returned in reverse chronological
 *        order
 * @param cb_cls closure for @a cb
 */
static void
age_withdraw_amount_cb (void *cls,
                        struct GNUNET_TIME_Absolute limit,
                        TALER_EXCHANGEDB_KycAmountCallback cb,
                        void *cb_cls)
{
  struct AgeWithdrawContext *awc = cls;
  enum GNUNET_DB_QueryStatus qs;
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Signaling amount %s for KYC check during age-withdrawal\n",
              TALER_amount2s (&awc->commitment.amount_with_fee));
  if (GNUNET_OK !=
      cb (cb_cls,
          &awc->commitment.amount_with_fee,
          awc->now.abs_time))
    return;
  qs = TEH_plugin->select_withdraw_amounts_for_kyc_check (TEH_plugin->cls,
                                                          &awc->h_payto,
                                                          limit,
                                                          cb,
                                                          cb_cls);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Got %d additional transactions for this age-withdrawal and limit %llu\n",
              qs,
              (unsigned long long) limit.abs_value_us);
  GNUNET_break (qs >= 0);
}
/**
 * Function implementing age withdraw transaction.  Runs the
 * transaction logic; IF it returns a non-error code, the transaction
 * logic MUST NOT queue a MHD response.  IF it returns an hard error,
 * the transaction logic MUST queue a MHD response and set @a mhd_ret.
 * IF it returns the soft error code, the function MAY be called again
 * to retry and MUST not queue a MHD response.
 *
 * Note that "awc->commitment.sig" is set before entering this function as we
 * signed before entering the transaction.
 *
 * @param cls a `struct AgeWithdrawContext *`
 * @param connection MHD request which triggered the transaction
 * @param[out] mhd_ret set to MHD response status for @a connection,
 *             if transaction failed (!)
 * @return transaction status
 */
static enum GNUNET_DB_QueryStatus
age_withdraw_transaction (void *cls,
                          struct MHD_Connection *connection,
                          MHD_RESULT *mhd_ret)
{
  struct AgeWithdrawContext *awc = cls;
  enum GNUNET_DB_QueryStatus qs;
  bool found = false;
  bool balance_ok = false;
  uint64_t ruuid;
  awc->now = GNUNET_TIME_timestamp_get ();
  qs = TEH_plugin->reserves_get_origin (TEH_plugin->cls,
                                        &awc->commitment.reserve_pub,
                                        &awc->h_payto);
  if (qs < 0)
    return qs;
  /* If no results, reserve was created by merge,
     in which case no KYC check is required as the
     merge already did that. */
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
  {
    char *kyc_required;
    qs = TALER_KYCLOGIC_kyc_test_required (
      TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW,
      &awc->h_payto,
      TEH_plugin->select_satisfied_kyc_processes,
      TEH_plugin->cls,
      &age_withdraw_amount_cb,
      awc,
      &kyc_required);
    if (qs < 0)
    {
      if (GNUNET_DB_STATUS_HARD_ERROR == qs)
      {
        GNUNET_break (0);
        *mhd_ret = TALER_MHD_reply_with_error (connection,
                                               MHD_HTTP_INTERNAL_SERVER_ERROR,
                                               TALER_EC_GENERIC_DB_FETCH_FAILED,
                                               "kyc_test_required");
      }
      return qs;
    }
    if (NULL != kyc_required)
    {
      /* insert KYC requirement into DB! */
      awc->kyc.ok = false;
      return TEH_plugin->insert_kyc_requirement_for_account (
        TEH_plugin->cls,
        kyc_required,
        &awc->h_payto,
        &awc->kyc.requirement_row);
    }
  }
  awc->kyc.ok = true;
  qs = TEH_plugin->do_age_withdraw (TEH_plugin->cls,
                                    &awc->commitment,
                                    &found,
                                    &balance_ok,
                                    &ruuid);
  if (0 > qs)
  {
    if (GNUNET_DB_STATUS_HARD_ERROR == qs)
      *mhd_ret = TALER_MHD_reply_with_error (connection,
                                             MHD_HTTP_INTERNAL_SERVER_ERROR,
                                             TALER_EC_GENERIC_DB_FETCH_FAILED,
                                             "do_age_withdraw");
    return qs;
  }
  else if (! found)
  {
    *mhd_ret = TALER_MHD_reply_with_error (connection,
                                           MHD_HTTP_NOT_FOUND,
                                           TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN,
                                           NULL);
    return GNUNET_DB_STATUS_HARD_ERROR;
  }
  else if (! balance_ok)
  {
    TEH_plugin->rollback (TEH_plugin->cls);
    *mhd_ret = TEH_RESPONSE_reply_reserve_insufficient_balance (
      connection,
      TALER_EC_EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS,
      &awc->commitment.amount_with_fee,
      &awc->commitment.reserve_pub);
    return GNUNET_DB_STATUS_HARD_ERROR;
  }
  if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
    TEH_METRICS_num_success[TEH_MT_SUCCESS_AGE_WITHDRAW]++;
  return qs;
}
/**
 * Check if the @a rc is replayed and we already have an
 * answer. If so, replay the existing answer and return the
 * HTTP response.
 *
 * @param rc request context
 * @param[in,out] awc parsed request data
 * @param[out] mret HTTP status, set if we return true
 * @return true if the request is idempotent with an existing request
 *    false if we did not find the request in the DB and did not set @a mret
 */
static bool
request_is_idempotent (struct TEH_RequestContext *rc,
                       struct AgeWithdrawContext *awc,
                       MHD_RESULT *mret)
{
  enum GNUNET_DB_QueryStatus qs;
  struct TALER_EXCHANGEDB_AgeWithdrawCommitment commitment;
  qs = TEH_plugin->get_age_withdraw_info (TEH_plugin->cls,
                                          &awc->commitment.reserve_pub,
                                          &awc->commitment.h_commitment,
                                          &commitment);
  if (0 > qs)
  {
    GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR == qs);
    if (GNUNET_DB_STATUS_HARD_ERROR == qs)
      *mret = TALER_MHD_reply_with_error (rc->connection,
                                          MHD_HTTP_INTERNAL_SERVER_ERROR,
                                          TALER_EC_GENERIC_DB_FETCH_FAILED,
                                          "get_age_withdraw_info");
    return true; /* well, kind-of */
  }
  if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    return false;
  /* generate idempotent reply */
  TEH_METRICS_num_requests[TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW]++;
  *mret = reply_age_withdraw_success (rc->connection,
                                      &commitment.h_commitment,
                                      commitment.noreveal_index);
  return true;
}
MHD_RESULT
TEH_handler_age_withdraw (struct TEH_RequestContext *rc,
                          const struct TALER_ReservePublicKeyP *reserve_pub,
                          const json_t *root)
{
  MHD_RESULT mhd_ret;
  struct AgeWithdrawContext awc = {0};
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("reserve_sig",
                                 &awc.commitment.reserve_sig),
    GNUNET_JSON_spec_fixed_auto ("h_commitment",
                                 &awc.commitment.h_commitment),
    TALER_JSON_spec_amount ("amount",
                            TEH_currency,
                            &awc.commitment.amount_with_fee),
    GNUNET_JSON_spec_uint16 ("max_age",
                             &awc.commitment.max_age),
    GNUNET_JSON_spec_end ()
  };
  awc.commitment.reserve_pub = *reserve_pub;
  /* Parse the JSON body */
  {
    enum GNUNET_GenericReturnValue res;
    res = TALER_MHD_parse_json_data (rc->connection,
                                     root,
                                     spec);
    if (GNUNET_OK != res)
      return (GNUNET_SYSERR == res) ? MHD_NO : MHD_YES;
  }
  do {
    /* If request was made before successfully, return the previous answer */
    if (request_is_idempotent (rc,
                               &awc,
                               &mhd_ret))
      break;
    /* Verify the signature of the request body with the reserve key */
    TEH_METRICS_num_verifications[TEH_MT_SIGNATURE_EDDSA]++;
    if (GNUNET_OK !=
        TALER_wallet_age_withdraw_verify (&awc.commitment.h_commitment,
                                          &awc.commitment.amount_with_fee,
                                          awc.commitment.max_age,
                                          &awc.commitment.reserve_pub,
                                          &awc.commitment.reserve_sig))
    {
      GNUNET_break_op (0);
      mhd_ret = TALER_MHD_reply_with_error (rc->connection,
                                            MHD_HTTP_FORBIDDEN,
                                            TALER_EC_EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID,
                                            NULL);
      break;
    }
    /* Run the transaction */
    if (GNUNET_OK !=
        TEH_DB_run_transaction (rc->connection,
                                "run age withdraw",
                                TEH_MT_REQUEST_AGE_WITHDRAW,
                                &mhd_ret,
                                &age_withdraw_transaction,
                                &awc))
      break;
    /* Clean up and send back final response */
    GNUNET_JSON_parse_free (spec);
    if (! awc.kyc.ok)
      return TEH_RESPONSE_reply_kyc_required (rc->connection,
                                              &awc.h_payto,
                                              &awc.kyc);
    return reply_age_withdraw_success (rc->connection,
                                       &awc.commitment.h_commitment,
                                       awc.commitment.noreveal_index);
  } while(0);
  GNUNET_JSON_parse_free (spec);
  return mhd_ret;
}
/* end of taler-exchange-httpd_age-withdraw.c */