/*
   This file is part of TALER
   Copyright (C) 2022--2026 Taler Systems SA

   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 <http://www.gnu.org/licenses/>
 */
/**
 * @file exchangedb/do_import_credits.c
 * @brief Implementation of the do_import_credits function for Postgres
 * @author Christian Grothoff
 * @author Joseph Xu
 */
#include "taler/taler_pq_lib.h"
#include "exchange-database/do_import_credits.h"
#include "helper.h"


/**
 * Compute the notification channel for a reserve that was credited.
 *
 * @param reserve_pub reserve to notify on
 * @return string to pass to postgres for the notification
 */
static char *
compute_notify_on_reserve (const struct TALER_ReservePublicKeyP *reserve_pub)
{
  struct TALER_EXCHANGEDB_ReserveEventP rep = {
    .header.size = htons (sizeof (rep)),
    .header.type = htons (TALER_DBEVENT_EXCHANGE_RESERVE_INCOMING),
    .reserve_pub = *reserve_pub
  };

  return GNUNET_PQ_get_event_notify_channel (&rep.header);
}


/**
 * Compute the notification channel for an account that authenticated itself.
 *
 * @param h_payto normalized hash of the account that was authenticated
 * @return string to pass to postgres for the notification
 */
static char *
compute_notify_on_kycauth (const struct TALER_NormalizedPaytoHashP *h_payto)
{
  struct TALER_EXCHANGEDB_KycCompletedEventP rep = {
    .header.size = htons (sizeof (rep)),
    .header.type = htons (TALER_DBEVENT_EXCHANGE_KYC_COMPLETED),
    .h_payto = *h_payto
  };

  return GNUNET_PQ_get_event_notify_channel (&rep.header);
}


/**
 * Closure for #helper_cb().
 */
struct Context
{
  /**
   * Array with entries set to 'true' for transfers we had already imported.
   */
  bool *transaction_duplicates;

  /**
   * Length of the @e transaction_duplicates array.
   */
  unsigned int length;

  /**
   * Set to #GNUNET_SYSERR on failures.
   */
  enum GNUNET_GenericReturnValue status;
};


/**
 * Function called with the rows the stored procedure returned, one per
 * reserve transfer, in input order.
 *
 * @param cls closure of type `struct Context *`
 * @param result the postgres result
 * @param num_results the number of results in @a result
 */
static void
helper_cb (void *cls,
           PGresult *result,
           unsigned int num_results)
{
  struct Context *ctx = cls;

  if (num_results > ctx->length)
  {
    /* The procedure returns one row per reserve transfer we passed in; a
       different count means the two have drifted apart, and writing past the
       caller's array is not the way to find that out. */
    GNUNET_break (0);
    ctx->status = GNUNET_SYSERR;
    return;
  }
  for (unsigned int i = 0; i<num_results; i++)
  {
    struct GNUNET_PQ_ResultSpec rs[] = {
      GNUNET_PQ_result_spec_bool ("out_duplicate",
                                  &ctx->transaction_duplicates[i]),
      GNUNET_PQ_result_spec_end
    };

    if (GNUNET_OK !=
        GNUNET_PQ_extract_result (result,
                                  rs,
                                  i))
    {
      GNUNET_break (0);
      ctx->status = GNUNET_SYSERR;
      return;
    }
  }
}


enum GNUNET_DB_QueryStatus
TALER_EXCHANGEDB_do_import_credits (
  struct TALER_EXCHANGEDB_PostgresContext *pg,
  const struct TALER_EXCHANGEDB_CreditBatch *batch,
  enum GNUNET_DB_QueryStatus *reserve_results)
{
  unsigned int rlen = batch->reserves_length;
  unsigned int klen = batch->kycauths_length;
  unsigned int wlen = batch->wads_length;
  unsigned int dups = 0;

  /* reserve transfers */
  struct TALER_ReservePublicKeyP reserve_pubs[GNUNET_NZL (rlen)];
  uint64_t wire_references[GNUNET_NZL (rlen)];
  struct TALER_Amount balances[GNUNET_NZL (rlen)];
  struct GNUNET_TIME_Timestamp execution_times[GNUNET_NZL (rlen)];
  struct TALER_FullPaytoHashP h_full_paytos[GNUNET_NZL (rlen)];
  struct TALER_NormalizedPaytoHashP h_normalized_paytos[GNUNET_NZL (rlen)];
  const char *payto_uris[GNUNET_NZL (rlen)];
  char *notify_s[GNUNET_NZL (rlen)];
  bool transaction_duplicates[GNUNET_NZL (rlen)];

