/*
  This file is part of TALER
  Copyright (C) 2023,2025 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 <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-exchange-httpd_reveal-withdraw.c
 * @brief Handle /reveal-withdraw requests
 * @author Özgür Kesim
 */
#include "platform.h"
#include <gnunet/gnunet_common.h>
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#include <microhttpd.h>
#include "taler-exchange-httpd_metrics.h"
#include "taler_error_codes.h"
#include "taler_exchangedb_plugin.h"
#include "taler_mhd_lib.h"
#include "taler-exchange-httpd_mhd.h"
#include "taler-exchange-httpd_reveal-withdraw.h"
#include "taler-exchange-httpd_responses.h"
#include "taler-exchange-httpd_keys.h"

/**
 * State for an /reveal-withdraw operation.
 */
struct WithdrawRevealContext
{

  /**
   * Commitment for the withdraw operation, previously called by the
   * client.
   */
  struct TALER_HashBlindedPlanchetsP planchets_h;

  /**
   * TALER_CNC_KAPPA-1 secrets for disclosed coin batches.
   */
  struct TALER_RevealWithdrawMasterSeedsP disclosed_batch_seeds;

  /**
   * The data from the original withdraw.  Will be retrieved from
   * the DB via @a wch.
   */
  struct TALER_EXCHANGEDB_Withdraw withdraw;
};


/**
 * Parse the json body of an '/reveal-withdraw' request.  It extracts
 * the denomination hashes, blinded coins and disclosed coins and allocates
 * memory for those.
 *
 * @param connection The MHD connection to handle
 * @param j_disclosed_batch_seeds The n*(kappa-1) disclosed coins' private keys in JSON format, from which all other attributes (age restriction, blinding, nonce) will be derived from
 * @param[out] actx The context of the operation, only partially built at call time
 * @param[out] mhd_ret The result if a reply is queued for MHD
 * @return true on success, false on failure, with a reply already queued for MHD.
 */
static enum GNUNET_GenericReturnValue
parse_withdraw_reveal_json (
  struct MHD_Connection *connection,
  const json_t *j_disclosed_batch_seeds,
  struct WithdrawRevealContext *actx,
  MHD_RESULT *mhd_ret)
{
  size_t num_entries;
  const char *error;
  struct GNUNET_JSON_Specification tuple[] = {
    GNUNET_JSON_spec_fixed (NULL,
                            &actx->disclosed_batch_seeds.tuple[0],
                            sizeof(actx->disclosed_batch_seeds.tuple[0])),
    GNUNET_JSON_spec_fixed (NULL,
                            &actx->disclosed_batch_seeds.tuple[1],
                            sizeof(actx->disclosed_batch_seeds.tuple[1])),
    GNUNET_JSON_spec_end ()
  };
  struct GNUNET_JSON_Specification spec[] = {
    TALER_JSON_spec_tuple_of (NULL,
                              tuple),
    GNUNET_JSON_spec_end ()
  };

  /**
   * Note that above, in tuple[], we have hard-wired
   * the size of TALER_CNC_KAPPA.
   * Let's make sure we keep this in sync.
   */
  _Static_assert ((TALER_CNC_KAPPA - 1) == 2);

  num_entries = json_array_size (j_disclosed_batch_seeds);   /* 0, if not an array */
  if (! json_is_array (j_disclosed_batch_seeds))
    error = "disclosed_batch_seeds must be an array";
  else if (num_entries == 0)
    error = "disclosed_batch_seeds must not be empty";
  else if (num_entries != TALER_CNC_KAPPA - 1)
    error =
      "disclosed_batch_seeds must be an array of size "
      TALER_CNC_KAPPA_MINUS_ONE_STR;
  else
    error = NULL;

  if ( (NULL != error) ||
       (GNUNET_OK !=
        GNUNET_JSON_parse (j_disclosed_batch_seeds,
                           spec,
                           &error,
                           NULL)) )
  {
    GNUNET_break_op (0);
    *mhd_ret = TALER_MHD_reply_with_ec (connection,
                                        TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                        error);
    return GNUNET_SYSERR;
  }

  return GNUNET_OK;
}


