/*
  This file is part of TALER
  (C) 2014-2018 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 
*/
/**
 * @file merchant/backend/taler-merchant-httpd.c
 * @brief HTTP serving layer intended to perform crypto-work and
 * communication with the exchange
 * @author Marcello Stanisci
 * @author Christian Grothoff
 */
#include "platform.h"
#include 
#include 
#include "taler_util.h"
#include "taler_signatures.h"
#include "taler_exchange_service.h"
#include "taler_json_lib.h"
#include "taler_bank_service.h"
#include "taler_fakebank_lib.h"
#include "taler_testing_lib.h"
#include "taler_testing_bank_lib.h"
#include "taler_error_codes.h"
/* Error codes.  */
enum BenchmarkError {
  MISSING_BANK_URL,
  FAILED_TO_LAUNCH_BANK,
  BAD_CLI_ARG,
  BAD_CONFIG_FILE,
  NO_CONFIG_FILE_GIVEN
};
/**
 * Probability that a spent coin will be refreshed.
 */
#define REFRESH_PROBABILITY 0.1
/**
 * The whole benchmark is a repetition of a "unit".  Each
 * unit is a array containing a withdraw+deposit operation,
 * and _possibly_ a refresh of the deposited coin.
 */
#define UNITY_SIZE 6
/**
 * Account number of the merchant.  Fakebank likes any number,
 * the only requirement is that this number then matches the
 * number given when building payto URLs at deposit time.
 */
#define USER_ACCOUNT_NUMBER 3
#define FIRST_INSTRUCTION -1
#define CMD_TRANSFER_TO_EXCHANGE(label, amount) \
   TALER_TESTING_cmd_fakebank_transfer (label, amount, \
     exchange_bank_account.bank_base_url, \
     USER_ACCOUNT_NUMBER, \
     exchange_bank_account.no, \
     "dummy_user", \
     "dummy_password", \
     "http://example.com/")
/**
 * Information about an account extracted from a payto://-URL.
 */
struct Account
{
  /**
   * Hostname of the bank (possibly including port).
   */
  char *hostname;
  /**
   * Bank account number.
   */
  unsigned long long no;
  /**
   * Base URL of the bank hosting the account above.
   */
  char *bank_base_url;
};
/**
 * Hold information regarding which bank has the exchange account.
 */
static struct Account exchange_bank_account;
/**
 * Time snapshot taken right before executing the CMDs.
 */
static struct GNUNET_TIME_Absolute start_time;
/**
 * Benchmark duration time taken right after the CMD interpreter
 * returns.
 */
static struct GNUNET_TIME_Relative duration;
/**
 * Exit code.
 */
static unsigned int result;
/**
 * How many coins we want to create.
 */
static unsigned int howmany_coins = 1;
/**
 * How many clients we want to create.
 */
static unsigned int howmany_clients = 1;
/**
 * Log level used during the run.
 */
static char *loglev;
/**
 * Log file.
 */
static char *logfile;
/**
 * Config filename.
 */
static char *cfg_filename;
/**
 * Currency used.
 */
static char *currency;
/**
 * Convenience macros to allocate all the currency-dependant
 * strings;  note that the argument list of the macro is ignored.
 * It is kept as a way to make the macro more auto-descriptive
 * where it is called.
 */
#define ALLOCATE_AMOUNTS(...) \
  char *AMOUNT_5; \
  char *AMOUNT_4; \
  char *AMOUNT_1; \
  \
  GNUNET_asprintf (&AMOUNT_5, \
                   "%s:5", \
                   currency); \
  GNUNET_asprintf (&AMOUNT_4, \
                   "%s:4", \
                   currency); \
  GNUNET_asprintf (&AMOUNT_1, \
                   "%s:1", \
                   currency);
/**
 * Decide which exchange account is going to be
 * used to address a wire transfer to.  Used at
 * withdrawal time.
 *
 * @param cls closure
 * @param section section name.
 */
static void
pick_exchange_account_cb (void *cls,
                          const char *section)
{
  if (0 == strncasecmp ("account-",
                         section,
                        strlen ("account-")))
  {
    const char **s = cls;
    *s = section;
  }
}
/**
 * Parse payto:// account URL (only account information,
 * wire subject and amount are ignored).
 *
 * @param account_url URL to parse
 * @param account[out] set to information, can be NULL
 * @return #TALER_EC_NONE if @a account_url is well-formed
 */
