/*
  This file is part of TALER
  Copyright (C) 2016 Inria
  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU 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 auditor/taler-auditor.c
 * @brief audits an exchange database.
 * @author Christian Grothoff
 */
#include "platform.h"
#include 
#include "taler_auditordb_plugin.h"
#include "taler_exchangedb_plugin.h"
/**
 * Return value from main().
 */
static int global_ret;
/**
 * Handle to access the exchange's database.
 */
static struct TALER_EXCHANGEDB_Plugin *edb;
/**
 * Our session with the #edb.
 */
static struct TALER_EXCHANGEDB_Session *esession;
/**
 * Handle to access the auditor's database.
 */
static struct TALER_AUDITORDB_Plugin *adb;
/**
 * Last reserve_in serial ID seen.
 */
static uint64_t reserve_in_serial_id;
/**
 * Last reserve_out serial ID seen.
 */
static uint64_t reserve_out_serial_id;
/**
 * Summary data we keep per reserve.
 */
struct ReserveSummary
{
  /**
   * Public key of the reserve.
   */
  struct TALER_ReservePublicKeyP reserve_pub;
  /**
   * Sum of all incoming transfers.
   */
  struct TALER_Amount total_in;
  /**
   * Sum of all outgoing transfers.
   */
  struct TALER_Amount total_out;
};
/**
 * Map from hash of reserve's public key to a `struct ReserveSummary`.
 */
static struct GNUNET_CONTAINER_MultiHashMap *reserves;
/**
 * Function called with details about incoming wire transfers.
 *
 * @param cls NULL
 * @param rowid unique serial ID for the refresh session in our DB
 * @param reserve_pub public key of the reserve (also the WTID)
 * @param credit amount that was received
 * @param sender_account_details information about the sender's bank account
 * @param transfer_details information that uniquely identifies the wire transfer
 * @param execution_date when did we receive the funds
 * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
 */
static int
handle_reserve_in (void *cls,
                   uint64_t rowid,
                   const struct TALER_ReservePublicKeyP *reserve_pub,
                   const struct TALER_Amount *credit,
                   const json_t *sender_account_details,
                   const json_t *transfer_details,
                   struct GNUNET_TIME_Absolute execution_date)
{
  struct GNUNET_HashCode key;
  struct ReserveSummary *rs;
  /* TODO: somewhere we need to check that 'reserve_in' data actually
     matches wire transfers from the bank. Not sure if this should be
     done within the core auditor logic though... */
  GNUNET_assert (rowid >= reserve_in_serial_id); /* should be monotonically increasing */
  reserve_in_serial_id = rowid + 1;
  GNUNET_CRYPTO_hash (reserve_pub,
                      sizeof (*reserve_pub),
                      &key);
  rs = GNUNET_CONTAINER_multihashmap_get (reserves,
                                          &key);
  if (NULL == rs)
  {
    rs = GNUNET_new (struct ReserveSummary);
    rs->reserve_pub = *reserve_pub;
    rs->total_in = *credit;
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_get_zero (credit->currency,
                                          &rs->total_out));
    GNUNET_assert (GNUNET_OK ==
                   GNUNET_CONTAINER_multihashmap_put (reserves,
                                                      &key,
                                                      rs,
                                                      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
  }
  else
  {
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_add (&rs->total_in,
                                     &rs->total_in,
                                     credit));
  }
  return GNUNET_OK;
}
/**
 * Function called with details about withdraw operations.
 *
 * @param cls closure
 * @param rowid unique serial ID for the refresh session in our DB
 * @param h_blind_ev blinded hash of the coin's public key
 * @param denom_pub public denomination key of the deposited coin
 * @param denom_sig signature over the deposited coin
 * @param reserve_pub public key of the reserve
 * @param reserve_sig signature over the withdraw operation
 * @param execution_date when did the wallet withdraw the coin
 * @param amount_with_fee amount that was withdrawn
 * @return #GNUNET_OK to continue to iterate, #GNUNET_SYSERR to stop
 */
