/*
  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-melt.c
 * @brief Handle /reveal-melt 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-melt.h"
#include "taler-exchange-httpd_responses.h"
#include "taler-exchange-httpd_keys.h"

#define KAPPA_MINUS_1  (TALER_CNC_KAPPA - 1)
/**
 * State for an /reveal-melt operation.
 */
struct MeltRevealContext
{

  /**
   * Commitment for the melt operation, previously called by the
   * client.
   */
  struct TALER_RefreshCommitmentP rc;

  /**
   * The data from the original melt.  Will be retrieved from
   * the DB via @a rc.
   */
  struct TALER_EXCHANGEDB_Refresh_v27 refresh;

  /**
   * TALER_CNC_KAPPA-1 disclosed signatures for public refresh nonces.
   */
  struct TALER_PrivateRefreshNonceSignatureP signatures[KAPPA_MINUS_1];

  /**
   * False, if no age commitment was provided
   */
  bool no_age_commitment;

  /**
   * If @e no_age_commitment is false, the age commitment of
   * the old coin.  Needed to ensure that the age commitment
   * is applied correctly to the fresh coins.
   */
  struct TALER_AgeCommitment age_commitment;
};


/**
 * Check if the request belongs to an existing refresh request.
 * If so, sets the refresh object with the request data.
 * Otherwise, it queues an appropriate MHD response.
 *
 * @param connection The HTTP connection to the client
 * @param rc Original commitment value sent with the melt request
 * @param[out] refresh Data from the original refresh 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 refresh request has been found,
 *   #GNUNET_SYSERR if we did not find the request in the DB
 */
static enum GNUNET_GenericReturnValue
find_original_refresh (
  struct MHD_Connection *connection,
  const struct TALER_RefreshCommitmentP *rc,
  struct TALER_EXCHANGEDB_Refresh_v27 *refresh,
  MHD_RESULT *result)
{
  enum GNUNET_DB_QueryStatus qs;

  for (unsigned int retry = 0; retry < 3; retry++)
  {
    qs = TEH_plugin->get_refresh (TEH_plugin->cls,
                                  rc,
                                  refresh);
    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_REFRESHES_REVEAL_SESSION_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_refresh");
      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*/
  *result = TALER_MHD_reply_with_ec (connection,
                                     TALER_EC_GENERIC_DB_FETCH_FAILED,
                                     "get_refresh");
  return GNUNET_SYSERR;
}


/**
 * Verify that the age commitment is sound, that is, if the
 * previous /melt provided a hash, ensure we have the corresponding
 * age commitment.  Or not, if it wasn't provided.
 *
 * @param connection The MHD connection to handle
 * @param actx The context of the operation, only partially built at this time
 * @param[out] mhd_ret The result if a reply is queued for MHD
 * @return GNUNET_OK on success, otherwise a reply is queued for MHD
 */
static enum GNUNET_GenericReturnValue
compare_age_commitment (
  struct MHD_Connection *connection,
  struct MeltRevealContext *actx,
  MHD_RESULT *mhd_ret)
{
  if (actx->no_age_commitment !=
      actx->refresh.coin.no_age_commitment)
  {
    *mhd_ret = TALER_MHD_reply_with_ec (connection,
                                        TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID,
                                        NULL);
    return GNUNET_SYSERR;
  }
  if (! actx->no_age_commitment)
  {
    struct TALER_AgeCommitmentHash ach;

    actx->age_commitment.mask = TEH_age_restriction_config.mask;
    TALER_age_commitment_hash (
      &actx->age_commitment,
      &ach);
    if (0 != GNUNET_memcmp (
          &actx->refresh.coin.h_age_commitment,
          &ach))
    {
      GNUNET_break_op (0);
      *mhd_ret = TALER_MHD_reply_with_ec (connection,
                                          TALER_EC_EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID,
                                          NULL);
      return GNUNET_SYSERR;
    }
  }
  return GNUNET_OK;
}


/**
 * @brief Derives an planchet from a given input and returns
 * blinded planchets detail
 *
 * @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 old_age_commitment The age commitment of the old coin, might be NULL
 * @param[out] detail planchet detail to write  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_detail (
  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,
  const struct TALER_AgeCommitment *old_age_commitment,
  struct TALER_PlanchetDetail *detail,
  MHD_RESULT *result)
{
  enum GNUNET_GenericReturnValue ret;
  struct TALER_AgeCommitmentHash ach;
  bool no_age_commitment = (NULL == old_age_commitment);

  /* calculate age commitment hash */
  if (! no_age_commitment)
  {
    struct TALER_AgeCommitment nac;

    TALER_age_commitment_derive_from_secret (old_age_commitment,
                                             secret,
                                             &nac);
    TALER_age_commitment_hash (&nac,
                               &ach);
    TALER_age_commitment_free (&nac);
  }

  /* Next: calculate planchet */
  {
    struct TALER_CoinPubHashP c_hash;
    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 blinding_values = {
      .blinding_inputs = &bi
    };

    switch (bi.cipher)
    {
    case GNUNET_CRYPTO_BSA_CS:
      GNUNET_assert (NULL != r_pub);
      GNUNET_assert (NULL != nonce);
      bi.details.cs_values = *r_pub;
      break;
    case GNUNET_CRYPTO_BSA_RSA:
      break;
    default:
      GNUNET_assert (0);
    }

    TALER_planchet_blinding_secret_create (secret,
                                           &blinding_values,
                                           &bks);
    TALER_planchet_setup_coin_priv (secret,
                                    &blinding_values,
                                    &coin_priv);
    ret = TALER_planchet_prepare (&denom_key->denom_pub,
                                  &blinding_values,
                                  &bks,
                                  nonce,
                                  &coin_priv,
                                  no_age_commitment
                                     ? NULL
                                     : &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;
    }
  }
  return ret;
}