/**
 * Check if the request belongs to an existing withdraw request.
 * If so, sets the withdraw object with the request data.
 * Otherwise, it queues an appropriate MHD response.
 *
 * @param connection The HTTP connection to the client
 * @param planchets_h Original commitment value sent with the withdraw request
 * @param[out] withdraw Data from the original withdraw request
 * @param[out] result In the error cases, a response will be queued with MHD and this will be the result.
 * @return #GNUNET_OK if the withdraw request has been found,
 *   #GNUNET_SYSERR if we did not find the request in the DB
 */
static enum GNUNET_GenericReturnValue
find_original_withdraw (
  struct MHD_Connection *connection,
  const struct TALER_HashBlindedPlanchetsP *planchets_h,
  struct TALER_EXCHANGEDB_Withdraw *withdraw,
  MHD_RESULT *result)
{
  enum GNUNET_DB_QueryStatus qs;

  for (unsigned int try = 0; try < 3; try++)
  {
    qs = TEH_plugin->get_withdraw (TEH_plugin->cls,
                                   planchets_h,
                                   withdraw);
    switch (qs)
    {
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      return GNUNET_OK; /* Only happy case */
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      *result = TALER_MHD_reply_with_error (connection,
                                            MHD_HTTP_NOT_FOUND,
                                            TALER_EC_EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN,
                                            NULL);
      return GNUNET_SYSERR;
    case GNUNET_DB_STATUS_HARD_ERROR:
      *result = TALER_MHD_reply_with_ec (connection,
                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
                                         "get_withdraw");
      return GNUNET_SYSERR;
    case GNUNET_DB_STATUS_SOFT_ERROR:
      break; /* try again */
    default:
      GNUNET_break (0);
      *result = TALER_MHD_reply_with_ec (connection,
                                         TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                                         NULL);
      return GNUNET_SYSERR;
    }
  }
  /* after unsuccessful retries*/
  GNUNET_break (0);
  *result = TALER_MHD_reply_with_ec (connection,
                                     TALER_EC_GENERIC_DB_FETCH_FAILED,
                                     "get_withdraw");
  return GNUNET_SYSERR;
}


/**
 * @brief Derives an age-restricted planchet from a given secret and calculates the hash
 *
 * @param connection Connection to the client
 * @param denom_key The denomination key
 * @param secret The secret to a planchet
 * @param r_pub The public R-values from the exchange in case of a CS denomination; might be NULL
 * @param nonce The derived nonce needed for CS denomination
 * @param max_age The maximum age allowed
 * @param[out] bch Hashcode to write
 * @param[out] result On error, a HTTP-response will be queued and result set accordingly
 * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise, with an error message
 * written to the client and @e result set.
 */
static enum GNUNET_GenericReturnValue
calculate_blinded_hash (
  struct MHD_Connection *connection,
  struct TEH_DenominationKey *denom_key,
  const struct TALER_PlanchetMasterSecretP *secret,
  const struct GNUNET_CRYPTO_CSPublicRPairP *r_pub,
  union GNUNET_CRYPTO_BlindSessionNonce *nonce,
  uint8_t max_age,
  struct TALER_BlindedCoinHashP *bch,
  MHD_RESULT *result)
{
  enum GNUNET_GenericReturnValue ret;
  struct TALER_AgeCommitmentHash ach;

  /* calculate age commitment hash */
  {
    struct TALER_AgeCommitmentProof acp;

    TALER_age_restriction_from_secret (secret,
                                       &denom_key->denom_pub.age_mask,
                                       max_age,
                                       &acp);
    TALER_age_commitment_hash (&acp.commitment,
                               &ach);
    TALER_age_commitment_proof_free (&acp);
  }

  /* Next: calculate planchet */
  {
    struct TALER_CoinPubHashP c_hash;
    struct TALER_PlanchetDetail detail = {0};
    struct TALER_CoinSpendPrivateKeyP coin_priv;
    union GNUNET_CRYPTO_BlindingSecretP bks;
    struct GNUNET_CRYPTO_BlindingInputValues bi = {
      .cipher = denom_key->denom_pub.bsign_pub_key->cipher
    };
    struct TALER_ExchangeBlindingValues alg_values = {
      .blinding_inputs = &bi
    };

    if (GNUNET_CRYPTO_BSA_CS == bi.cipher)
    {
      GNUNET_assert (NULL != r_pub);
      GNUNET_assert (NULL != nonce);
      bi.details.cs_values = *r_pub;
    }
    TALER_planchet_blinding_secret_create (secret,
                                           &alg_values,
                                           &bks);
    TALER_planchet_setup_coin_priv (secret,
                                    &alg_values,
                                    &coin_priv);
    ret = TALER_planchet_prepare (&denom_key->denom_pub,
                                  &alg_values,
                                  &bks,
                                  nonce,
                                  &coin_priv,
                                  &ach,
                                  &c_hash,
                                  &detail);
    if (GNUNET_OK != ret)
    {
      GNUNET_break (0);
      *result = TALER_MHD_REPLY_JSON_PACK (connection,
                                           MHD_HTTP_INTERNAL_SERVER_ERROR,
                                           GNUNET_JSON_pack_string (
                                             "details",
                                             "failed to prepare planchet from base key"));
      return ret;
    }

    TALER_coin_ev_hash (&detail.blinded_planchet,
                        &denom_key->h_denom_pub,
                        bch);
    TALER_blinded_planchet_free (&detail.blinded_planchet);
  }

  return ret;
}