static int
handle_reserve_out (void *cls,
                    uint64_t rowid,
                    const struct GNUNET_HashCode *h_blind_ev,
                    const struct TALER_DenominationPublicKey *denom_pub,
                    const struct TALER_DenominationSignature *denom_sig,
                    const struct TALER_ReservePublicKeyP *reserve_pub,
                    const struct TALER_ReserveSignatureP *reserve_sig,
                    struct GNUNET_TIME_Absolute execution_date,
                    const struct TALER_Amount *amount_with_fee)
{
  struct GNUNET_HashCode key;
  struct ReserveSummary *rs;
  /* TODO: check signatures, in particluar the reserve_sig! */
  GNUNET_assert (rowid >= reserve_out_serial_id); /* should be monotonically increasing */
  reserve_out_serial_id = rowid + 1;
  GNUNET_CRYPTO_hash (reserve_pub,
                      sizeof (*reserve_pub),
                      &key);
  rs = GNUNET_CONTAINER_multihashmap_get (reserves,
                                          &key);
  if (NULL == rs)
  {
    rs = GNUNET_new (struct ReserveSummary);
    rs->reserve_pub = *reserve_pub;
    rs->total_out = *amount_with_fee;
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_get_zero (amount_with_fee->currency,
                                          &rs->total_in));
    GNUNET_assert (GNUNET_OK ==
                   GNUNET_CONTAINER_multihashmap_put (reserves,
                                                      &key,
                                                      rs,
                                                      GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
  }
  else
  {
    GNUNET_assert (GNUNET_OK ==
                   TALER_amount_add (&rs->total_out,
                                     &rs->total_out,
                                     amount_with_fee));
  }
  return GNUNET_OK;
}
/**
 * Check that the reserve summary matches what the exchange database
 * thinks about the reserve, and update our own state of the reserve.
 *
 * Remove all reserves that we are happy with from the DB.
 *
 * @param cls NULL
 * @param key hash of the reserve public key
 * @param value a `struct ReserveSummary`
 * @return #GNUNET_OK to process more entries
 */
static int
verify_reserve_balance (void *cls,
                        const struct GNUNET_HashCode *key,
                        void *value)
{
  struct ReserveSummary *rs = value;
  struct TALER_EXCHANGEDB_Reserve reserve;
  struct TALER_Amount balance;
  reserve.pub = rs->reserve_pub;
  if (GNUNET_OK !=
      edb->reserve_get (edb->cls,
                        esession,
                        &reserve))
  {
    GNUNET_break (0);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Failed to find summary for reserve `%s'\n",
                TALER_B2S (&rs->reserve_pub));
    return GNUNET_OK;
  }
  /* TODO: check reserve.expiry? */
  /* FIXME: get previous reserve state from auditor DB */
  /* FIXME: simplified computation as we have no previous reserve state yet */
  if (GNUNET_SYSERR ==
      TALER_amount_subtract (&balance,
                             &rs->total_in,
                             &rs->total_out))
  {
    GNUNET_break (0);
    return GNUNET_OK;
  }
  if (0 != TALER_amount_cmp (&balance,
                             &reserve.balance))
  {
    GNUNET_break (0);
    return GNUNET_OK;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Reserve balance `%s' OK\n",
              TALER_B2S (&rs->reserve_pub));
  /* FIXME: commit new reserve state from auditor DB */
  GNUNET_assert (GNUNET_YES ==
                 GNUNET_CONTAINER_multihashmap_remove (reserves,
                                                       key,
                                                       rs));
  GNUNET_free (rs);
  return GNUNET_OK;
}
/**
 * Analyze reserves for being well-formed.
 *
 * @return #GNUNET_OK on success, #GNUNET_SYSERR on invariant violation
 */
static int
analyze_reserves ()
{
  reserves = GNUNET_CONTAINER_multihashmap_create (512,
                                                   GNUNET_NO);
  /* FIXME: check return values... */
  edb->select_reserves_in_above_serial_id (edb->cls,
                                           esession,
                                           reserve_in_serial_id,
                                           &handle_reserve_in,
                                           NULL);
  edb->select_reserves_out_above_serial_id (edb->cls,
                                            esession,
                                            reserve_out_serial_id,
                                            &handle_reserve_out,
                                            NULL);
  GNUNET_CONTAINER_multihashmap_iterate (reserves,
                                         &verify_reserve_balance,
                                         NULL);
  /* FIXME: any values left in #reserves indicate errors! */
  GNUNET_CONTAINER_multihashmap_destroy (reserves);
  return GNUNET_OK;
}
/**
 * Main function that will be run.
 *
 * @param cls closure
 * @param args remaining command-line arguments
 * @param cfgfile name of the configuration file used (for saving, can be NULL!)
 * @param cfg configuration
 */
static void
run (void *cls,
     char *const *args,
     const char *cfgfile,
     const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  if (NULL ==
      (edb = TALER_EXCHANGEDB_plugin_load (cfg)))
  {
    fprintf (stderr,
             "Failed to initialize exchange database plugin.\n");
    global_ret = 1;
    return;
  }
  if (NULL ==
      (adb = TALER_AUDITORDB_plugin_load (cfg)))
  {
    fprintf (stderr,
             "Failed to initialize auditor database plugin.\n");
    global_ret = 1;
    TALER_EXCHANGEDB_plugin_unload (edb);
    return;
  }
  esession = edb->get_session (edb->cls);
  if (NULL == esession)
  {
    fprintf (stderr,
             "Failed to initialize exchange session.\n");
    global_ret = 1;
    TALER_AUDITORDB_plugin_unload (adb);
    TALER_EXCHANGEDB_plugin_unload (edb);
    return;
  }
  /* FIXME: init these from auditordb */
  reserve_in_serial_id = 1;
  reserve_out_serial_id = 1;
  analyze_reserves ();
  TALER_AUDITORDB_plugin_unload (adb);
  TALER_EXCHANGEDB_plugin_unload (edb);
}
/**
 * The main function of the database initialization tool.
 * Used to initialize the Taler Exchange's database.
 *
 * @param argc number of arguments from the command line
 * @param argv command line arguments
 * @return 0 ok, 1 on error
 */
int
main (int argc,
      char *const *argv)
{
  const struct GNUNET_GETOPT_CommandLineOption options[] = {
    GNUNET_GETOPT_OPTION_END
  };
  /* force linker to link against libtalerutil; if we do
     not do this, the linker may "optimize" libtalerutil
     away and skip #TALER_OS_init(), which we do need */
  (void) TALER_project_data_default ();
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_log_setup ("taler-auditor",
                                   "INFO",
                                   NULL));
  if (GNUNET_OK !=
      GNUNET_PROGRAM_run (argc,
			  argv,
                          "taler-auditor",
			  "Audit Taler exchange database",
			  options,
			  &run,
			  NULL))
    return 1;
  return global_ret;
}
/* end of taler-auditor.c */