static enum TALER_ErrorCode
parse_payto (const char *account_url,
             struct Account *account)
{
  const char *hostname;
  const char *a;
  const char *q;
  unsigned long long no;
#define PREFIX "payto://x-taler-bank/"
#define MAX_ACCOUNT_NO (1LLU << 52)
  if (0 != strncasecmp (account_url,
                        PREFIX,
                        strlen (PREFIX)))
    return TALER_EC_PAYTO_WRONG_METHOD;
  hostname = &account_url[strlen (PREFIX)];
  if (NULL == (a = strchr (hostname,
                           (unsigned char) '/')))
    return TALER_EC_PAYTO_MALFORMED;
  a++;
  if (NULL != (q = strchr (a,
                           (unsigned char) '?')))
  {
    char *s;
    s = GNUNET_strndup (a,
                        q - a);
    if (1 != sscanf (s,
                     "%llu",
                     &no))
    {
      GNUNET_free (s);
      return TALER_EC_PAYTO_MALFORMED;
    }
    GNUNET_free (s);
  }
  else
  {
    if (1 != sscanf (a,
                     "%llu",
                     &no))
      return TALER_EC_PAYTO_MALFORMED;
  }
  if (no > MAX_ACCOUNT_NO)
    return TALER_EC_PAYTO_MALFORMED;
  if (NULL != account)
  {
    long long unsigned port;
    char *p;
    /* the "-1" crops the final slash away.  */
    account->hostname = GNUNET_strndup (hostname,
                                        a - hostname - 1);
    account->no = no;
    port = 443; /* if non given, equals 443.  */
    if (NULL != (p = strchr (account->hostname,
                           (unsigned char) ':')))
    {
      p++;
      if (1 != sscanf (p,
                       "%llu",
                       &port))
      {
        GNUNET_break (0);
        TALER_LOG_ERROR ("Malformed host from payto:// URI\n");
        GNUNET_free (account->hostname);
        return TALER_EC_PAYTO_MALFORMED;
      }
    }
    if (443 != port)
    {
      GNUNET_assert
        (GNUNET_SYSERR != GNUNET_asprintf
          (&account->bank_base_url,
           "http://%s",
           account->hostname));
    }
    else
    {
      GNUNET_assert
        (GNUNET_SYSERR != GNUNET_asprintf
          (&account->bank_base_url,
           "https://%s",
           account->hostname));
    }
  }
  return TALER_EC_NONE;
}
/**
 * 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 / UINT64_MAX;
  return (random_01 <= probability) ? GNUNET_OK : GNUNET_NO;
}
/**
 * Actual commands collection.
 */
