/*
  This file is part of TALER
  (C) 2021, 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 benchmark/taler-aggregator-benchmark.c
 * @brief Setup exchange database suitable for aggregator benchmarking
 * @author Christian Grothoff
 */
#include "platform.h"
#include <jansson.h>
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_json_lib.h>
#include "taler_util.h"
#include "taler_signatures.h"
#include "taler_exchangedb_lib.h"
#include "taler_json_lib.h"
#include "taler_error_codes.h"


/**
 * Exit code.
 */
static int global_ret;

/**
 * How many deposits we want to create per merchant.
 */
static unsigned int howmany_deposits = 1;

/**
 * How many merchants do we want to setup.
 */
static unsigned int howmany_merchants = 1;

/**
 * Probability of a refund, as in $NUMBER:100.
 * Use 0 for no refunds.
 */
static unsigned int refund_rate = 0;

/**
 * Currency used.
 */
static char *currency;

/**
 * Configuration.
 */
static const struct GNUNET_CONFIGURATION_Handle *cfg;

/**
 * Database plugin.
 */
static struct TALER_EXCHANGEDB_Plugin *plugin;

/**
 * Main task doing the work().
 */
static struct GNUNET_SCHEDULER_Task *task;

/**
 * Hash of the denomination.
 */
static struct TALER_DenominationHashP h_denom_pub;

/**
 * "signature" to use for the coin(s).
 */
static struct TALER_DenominationSignature denom_sig;

/**
 * Time range when deposits start.
 */
static struct GNUNET_TIME_Timestamp start;

/**
 * Time range when deposits end.
 */
static struct GNUNET_TIME_Timestamp end;


/**
 * Throw a weighted coin with @a probability.
 *
 * @return #GNUNET_OK with @a probability,
 *         #GNUNET_NO with 1 - @a probability
 */
static unsigned int
eval_probability (float probability)
{
  uint64_t random;
  float random_01;

  random = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK,
                                     UINT64_MAX);
  random_01 = (double) random / (double) UINT64_MAX;
  return (random_01 <= probability) ? GNUNET_OK : GNUNET_NO;
}


/**
 * Randomize data at pointer @a x
 *
 * @param x pointer to data to randomize
 */
#define RANDOMIZE(x) \
        GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, x, sizeof (*x))


/**
 * Initialize @a out with an amount given by @a val and
 * @a frac using the main "currency".
 *
 * @param val value to set
 * @param frac fraction to set
 * @param[out] out where to write the amount
 */
static void
make_amount (unsigned int val,
             unsigned int frac,
             struct TALER_Amount *out)
{
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (currency,
                                        out));
  out->value = val;
  out->fraction = frac;
}


/**
 * Create random-ish timestamp.
 *
 * @return time stamp between start and end
 */
static struct GNUNET_TIME_Timestamp
random_time (void)
{
  uint64_t delta;
  struct GNUNET_TIME_Absolute ret;

  delta = end.abs_time.abs_value_us - start.abs_time.abs_value_us;
  delta = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
                                    delta);
  ret.abs_value_us = start.abs_time.abs_value_us + delta;
  return GNUNET_TIME_absolute_to_timestamp (ret);
}


/**
 * Function run on shutdown.
 *
 * @param cls unused
 */
static void
do_shutdown (void *cls)
{
  (void) cls;
  if (NULL != plugin)
  {
    TALER_EXCHANGEDB_plugin_unload (plugin);
    plugin = NULL;
  }
  if (NULL != task)
  {
    GNUNET_SCHEDULER_cancel (task);
    task = NULL;
  }
  TALER_denom_sig_free (&denom_sig);
}


struct Merchant
{

  /**
   * Public key of the merchant.  Enables later identification
   * of the merchant in case of a need to rollback transactions.
   */
  struct TALER_MerchantPublicKeyP merchant_pub;

  /**
   * Hash of the (canonical) representation of @e wire, used
   * to check the signature on the request.  Generated by
   * the exchange from the detailed wire data provided by the
   * merchant.
   */
  struct TALER_MerchantWireHashP h_wire;

  /**
   * Salt used when computing @e h_wire.
   */
  struct TALER_WireSaltP wire_salt;

  /**
   * Account information for the merchant.
   */
  struct TALER_FullPayto payto_uri;

};

struct Deposit
{