/**
 * @brief Checks the validity of the disclosed coins as follows:
 * - Derives and calculates the disclosed coins'
 *    - public keys,
 *    - nonces (if applicable),
 *    - age commitments,
 *    - blindings
 *    - blinded hashes
 * - Computes planchets_h with those calculated and the undisclosed hashes
 * - Compares planchets_h with the value from the original commitment
 * - Verifies that all public keys in indices larger than the age group
 *   corresponding to max_age are derived from the constant public key.
 *
 * The derivation of the blindings, (potential) nonces and age-commitment from
 * a coin's private keys is defined in
 * https://docs.taler.net/design-documents/024-age-restriction.html#withdraw
 *
 * @param con HTTP-connection to the client
 * @param wd Original withdraw request
 * @param disclosed_batch_seeds The secrets of the disclosed coins, (TALER_CNC_KAPPA - 1)*num_coins many
 * @param[out] result On error, a HTTP-response will be queued and result set accordingly
 * @return #GNUNET_OK on success, #GNUNET_SYSERR otherwise
 */
static enum GNUNET_GenericReturnValue
verify_commitment_and_max_age (
  struct MHD_Connection *con,
  const struct TALER_EXCHANGEDB_Withdraw *wd,
  const struct TALER_RevealWithdrawMasterSeedsP *disclosed_batch_seeds,
  MHD_RESULT *result)
{
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
  struct GNUNET_HashContext *hash_context;
  struct TEH_KeyStateHandle *keys;
  struct TEH_DenominationKey *denom_keys[wd->num_coins];
  struct TALER_Amount total_amount;
  struct TALER_Amount total_fee;
  struct TALER_AgeMask mask;
  struct TALER_PlanchetMasterSecretP secrets[
    TALER_CNC_KAPPA - 1][wd->num_coins];
  bool is_cs[wd->num_coins];
  size_t cs_count = 0;
  uint8_t secrets_idx = 0;   /* first index into secrets */

  GNUNET_assert (wd->noreveal_index < TALER_CNC_KAPPA);

  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (TEH_currency,
                                        &total_amount));
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (TEH_currency,
                                        &total_fee));

  memset (denom_keys,
          0,
          sizeof(denom_keys));
  memset (is_cs,
          0,
          sizeof(is_cs));

  /* We need the current keys in memory for the meta-data of the denominations */
  keys = TEH_keys_get_state ();
  if (NULL == keys)
  {
    *result = TALER_MHD_reply_with_ec (con,
                                       TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
                                       NULL);
    return GNUNET_SYSERR;
  }

  /* Find the denomination keys */
  for (size_t i = 0; i < wd->num_coins; i++)
  {
    denom_keys[i] =
      TEH_keys_denomination_by_serial_from_state (
        keys,
        wd->denom_serials[i]);
    if (NULL == denom_keys[i])
    {
      GNUNET_break_op (0);
      *result = TALER_MHD_reply_with_ec (con,
                                         TALER_EC_EXCHANGE_GENERIC_KEYS_MISSING,
                                         NULL);
      return GNUNET_SYSERR;
    }

    /* Accumulate amount and fees */
    GNUNET_assert (0 <= TALER_amount_add (&total_amount,
                                          &total_amount,
                                          &denom_keys[i]->meta.value));
    GNUNET_assert (0 <= TALER_amount_add (&total_fee,
                                          &total_fee,
                                          &denom_keys[i]->meta.fees.withdraw));

    if (i == 0)
      mask = denom_keys[i]->meta.age_mask;
    GNUNET_assert (mask.bits == denom_keys[i]->meta.age_mask.bits);

    if (GNUNET_CRYPTO_BSA_CS ==
        denom_keys[i]->denom_pub.bsign_pub_key->cipher)
    {
      is_cs[i] = true;
      cs_count++;
    }
  }

  hash_context = GNUNET_CRYPTO_hash_context_start ();

  {
    uint32_t cs_indices[cs_count];
    union GNUNET_CRYPTO_BlindSessionNonce nonces[cs_count];
    size_t cs_idx = 0; /* [0...cs_count) */

    for (size_t i = 0; i < wd->num_coins; i++)
      if (is_cs[i])
        cs_indices[cs_idx++] = i;

    TALER_cs_derive_only_cs_blind_nonces_from_seed (&wd->blinding_seed,
                                                    false, /* not for melt */
                                                    cs_count,
                                                    cs_indices,
                                                    nonces);

    for (uint8_t gamma = 0; gamma<TALER_CNC_KAPPA; gamma++)
    {
      if (gamma == wd->noreveal_index)
      {
        /**
         * For the disclosed index, all we have to do is to accumulate the hash
         * of the selected coins
         */
        GNUNET_CRYPTO_hash_context_read (
          hash_context,
          &wd->selected_h,
          sizeof(wd->selected_h));
      }
      else
      {
        /**
         * For the non-disclosed index, we have to generate the planchets in
         * this batch and calculate their hash
         */
        struct GNUNET_HashContext *batch_ctx;
        struct TALER_BlindedCoinHashP batch_h;
        cs_idx = 0;

        batch_ctx = GNUNET_CRYPTO_hash_context_start ();
        GNUNET_assert (NULL != batch_ctx);

        /* Expand the secrets for a disclosed batch */
        GNUNET_assert (secrets_idx < (TALER_CNC_KAPPA - 1));
        TALER_withdraw_expand_secrets (
          wd->num_coins,
          &disclosed_batch_seeds->tuple[secrets_idx],
          secrets[secrets_idx]);

        /**
         * Now individually create each coin in this batch and calculate
         * its hash, and accumulate the hash of the batch with it
         */
        for (size_t coin_idx = 0; coin_idx < wd->num_coins; coin_idx++)
        {
          struct TALER_BlindedCoinHashP bch;
          struct GNUNET_CRYPTO_CSPublicRPairP *rp;
          union GNUNET_CRYPTO_BlindSessionNonce *np;

          if (is_cs[coin_idx])
          {
            GNUNET_assert (cs_idx < cs_count);
            np = &nonces[cs_idx];
            rp = &wd->cs_r_values[cs_idx];
            cs_idx++;
          }
          else
          {
            np = NULL;
            rp = NULL;
          }
          ret = calculate_blinded_hash (con,
                                        denom_keys[coin_idx],
                                        &secrets[secrets_idx][coin_idx],
                                        rp,
                                        np,
                                        wd->max_age,
                                        &bch,
                                        result);
          if (GNUNET_OK != ret)
          {
            GNUNET_CRYPTO_hash_context_abort (hash_context);
            return GNUNET_SYSERR;
          }
          /**
           * Continue the running hash of all coin hashes in the batch
           * with the calculated hash-value of the current, disclosed coin
           */
          GNUNET_CRYPTO_hash_context_read (batch_ctx,
                                           &bch,
                                           sizeof(bch));
        }
        /**
         * Finalize the hash of this batch and add it to
         * the total hash
         */
        GNUNET_CRYPTO_hash_context_finish (
          batch_ctx,
          &batch_h.hash);
        GNUNET_CRYPTO_hash_context_read (
          hash_context,
          &batch_h,
          sizeof(batch_h));

        secrets_idx++;
      }
    }
  }

  /* Finally, compare the calculated hash with the original wd */
  {
    struct TALER_HashBlindedPlanchetsP planchets_h;

    GNUNET_CRYPTO_hash_context_finish (
      hash_context,
      &planchets_h.hash);

    if (0 != GNUNET_CRYPTO_hash_cmp (
          &wd->planchets_h.hash,
          &planchets_h.hash))
    {
      GNUNET_break_op (0);
      *result = TALER_MHD_reply_with_ec (con,
                                         TALER_EC_EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH,
                                         NULL);
      return GNUNET_SYSERR;
    }

  }
  return GNUNET_OK;
}