/**
 * @brief Checks the validity of the disclosed signatures as follows:
 * - Verifies the validity of the disclosed signatures with the old coin's public key
 * - Derives the seeds from those signatures for disclosed fresh coins
 * - Derives the fresh coins from the seeds
 * - Derives new age commitment
 * - Calculates the blinded coin planchet hashes
 * - Calculates the refresh commitment from above data
 * - Compares the calculated commitment with existing one
 *
 * The derivation of a fresh coin from the old coin is defined in
 * https://docs.taler.net/design-documents/062-pq-refresh.html
 *
 * The derivation of age-commitment from a coin's age-commitment
 * https://docs.taler.net/design-documents/024-age-restriction.html#melt
 *
 * @param con HTTP-connection to the client
 * @param rf Original refresh object from the previous /melt request
 * @param old_age_commitment The age commitment of the original coin
 * @param signatures The secrets of the disclosed coins, KAPPA_MINUS_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 (
  struct MHD_Connection *con,
  const struct TALER_EXCHANGEDB_Refresh_v27 *rf,
  const struct TALER_AgeCommitment *old_age_commitment,
  const struct TALER_PrivateRefreshNonceSignatureP signatures[KAPPA_MINUS_1],
  MHD_RESULT *result)
{
  enum GNUNET_GenericReturnValue ret;
  struct TEH_KeyStateHandle *keys;
  struct TEH_DenominationKey *denom_keys[rf->num_coins];
  struct TALER_DenominationHashP  *denoms_h[rf->num_coins];
  struct TALER_Amount total_amount;
  struct TALER_Amount total_fee;
  struct TALER_KappaPublicRefreshNoncesP kappa_nonces;
  bool is_cs[rf->num_coins];
  size_t cs_count = 0;

  GNUNET_assert (rf->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 from the original request to /melt
   * and keep track of those of type CS.
   */
  for (size_t i = 0; i < rf->num_coins; i++)
  {
    denom_keys[i] =
      TEH_keys_denomination_by_serial_from_state (
        keys,
        rf->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.refresh));

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

    /* Remember the hash of the public key of the denomination for later */
    denoms_h[i] = &denom_keys[i]->h_denom_pub;
  }

  /**
   * Sanity check:
   * The number CS denominations must match those from the /melt request
   */
  if (cs_count != rf->num_cs_r_values)
  {
    GNUNET_break (0);
    *result = TALER_MHD_reply_with_ec (con,
                                       TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
                                       NULL);
    return GNUNET_SYSERR;
  }

  /**
   * We expand the provided refresh_seed from the original call to /melt,
   * into kappa many batch seeds, from which we will later use all except the
   * noreveal_index one.
   */
  TALER_refresh_expand_kappa_nonces (
    &rf->refresh_seed,
    &kappa_nonces);

  /**
   * First things first: Verify the signature of the old coin
   * over the refresh nonce.  This proves the ownership
   * for the fresh coin.
   */
  {
    size_t sig_idx = 0;

    for (uint8_t k=0; k < TALER_CNC_KAPPA; k++)
    {
      if (rf->noreveal_index == k)
        continue;
      if (GNUNET_OK !=
          TALER_wallet_refresh_nonce_verify (
            &rf->coin.coin_pub,
            &kappa_nonces.tuple[k],
            rf->num_coins,
            denoms_h,
            k,
            &signatures[sig_idx++]))
      {
        GNUNET_break_op (0);
        *result = TALER_MHD_reply_with_ec (con,
                                           TALER_EC_EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID,
                                           NULL);
        return GNUNET_SYSERR;
      }
    }
  }

  /**
   * In the following scope, we start collecting blinded coin planchet hashes,
   * either those persisted from the original request to /melt, or we
   * derive and calculate them from the provided signatures, after having
   * verified that each of them was signed by the old coin's private key.
   *
   * After collecting the blinded coin planchet hashes, we can then get
   * the commitment for the calculated values and compare the result with
   * the commitment from the /melt request.
   */
  {
    struct TALER_KappaHashBlindedPlanchetsP kappa_planchets_h;
    union GNUNET_CRYPTO_BlindSessionNonce b_nonces[GNUNET_NZL (cs_count)];
    size_t cs_idx = 0; /* [0...cs_count) */
    uint8_t sig_idx = 0; /* [0..KAPPA_MINUS_1) */

    /**
     * First, derive the blinding nonces for the CS denominations all at once.
     */
    if (0 < cs_count)
    {
      uint32_t cs_indices[cs_count];
      size_t idx = 0; /* [0...cs_count) */

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

      TALER_cs_derive_only_cs_blind_nonces_from_seed (&rf->blinding_seed,
                                                      true, /* for melt */
                                                      cs_count,
                                                      cs_indices,
                                                      b_nonces);
    }
    /**
     * We handle the kappa batches of rf->num_coins depths first.
     */
    for (uint8_t k = 0; k<TALER_CNC_KAPPA; k++)
    {
      if (k ==  rf->noreveal_index)
      {
        /**
         * We take the stored value for the hash of selected batch
         */
        kappa_planchets_h.tuple[k] = rf->selected_h;
      }
      else
      {
        /**
         * We have to generate all the planchets' details from
         * the disclosed input material and generate the
         * hashes of them.
         */
        struct TALER_PlanchetMasterSecretP secrets[rf->num_coins];
        struct TALER_PlanchetDetail details[rf->num_coins];

        memset (secrets,
                0,
                sizeof(secrets));
        memset (details,
                0,
                sizeof(details));
        /**
         * Expand from the k-th signature all num_coin planchet secrets,
         * except for the noreveal_index.
         */
        GNUNET_assert (sig_idx < KAPPA_MINUS_1);
        TALER_refresh_signature_to_secrets (
          &signatures[sig_idx++],
          rf->num_coins,
          secrets);
        /**
         * Reset the index for the CS  denominations.
         */
        cs_idx = 0;

        for (size_t coin_idx = 0; coin_idx < rf->num_coins; coin_idx++)
        {
          struct GNUNET_CRYPTO_CSPublicRPairP *rp;
          union GNUNET_CRYPTO_BlindSessionNonce *np;

          if (is_cs[coin_idx])
          {
            GNUNET_assert (cs_idx < cs_count);
            np = &b_nonces[cs_idx];
            rp = &rf->cs_r_values[cs_idx];
            cs_idx++;
          }
          else
          {
            np = NULL;
            rp = NULL;
          }
          ret = calculate_blinded_detail (con,
                                          denom_keys[coin_idx],
                                          &secrets[coin_idx],
                                          rp,
                                          np,
                                          old_age_commitment,
                                          &details[coin_idx],
                                          result);
          if (GNUNET_OK != ret)
            return GNUNET_SYSERR;
        }
        /**
         * Now we can generate the hashes for the kappa-th batch of coins
         */
        TALER_wallet_blinded_planchet_details_hash (
          rf->num_coins,
          details,
          &kappa_planchets_h.tuple[k]);

        for (size_t i =0; i<rf->num_coins; i++)
          TALER_planchet_detail_free (&details[i]);
      }
    }
    /**
     * Finally, calculate the refresh commitment and compare it with the original.
     */
    {
      struct TALER_RefreshCommitmentP rc;

      TALER_refresh_get_commitment_v27 (&rc,
                                        &rf->refresh_seed,
                                        rf->no_blinding_seed
                                            ? NULL
                                            : &rf->blinding_seed,
                                        &kappa_planchets_h,
                                        &rf->coin.coin_pub,
                                        &rf->amount_with_fee);
      if (0 != GNUNET_CRYPTO_hash_cmp (
            &rf->rc.session_hash,
            &rc.session_hash))
      {
        GNUNET_break_op (0);
        *result = TALER_MHD_reply_with_ec (con,
                                           TALER_EC_EXCHANGE_REFRESHES_REVEAL_INVALID_RCH,
                                           NULL);
        return GNUNET_SYSERR;
      }
    }
  }
  return GNUNET_OK;
}