  /**
   * Information about the coin that is being deposited.
   */
  struct TALER_CoinPublicInfo coin;

  /**
   * Hash over the proposal data between merchant and customer
   * (remains unknown to the Exchange).
   */
  struct TALER_PrivateContractHashP h_contract_terms;

};


/**
 * Add a refund from @a m for @a d.
 *
 * @param m merchant granting the refund
 * @param d deposit being refunded
 * @return true on success
 */
static bool
add_refund (const struct Merchant *m,
            const struct Deposit *d)
{
  struct TALER_EXCHANGEDB_Refund r;

  r.coin = d->coin;
  r.details.merchant_pub = m->merchant_pub;
  RANDOMIZE (&r.details.merchant_sig);
  r.details.h_contract_terms = d->h_contract_terms;
  r.details.rtransaction_id = 42;
  make_amount (0, 5000000, &r.details.refund_amount);
  make_amount (0, 5, &r.details.refund_fee);
  if (0 >=
      plugin->insert_refund (plugin->cls,
                             &r))
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return false;
  }
  return true;
}


/**
 * Add a (random-ish) deposit for merchant @a m.
 *
 * @param m merchant to receive the deposit
 * @return true on success
 */
static bool
add_deposit (const struct Merchant *m)
{
  struct Deposit d;
  struct TALER_EXCHANGEDB_CoinDepositInformation deposit;
  struct TALER_EXCHANGEDB_BatchDeposit bd = {
    .cdis = &deposit,
    .num_cdis = 1
  };
  uint64_t known_coin_id;
  struct TALER_DenominationHashP dph;
  struct TALER_AgeCommitmentHash agh;

  RANDOMIZE (&d.coin.coin_pub);
  d.coin.denom_pub_hash = h_denom_pub;
  d.coin.denom_sig = denom_sig;
  RANDOMIZE (&d.h_contract_terms);
  d.coin.no_age_commitment = true;
  memset (&d.coin.h_age_commitment,
          0,
          sizeof (d.coin.h_age_commitment));

  if (0 >=
      plugin->ensure_coin_known (plugin->cls,
                                 &d.coin,
                                 &known_coin_id,
                                 &dph,
                                 &agh))
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    GNUNET_SCHEDULER_shutdown ();
    return false;
  }
  deposit.coin = d.coin;
  RANDOMIZE (&deposit.csig);
  bd.merchant_pub = m->merchant_pub;
  bd.h_contract_terms = d.h_contract_terms;
  bd.wire_salt = m->wire_salt;
  bd.receiver_wire_account = m->payto_uri;
  bd.wallet_timestamp = random_time ();
  do {
    bd.refund_deadline = random_time ();
    bd.wire_deadline = random_time ();
  } while (GNUNET_TIME_timestamp_cmp (bd.wire_deadline,
                                      <,
                                      bd.refund_deadline));

  make_amount (1, 0, &deposit.amount_with_fee);

  {
    struct GNUNET_TIME_Timestamp now;
    bool balance_ok;
    uint32_t bad_idx;
    bool conflict;

    now = random_time ();
    if (0 >=
        plugin->do_deposit (plugin->cls,
                            &bd,
                            &now,
                            &balance_ok,
                            &bad_idx,
                            &conflict))
    {
      GNUNET_break (0);
      global_ret = EXIT_FAILURE;
      GNUNET_SCHEDULER_shutdown ();
      return false;
    }
  }
  if (GNUNET_YES ==
      eval_probability (((float) refund_rate) / 100.0f))
    return add_refund (m,
                       &d);
  return true;
}


/**
 * Function to do the work.
 *
 * @param cls unused
 */