/**
 * @brief Send a response for "/reveal-withdraw"
 *
 * @param connection The http connection to the client to send the response to
 * @param commitment The data from the commitment with signatures
 * @return a MHD result code
 */
static MHD_RESULT
reply_withdraw_reveal_success (
  struct MHD_Connection *connection,
  const struct TALER_EXCHANGEDB_Withdraw *commitment)
{
  json_t *list = json_array ();

  GNUNET_assert (NULL != list);
  for (size_t i = 0; i < commitment->num_coins; i++)
  {
    json_t *obj = GNUNET_JSON_PACK (
      TALER_JSON_pack_blinded_denom_sig (NULL,
                                         &commitment->denom_sigs[i]));
    GNUNET_assert (0 ==
                   json_array_append_new (list,
                                          obj));
  }

  return TALER_MHD_REPLY_JSON_PACK (
    connection,
    MHD_HTTP_OK,
    GNUNET_JSON_pack_array_steal ("ev_sigs",
                                  list));
}


MHD_RESULT
TEH_handler_reveal_withdraw (
  struct TEH_RequestContext *rc,
  const json_t *root,
  const char *const args[0])
{
  MHD_RESULT result = MHD_NO;
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
  struct WithdrawRevealContext actx = {0};
  const json_t *j_disclosed_batch_seeds;
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("planchets_h",
                                 &actx.planchets_h),
    GNUNET_JSON_spec_array_const ("disclosed_batch_seeds",
                                  &j_disclosed_batch_seeds),
    GNUNET_JSON_spec_end ()
  };

  (void) args;
  /* Parse JSON body*/
  ret = TALER_MHD_parse_json_data (rc->connection,
                                   root,
                                   spec);
  if (GNUNET_OK != ret)
  {
    GNUNET_break_op (0);
    return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES;
  }

  do {
    /* Extract denominations, blinded and disclosed coins */
    if (GNUNET_OK !=
        parse_withdraw_reveal_json (
          rc->connection,
          j_disclosed_batch_seeds,
          &actx,
          &result))
      break;

    /* Find original commitment */
    if (GNUNET_OK !=
        find_original_withdraw (
          rc->connection,
          &actx.planchets_h,
          &actx.withdraw,
          &result))
      break;

    /* Verify the computed planchets_h equals the committed one and that coins
     * have a maximum age group corresponding max_age (age-mask dependent) */
    if (GNUNET_OK !=
        verify_commitment_and_max_age (
          rc->connection,
          &actx.withdraw,
          &actx.disclosed_batch_seeds,
          &result))
      break;

    /* Finally, return the signatures */
    result = reply_withdraw_reveal_success (rc->connection,
                                            &actx.withdraw);

  } while (0);

  GNUNET_JSON_parse_free (spec);
  if (NULL != actx.withdraw.denom_sigs)
  {
    for (size_t i = 0; i<actx.withdraw.num_coins; i++)
      TALER_blinded_denom_sig_free (&actx.withdraw.denom_sigs[i]);
    GNUNET_free (actx.withdraw.denom_sigs);
  }
  GNUNET_free (actx.withdraw.cs_r_values);
  GNUNET_free (actx.withdraw.denom_pub_hashes);
  GNUNET_free (actx.withdraw.denom_serials);
  return result;
}


/* end of taler-exchange-httpd_reveal_withdraw.c */