  /* KYC authentication transfers */
  union TALER_AccountPublicKeyP ka_account_pubs[GNUNET_NZL (klen)];
  uint64_t ka_wire_references[GNUNET_NZL (klen)];
  struct TALER_Amount ka_balances[GNUNET_NZL (klen)];
  struct GNUNET_TIME_Timestamp ka_execution_times[GNUNET_NZL (klen)];
  struct TALER_FullPaytoHashP ka_h_full_paytos[GNUNET_NZL (klen)];
  struct TALER_NormalizedPaytoHashP ka_h_normalized_paytos[GNUNET_NZL (klen)];
  const char *ka_payto_uris[GNUNET_NZL (klen)];
  char *ka_notify_s[GNUNET_NZL (klen)];

  /* WAD transfers */
  struct TALER_WadIdentifierP wad_ids[GNUNET_NZL (wlen)];
  const char *wad_origin_exchange_urls[GNUNET_NZL (wlen)];
  struct TALER_Amount wad_balances[GNUNET_NZL (wlen)];
  struct GNUNET_TIME_Timestamp wad_execution_times[GNUNET_NZL (wlen)];

  struct GNUNET_TIME_Timestamp reserve_expiration
    = GNUNET_TIME_relative_to_timestamp (pg->idle_reserve_expiration_time);
  struct GNUNET_TIME_Timestamp gc
    = GNUNET_TIME_relative_to_timestamp (pg->legal_reserve_expiration_time);
  struct GNUNET_TIME_Absolute lease_until
    = GNUNET_TIME_relative_to_absolute (batch->lease);
  enum GNUNET_DB_QueryStatus qs;

  for (unsigned int i = 0; i<rlen; i++)
  {
    const struct TALER_EXCHANGEDB_ReserveInInfo *reserve = &batch->reserves[i];

    TALER_full_payto_hash (reserve->sender_account_details,
                           &h_full_paytos[i]);
    TALER_full_payto_normalize_and_hash (reserve->sender_account_details,
                                         &h_normalized_paytos[i]);
    notify_s[i] = compute_notify_on_reserve (reserve->reserve_pub);
    reserve_pubs[i] = *reserve->reserve_pub;
    balances[i] = *reserve->balance;
    execution_times[i] = reserve->execution_time;
    payto_uris[i] = reserve->sender_account_details.full_payto;
    wire_references[i] = reserve->wire_reference;
  }
  for (unsigned int i = 0; i<klen; i++)
  {
    const struct TALER_EXCHANGEDB_KycauthInInfo *ka = &batch->kycauths[i];

    TALER_full_payto_hash (ka->sender_account_details,
                           &ka_h_full_paytos[i]);
    TALER_full_payto_normalize_and_hash (ka->sender_account_details,
                                         &ka_h_normalized_paytos[i]);
    ka_notify_s[i] = compute_notify_on_kycauth (&ka_h_normalized_paytos[i]);
    ka_account_pubs[i] = *ka->account_pub;
    ka_balances[i] = *ka->balance;
    ka_execution_times[i] = ka->execution_time;
    ka_payto_uris[i] = ka->sender_account_details.full_payto;
    ka_wire_references[i] = ka->wire_reference;
  }
  for (unsigned int i = 0; i<wlen; i++)
  {
    const struct TALER_EXCHANGEDB_WadInInfo *wad = &batch->wads[i];

    wad_ids[i] = *wad->wad_id;
    wad_origin_exchange_urls[i] = wad->origin_exchange_url;
    wad_balances[i] = *wad->balance;
    wad_execution_times[i] = wad->execution_time;
  }