static void
work (void *cls)
{
  struct Merchant m;
  uint64_t rnd1;
  uint64_t rnd2;

  (void) cls;
  task = NULL;
  rnd1 = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
                                   UINT64_MAX);
  rnd2 = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE,
                                   UINT64_MAX);
  GNUNET_asprintf (&m.payto_uri.full_payto,
                   "payto://x-taler-bank/localhost:8082/account-%llX-%llX",
                   (unsigned long long) rnd1,
                   (unsigned long long) rnd2);
  RANDOMIZE (&m.merchant_pub);
  RANDOMIZE (&m.wire_salt);
  TALER_merchant_wire_signature_hash (m.payto_uri,
                                      &m.wire_salt,
                                      &m.h_wire);
  if (GNUNET_OK !=
      plugin->start (plugin->cls,
                     "aggregator-benchmark-fill"))
  {
    GNUNET_break (0);
    global_ret = EXIT_FAILURE;
    goto exit;
  }
  for (unsigned int i = 0; i<howmany_deposits; i++)
  {
    if (! add_deposit (&m))
    {
      global_ret = EXIT_FAILURE;
      goto exit;
    }
  }
  if (0 <=
      plugin->commit (plugin->cls))
  {
    if (0 == --howmany_merchants)
    {
      goto exit;
    }
  }
  else
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Failed to commit, will try again\n");
  }
  GNUNET_free (m.payto_uri.full_payto);
  task = GNUNET_SCHEDULER_add_now (&work,
                                   NULL);
  return;
exit:
  GNUNET_SCHEDULER_shutdown ();
  GNUNET_free (m.payto_uri.full_payto);
  return;
}


/**
 * Actual execution.
 *
 * @param cls unused
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be NULL!)
 * @param c configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *c)
{
  struct TALER_EXCHANGEDB_DenominationKeyInformation issue;

  (void) cls;
  (void) args;
  (void) cfgfile;
  /* make sure everything 'ends' before the current time,
     so that the aggregator will process everything without
     need for time-travel */
  end = GNUNET_TIME_timestamp_get ();
  start = GNUNET_TIME_absolute_to_timestamp (
    GNUNET_TIME_absolute_subtract (end.abs_time,
                                   GNUNET_TIME_UNIT_MONTHS));
  cfg = c;
  if (GNUNET_OK !=
      TALER_config_get_currency (cfg,
                                 "exchange",
                                 &currency))
  {
    global_ret = EXIT_NOTCONFIGURED;
    return;
  }
  plugin = TALER_EXCHANGEDB_plugin_load (cfg,
                                         false);
  if (NULL == plugin)
  {
    global_ret = EXIT_NOTCONFIGURED;
    return;
  }
  if (GNUNET_SYSERR ==
      plugin->preflight (plugin->cls))
  {
    global_ret = EXIT_FAILURE;
    TALER_EXCHANGEDB_plugin_unload (plugin);
    return;
  }
  GNUNET_SCHEDULER_add_shutdown (&do_shutdown,
                                 NULL);
  memset (&issue,
          0,
          sizeof (issue));
  RANDOMIZE (&issue.signature);
  issue.start
    = start;
  issue.expire_withdraw
    = GNUNET_TIME_absolute_to_timestamp (
        GNUNET_TIME_absolute_add (start.abs_time,
                                  GNUNET_TIME_UNIT_DAYS));
  issue.expire_deposit
    = end;
  issue.expire_legal
    = GNUNET_TIME_absolute_to_timestamp (
        GNUNET_TIME_absolute_add (end.abs_time,
                                  GNUNET_TIME_UNIT_YEARS));
  {
    struct TALER_DenominationPrivateKey pk;
    struct TALER_DenominationPublicKey denom_pub;
    struct TALER_CoinPubHashP c_hash;
    struct TALER_PlanchetDetail pd;
    struct TALER_BlindedDenominationSignature bds;
    struct TALER_PlanchetMasterSecretP ps;
    struct TALER_CoinSpendPublicKeyP coin_pub;
    struct TALER_AgeCommitmentHash hac;
    union GNUNET_CRYPTO_BlindingSecretP bks;
    const struct TALER_ExchangeBlindingValues *alg_values;

    RANDOMIZE (&coin_pub);
    GNUNET_assert (GNUNET_OK ==
                   TALER_denom_priv_create (&pk,
                                            &denom_pub,
                                            GNUNET_CRYPTO_BSA_RSA,
                                            1024));
    alg_values = TALER_denom_ewv_rsa_singleton ();
    denom_pub.age_mask = issue.age_mask;
    TALER_denom_pub_hash (&denom_pub,
                          &h_denom_pub);
    make_amount (2, 0, &issue.value);
    make_amount (0, 5, &issue.fees.withdraw);
    make_amount (0, 5, &issue.fees.deposit);
    make_amount (0, 5, &issue.fees.refresh);
    make_amount (0, 5, &issue.fees.refund);
    issue.denom_hash = h_denom_pub;
    if (0 >=
        plugin->insert_denomination_info (plugin->cls,
                                          &denom_pub,
                                          &issue))
    {
      GNUNET_break (0);
      GNUNET_SCHEDULER_shutdown ();
      global_ret = EXIT_FAILURE;
      return;
    }

    TALER_planchet_blinding_secret_create (&ps,
                                           TALER_denom_ewv_rsa_singleton (),
                                           &bks);

    {
      struct GNUNET_HashCode seed;
      struct TALER_AgeMask mask = {
        .bits = 1 | (1 << 8) | (1 << 12) | (1 << 16) | (1 << 18)
      };
      struct TALER_AgeCommitmentProof acp = {0};

      GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
                                  &seed,
                                  sizeof(seed));
      TALER_age_restriction_commit (&mask,
                                    13,
                                    &seed,
                                    &acp);
      TALER_age_commitment_hash (&acp.commitment,
                                 &hac);
    }

    GNUNET_assert (GNUNET_OK ==
                   TALER_denom_blind (&denom_pub,
                                      &bks,
                                      NULL,
                                      &hac,
                                      &coin_pub,
                                      alg_values,
                                      &c_hash,
                                      &pd.blinded_planchet));
    GNUNET_assert (GNUNET_OK ==
                   TALER_denom_sign_blinded (&bds,
                                             &pk,
                                             false,
                                             &pd.blinded_planchet));
    TALER_blinded_planchet_free (&pd.blinded_planchet);
    GNUNET_assert (GNUNET_OK ==
                   TALER_denom_sig_unblind (&denom_sig,
                                            &bds,
                                            &bks,
                                            &c_hash,
                                            alg_values,
                                            &denom_pub));
    TALER_blinded_denom_sig_free (&bds);
    TALER_denom_pub_free (&denom_pub);
    TALER_denom_priv_free (&pk);
  }

  {
    struct TALER_WireFeeSet fees;
    struct TALER_MasterSignatureP master_sig;
    unsigned int year;
    struct GNUNET_TIME_Timestamp ws;
    struct GNUNET_TIME_Timestamp we;

    year = GNUNET_TIME_get_current_year ();
    for (unsigned int y = year - 1; y<year + 2; y++)
    {
      ws = GNUNET_TIME_absolute_to_timestamp (GNUNET_TIME_year_to_time (y - 1));
      we = GNUNET_TIME_absolute_to_timestamp (GNUNET_TIME_year_to_time (y));
      make_amount (0, 5, &fees.wire);
      make_amount (0, 5, &fees.closing);
      memset (&master_sig,
              0,
              sizeof (master_sig));
      if (0 >
          plugin->insert_wire_fee (plugin->cls,
                                   "x-taler-bank",
                                   ws,
                                   we,
                                   &fees,
                                   &master_sig))
      {
        GNUNET_break (0);
        GNUNET_SCHEDULER_shutdown ();
        global_ret = EXIT_FAILURE;
        return;
      }
    }
  }

  task = GNUNET_SCHEDULER_add_now (&work,
                                   NULL);
}