/**
 * @brief Send a response for "/reveal-melt"
 *
 * @param connection The http connection to the client to send the response to
 * @param refresh The data from the previous call to /melt with signatures
 * @return a MHD result code
 */
static MHD_RESULT
reply_melt_reveal_success (
  struct MHD_Connection *connection,
  const struct TALER_EXCHANGEDB_Refresh_v27 *refresh)
{
  json_t *list = json_array ();
  GNUNET_assert (NULL != list);

  for (unsigned int i = 0; i < refresh->num_coins; i++)
  {
    json_t *obj = GNUNET_JSON_PACK (
      TALER_JSON_pack_blinded_denom_sig (NULL,
                                         &refresh->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_melt (
  struct TEH_RequestContext *rc,
  const json_t *root,
  const char *const args[2])
{
  MHD_RESULT result = MHD_NO;
  enum GNUNET_GenericReturnValue ret = GNUNET_SYSERR;
  struct MeltRevealContext actx = {0};
  struct GNUNET_JSON_Specification tuple[] =     {
    GNUNET_JSON_spec_fixed_auto (NULL,
                                 &actx.signatures[0]),
    GNUNET_JSON_spec_fixed_auto (NULL,
                                 &actx.signatures[1]),
    GNUNET_JSON_spec_end ()
  };
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("rc",
                                 &actx.rc),
    TALER_JSON_spec_tuple_of ("signatures",
                              tuple),
    GNUNET_JSON_spec_mark_optional (
      TALER_JSON_spec_age_commitment ("age_commitment",
                                      &actx.age_commitment),
      &actx.no_age_commitment),
    GNUNET_JSON_spec_end ()
  };

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

  /* 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;
  }

  (void) args;

  do {
    /* Find original commitment */
    if (GNUNET_OK !=
        find_original_refresh (
          rc->connection,
          &actx.rc,
          &actx.refresh,
          &result))
      break;

    /* Compare age commitment with the hash from the /melt request, if present */
    if (GNUNET_OK !=
        compare_age_commitment (
          rc->connection,
          &actx,
          &result))
      break;

    /* verify the commitment  */
    if (GNUNET_OK !=
        verify_commitment (
          rc->connection,
          &actx.refresh,
          actx.no_age_commitment
                ? NULL
                : &actx.age_commitment,
          actx.signatures,
          &result))
      break;

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

  } while (0);

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


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