  PREPARE (pg,
           "do_import_credits",
           "SELECT"
           " out_duplicate"
           " FROM exchange_do_import_credits"
           " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17"
           " ,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28);");
  {
    struct GNUNET_PQ_QueryParam params[] = {
      GNUNET_PQ_query_param_string (batch->exchange_account_name),
      GNUNET_PQ_query_param_timestamp (&gc),
      GNUNET_PQ_query_param_timestamp (&reserve_expiration),
      /* reserve transfers */
      GNUNET_PQ_query_param_array_auto_from_type (rlen,
                                                  reserve_pubs,
                                                  pg->conn),
      GNUNET_PQ_query_param_array_uint64 (rlen,
                                          wire_references,
                                          pg->conn),
      TALER_PQ_query_param_array_amount (rlen,
                                         balances,
                                         pg->conn),
      GNUNET_PQ_query_param_array_timestamp (rlen,
                                             execution_times,
                                             pg->conn),
      GNUNET_PQ_query_param_array_auto_from_type (rlen,
                                                  h_full_paytos,
                                                  pg->conn),
      GNUNET_PQ_query_param_array_auto_from_type (rlen,
                                                  h_normalized_paytos,
                                                  pg->conn),
      GNUNET_PQ_query_param_array_ptrs_string (rlen,
                                               payto_uris,
                                               pg->conn),
      GNUNET_PQ_query_param_array_ptrs_string (rlen,
                                               (const char **) notify_s,
                                               pg->conn),
      /* KYC authentication transfers */
      GNUNET_PQ_query_param_array_auto_from_type (klen,
                                                  ka_account_pubs,
                                                  pg->conn),
      GNUNET_PQ_query_param_array_uint64 (klen,
                                          ka_wire_references,
                                          pg->conn),
      TALER_PQ_query_param_array_amount (klen,
                                         ka_balances,
                                         pg->conn),
      GNUNET_PQ_query_param_array_timestamp (klen,
                                             ka_execution_times,
                                             pg->conn),
      GNUNET_PQ_query_param_array_auto_from_type (klen,
                                                  ka_h_full_paytos,
                                                  pg->conn),
      GNUNET_PQ_query_param_array_auto_from_type (klen,
                                                  ka_h_normalized_paytos,
                                                  pg->conn),
      GNUNET_PQ_query_param_array_ptrs_string (klen,
                                               ka_payto_uris,
                                               pg->conn),
      GNUNET_PQ_query_param_array_ptrs_string (klen,
                                               (const char **) ka_notify_s,
                                               pg->conn),
      /* WAD transfers */
      GNUNET_PQ_query_param_array_auto_from_type (wlen,
                                                  wad_ids,
                                                  pg->conn),
      GNUNET_PQ_query_param_array_ptrs_string (wlen,
                                               wad_origin_exchange_urls,
                                               pg->conn),
      TALER_PQ_query_param_array_amount (wlen,
                                         wad_balances,
                                         pg->conn),
      GNUNET_PQ_query_param_array_timestamp (wlen,
                                             wad_execution_times,
                                             pg->conn),
      /* shard bookkeeping */
      GNUNET_PQ_query_param_string (batch->job_name),
      GNUNET_PQ_query_param_uint64 (&batch->shard_start),
      GNUNET_PQ_query_param_uint64 (&batch->shard_end),
      GNUNET_PQ_query_param_uint64 (&batch->progress_row),
      GNUNET_PQ_query_param_absolute_time (&lease_until),
      GNUNET_PQ_query_param_end
    };
    struct Context ctx = {
      .transaction_duplicates = transaction_duplicates,
      .length = rlen,
      .status = GNUNET_OK
    };

    qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
                                               "do_import_credits",
                                               params,
                                               &helper_cb,
                                               &ctx);
    GNUNET_PQ_cleanup_query_params_closures (params);
    if ( (qs >= 0) &&
         (GNUNET_OK != ctx.status) )
      qs = GNUNET_DB_STATUS_HARD_ERROR;
    if (qs < 0)
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Failed to import credit batch (%d)\n",
                  qs);
  }

  for (unsigned int i = 0; i<rlen; i++)
    GNUNET_free (notify_s[i]);
  for (unsigned int i = 0; i<klen; i++)
    GNUNET_free (ka_notify_s[i]);
  if (qs < 0)
    return qs;
  for (unsigned int i = 0; i<rlen; i++)
  {
    if (transaction_duplicates[i])
      dups++;
    reserve_results[i] = transaction_duplicates[i]
      ? GNUNET_DB_STATUS_SUCCESS_NO_RESULTS
      : GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
  }
  GNUNET_PQ_event_do_poll (pg->conn);
  if (0 != dups)
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "%u/%u duplicates among incoming transactions. Try increasing WIREWATCH_IDLE_SLEEP_INTERVAL in the [exchange] configuration section (if this happens a lot).\n",
                dups,
                rlen);
  return qs;
}