/**
 * The main function of the taler-aggregator-benchmark tool.
 *
 * @param argc number of arguments from the command line
 * @param argv command line arguments
 * @return 0 ok, non-zero on failure
 */
int
main (int argc,
      char *const *argv)
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_option_uint ('d',
                               "deposits",
                               "DN",
                               "How many deposits we should instantiate per merchant",
                               &howmany_deposits),
    GNUNET_GETOPT_option_uint ('m',
                               "merchants",
                               "DM",
                               "How many merchants should we create",
                               &howmany_merchants),
    GNUNET_GETOPT_option_uint ('r',
                               "refunds",
                               "RATE",
                               "Probability of refund per deposit (0-100)",
                               &refund_rate),
    GNUNET_GETOPT_OPTION_END
  };
  enum GNUNET_GenericReturnValue result;

  unsetenv ("XDG_DATA_HOME");
  unsetenv ("XDG_CONFIG_HOME");
  if (0 >=
      (result = GNUNET_PROGRAM_run (
         TALER_EXCHANGE_project_data (),
         argc,
         argv,
         "taler-aggregator-benchmark",
         "generate database to benchmark the aggregator",
         options,
         &run,
         NULL)))
  {
    if (GNUNET_NO == result)
      return EXIT_SUCCESS;
    return EXIT_INVALIDARGUMENT;
  }
  return global_ret;
}