static void
run (void *cls,
     struct TALER_TESTING_Interpreter *is)
{
  struct TALER_Amount total_reserve_amount;
  struct TALER_Amount withdraw_fee;
  char *withdraw_fee_str;
  struct TALER_TESTING_Command all_commands
    [1 + /* Withdraw block */
     howmany_coins + /* All units */
     1 /* End CMD */];
  ALLOCATE_AMOUNTS
    (AMOUNT_5,
     AMOUNT_4,
     AMOUNT_1);
  total_reserve_amount.value = 5 * howmany_coins;
  total_reserve_amount.fraction = 0;
  strncpy (total_reserve_amount.currency,
           currency,
           TALER_CURRENCY_LEN);
  GNUNET_asprintf (&withdraw_fee_str,
                   "%s:0.1",
                   currency);
  TALER_string_to_amount (withdraw_fee_str,
                          &withdraw_fee);
  for (unsigned int i = 0; i < howmany_coins; i++)
    TALER_amount_add (&total_reserve_amount,
                      &total_reserve_amount,
                      &withdraw_fee);
  struct TALER_TESTING_Command make_reserve[] = {
    CMD_TRANSFER_TO_EXCHANGE
      ("create-reserve",
       TALER_amount_to_string (&total_reserve_amount)),
    TALER_TESTING_cmd_end ()
  };
  all_commands[0] = TALER_TESTING_cmd_batch ("make-reserve",
                                             make_reserve);
  for (unsigned int i = 0; i < howmany_coins; i++)
  {
    char *withdraw_label;
    char *order_enc;
    struct TALER_TESTING_Command unit[UNITY_SIZE];
    GNUNET_asprintf (&withdraw_label,
                     "withdraw-%u",
                     i);
    GNUNET_asprintf (&order_enc,
                     "{\"nonce\": %u}",
                     i);
    unit[0] =
      TALER_TESTING_cmd_withdraw_with_retry
      (TALER_TESTING_cmd_withdraw_amount
       (withdraw_label,
	is->exchange,
	"create-reserve",
	AMOUNT_5,
	MHD_HTTP_OK));
    unit[1] = TALER_TESTING_cmd_deposit
      ("deposit",
       is->exchange,
       withdraw_label,
       0, /* Index of the one withdrawn coin in the traits.  */
       TALER_TESTING_make_wire_details
         (USER_ACCOUNT_NUMBER,
          exchange_bank_account.hostname),
       order_enc,
       GNUNET_TIME_UNIT_ZERO,
       AMOUNT_1,
       MHD_HTTP_OK);
    if (eval_probability (REFRESH_PROBABILITY))
    {
      char *melt_label;
      char *reveal_label;
      GNUNET_asprintf (&melt_label,
                       "refresh-melt-%u",
                       i);
      GNUNET_asprintf (&reveal_label,
                       "refresh-reveal-%u",
                       i);
      unit[2] = TALER_TESTING_cmd_refresh_melt
        (melt_label,
         is->exchange,
         AMOUNT_4,
         withdraw_label,
         MHD_HTTP_OK);
      unit[3] = TALER_TESTING_cmd_refresh_reveal
        (reveal_label,
         is->exchange,
         melt_label,
         MHD_HTTP_OK);
      unit[4] = TALER_TESTING_cmd_refresh_link
        ("refresh-link",
         is->exchange,
         reveal_label,
         MHD_HTTP_OK);
      unit[5] = TALER_TESTING_cmd_end ();
    }
    else
      unit[2] = TALER_TESTING_cmd_end ();
    all_commands[1 + i] = TALER_TESTING_cmd_batch ("unit",
                                                   unit);
  }
  all_commands[1 + howmany_coins] = TALER_TESTING_cmd_end ();
  TALER_TESTING_run2 (is,
                      all_commands,
                      GNUNET_TIME_UNIT_FOREVER_REL); /* no timeout */
  result = 1;
}
/**
 * Stop the fakebank.
 *
 * @param cls fakebank handle
 */
static void
stop_fakebank (void *cls)
{
  struct TALER_FAKEBANK_Handle *fakebank = cls;
  TALER_FAKEBANK_stop (fakebank);
}
/**
 * Start the fakebank.
 *
 * @param cls the URL of the fakebank
 */
static void
launch_fakebank (void *cls)
{
  const char *bank_base_url = cls;
  const char *port;
  long pnum;
  struct TALER_FAKEBANK_Handle * fakebank;
  port = strrchr (bank_base_url,
                  (unsigned char) ':');
  if (NULL == port)
    pnum = 80;
  else
    pnum = strtol (port + 1, NULL, 10);
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "Starting Fakebank on port %u (%s)\n",
              (unsigned int) pnum,
              bank_base_url);
  fakebank = TALER_FAKEBANK_start ((uint16_t) pnum);
  if (NULL == fakebank)
  {
    GNUNET_break (0);
    return;
  }
  GNUNET_SCHEDULER_add_shutdown (&stop_fakebank,
                                 fakebank);
}
/**
 * Run the benchmark in parallel in many (client) processes
 * and summarize result.
 *
 * @param main_cb main function to run per process
 * @param main_cb_cls closure for @a main_cb
 * @param config_file configuration file to use
 * @param exchange_url exchange URL to use
 * @return #GNUNET_OK on success
 */
static int
parallel_benchmark (TALER_TESTING_Main main_cb,
                    void *main_cb_cls,
                    const char *config_file,
                    const char *exchange_url)
{
  int result;
  pid_t cpids[howmany_clients];
  pid_t fakebank;
  int wstatus;
  struct GNUNET_OS_Process *exchanged;
  struct GNUNET_OS_Process *wirewatch;
  /* start fakebank */
  fakebank = fork ();
  if (0 == fakebank)
  {
    GNUNET_SCHEDULER_run (&launch_fakebank,
                          exchange_bank_account.bank_base_url);
    exit (0);
  }
  if (-1 == fakebank)
  {
    GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR,
                         "fork");
    return GNUNET_SYSERR;
  }
  /* start exchange */
  exchanged = GNUNET_OS_start_process (GNUNET_NO,
                                       GNUNET_OS_INHERIT_STD_ALL,
                                       NULL, NULL, NULL,
                                       "taler-exchange-httpd",
                                       "taler-exchange-httpd",
                                       "-c", config_file,
                                       "-i",
                                       NULL);
  if (NULL == exchanged)
  {
    kill (fakebank,
	  SIGTERM);
    waitpid (fakebank,
	     &wstatus,
	     0);
    return 77;
  }
  /* start exchange */
  wirewatch = GNUNET_OS_start_process (GNUNET_NO,
                                       GNUNET_OS_INHERIT_STD_ALL,
                                       NULL, NULL, NULL,
                                       "taler-exchange-wirewatch",
                                       "taler-exchange-wirewatch",
                                       "-c", config_file,
                                       NULL);
  if (NULL == wirewatch)
  {
    GNUNET_OS_process_kill (exchanged,
                            SIGTERM);
    kill (fakebank,
	  SIGTERM);
    GNUNET_OS_process_wait (exchanged);
    GNUNET_OS_process_destroy (exchanged);
    waitpid (fakebank,
	     &wstatus,
	     0);
    return 77;
  }
  if (0 != TALER_TESTING_wait_exchange_ready (exchange_url))
  {
    GNUNET_OS_process_kill (exchanged,
                            SIGTERM);
    kill (fakebank,
	  SIGTERM);
    GNUNET_OS_process_wait (exchanged);
    GNUNET_OS_process_destroy (exchanged);
    waitpid (fakebank,
	     &wstatus,
	     0);
    return 77;
  }
  sleep (1); /* make sure fakebank process is ready before continuing */
  start_time = GNUNET_TIME_absolute_get ();
  result = GNUNET_OK;
  for (unsigned int i=0;i 1024)
  {
    TALER_LOG_ERROR ("-p option value given is too large\n");
    return BAD_CLI_ARG;
  }
  {
    char *bank_details_section;
    char *exchange_payto_url;
    GNUNET_CONFIGURATION_iterate_sections
      (cfg,
       pick_exchange_account_cb,
       &bank_details_section);
    GNUNET_assert (NULL != bank_details_section);
    GNUNET_assert
      (GNUNET_OK == GNUNET_CONFIGURATION_get_value_string
        (cfg,
         bank_details_section,
         "url",
         &exchange_payto_url));
    GNUNET_assert
      (TALER_EC_NONE == parse_payto (exchange_payto_url,
                                     &exchange_bank_account));
  }
  GNUNET_CONFIGURATION_destroy (cfg);
  compute_wire_response = GNUNET_OS_start_process
    (GNUNET_NO,
     GNUNET_OS_INHERIT_STD_ALL,
     NULL, NULL, NULL,
     "taler-exchange-wire",
     "taler-exchange-wire",
     "-c", cfg_filename,
     NULL);
  if (NULL == compute_wire_response)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
		"Failed to run `taler-exchange-wire`,"
                " is your PATH correct?\n");
    return GNUNET_NO;
  }
  GNUNET_OS_process_wait (compute_wire_response);
  GNUNET_OS_process_destroy (compute_wire_response);
  GNUNET_assert
    /* Takes care of dropping all tables.  */
    (GNUNET_OK == TALER_TESTING_prepare_exchange
      (cfg_filename,
       &exchange_url));
  result = parallel_benchmark (&run,
                               NULL,
                               cfg_filename,
                               exchange_url);
  GNUNET_free (exchange_url);
  duration = GNUNET_TIME_absolute_get_duration (start_time);
  if (GNUNET_OK == result)
  {
    fprintf (stdout,
             "Executed (W=%u, D=%u, R~=%5.2f) * P=%u, operations in %s\n",
             howmany_coins,
             howmany_coins,
             (float) howmany_coins * REFRESH_PROBABILITY,
             howmany_clients,
             GNUNET_STRINGS_relative_time_to_string
             (duration,
              GNUNET_NO));
    fprintf (stdout,
             "(approximately %s/coin)\n",
             GNUNET_STRINGS_relative_time_to_string
             (GNUNET_TIME_relative_divide (duration,
                                           howmany_coins * howmany_clients),
              GNUNET_YES));
  }
  return (GNUNET_OK == result) ? 0 : result;
}