/*
  This file is part of TALER
  Copyright (C) 2024, 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 donau-httpd_batch-issue.c
 * @brief Handle request to issue receipts.
 * @author Lukas Matyja
 */
#include <donau_config.h>
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_json_lib.h>
#include <jansson.h>
#include <microhttpd.h>
#include <pthread.h>
#include <taler/taler_json_lib.h>
#include <taler/taler_mhd_lib.h>
#include <taler/taler_signatures.h>
#include "donaudb_plugin.h"
#include "donau-httpd_batch-issue.h"
#include "donau-httpd_db.h"
#include "donau_json_lib.h"
#include "donau-httpd_keys.h"


/**
 * Parse a bkp encoded in JSON.
 *
 * @param[out] bkp where to return the result
 * @param bkp_key_obj json to parse
 * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if @a bkp_key_obj
 * is malformed.
 */
static enum GNUNET_GenericReturnValue
parse_json_bkp (struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkp,
                const json_t *bkp_key_obj)
{
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("h_donation_unit_pub",
                                 &bkp->h_donation_unit_pub),
    DONAU_JSON_spec_blinded_donation_identifier ("blinded_udi",
                                                 &bkp->blinded_udi),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (bkp_key_obj,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  /* FIXME: Check for duplicate blinded UDIs.*/
  return GNUNET_OK;
}


/**
 * Free @a bkps array.
 *
 * @param num_bkps length of the array
 * @param[in] bkps array to release
 */
static void
free_bkps (size_t num_bkps,
           struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkps)
{
  for (unsigned int i = 0; i<num_bkps; i++)
  {
    struct DONAU_BlindedUniqueDonorIdentifier *budi = &bkps[i].blinded_udi;

    if (NULL != budi->blinded_message)
      GNUNET_CRYPTO_blinded_message_decref (budi->blinded_message);
  }
  GNUNET_free (bkps);
}


/**
 * Parse signatures to JSON.
 *
 * @param num_sig number of signatures
 * @param signatures Blinded donation unit signatures
 * @param[out] j_signatures JSON object
 * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if we could not parse
 * is malformed.
 */
static void
signatures_to_json (const size_t num_sig,
                    struct DONAU_BlindedDonationUnitSignature *signatures,
                    json_t *j_signatures)
{
  for (size_t i = 0; i < num_sig; i++)
  {
    struct DONAU_BlindedDonationUnitSignature *signature = &signatures[i];

    GNUNET_assert (
      0 == json_array_append_new (
        j_signatures,
        GNUNET_JSON_PACK (
          DONAU_JSON_pack_blinded_donation_unit_sig ("blinded_signature",
                                                     signature))));
  }
}


MHD_RESULT
DH_handler_issue_receipts_post (struct DH_RequestContext *rc,
                                const json_t *root,
                                const char *const args[1])
{
  struct DONAU_CharitySignatureP charity_sig;
  uint64_t year;
  bool second_time = false;
  unsigned long long charity_id;
  char dummy;
  const json_t *budikeypairs;
  size_t num_bkps;
  struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkps;
  struct DONAUDB_CharityMetaData charity_meta;
  json_t *blind_signatures;
  struct DONAU_DonationReceiptHashP h_receipts;
  struct TALER_Amount receipts_sum;

  if ( (NULL == args[0]) ||
       (1 != sscanf (args[0],
                     "%llu%c",
                     &charity_id,
                     &dummy)) )
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (rc->connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "charity_id");
  }
  GNUNET_log (GNUNET_ERROR_TYPE_INFO,
              "issue receipts for charity id: %llu\n",
              charity_id);

  {
    struct GNUNET_JSON_Specification spec[] = {
      GNUNET_JSON_spec_array_const ("budikeypairs",
                                    &budikeypairs),
      GNUNET_JSON_spec_fixed_auto ("charity_sig",
                                   &charity_sig),
      GNUNET_JSON_spec_uint64 ("year",
                               &year),
      GNUNET_JSON_spec_end ()
    };
    enum GNUNET_GenericReturnValue res;

    res = TALER_MHD_parse_json_data (rc->connection,
                                     root,
                                     spec);
    if (GNUNET_SYSERR == res)
      return MHD_NO; /* hard failure */
    if (GNUNET_NO == res)
    {
      GNUNET_break_op (0);
      return MHD_YES; /* failure */
    }
  }

  /* parse the budikeypairs array */
  num_bkps = json_array_size (budikeypairs);
  if (0 == num_bkps)
  {
    GNUNET_break_op (0);
    return TALER_MHD_reply_with_error (rc->connection,
                                       MHD_HTTP_BAD_REQUEST,
                                       TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                       "budikeypairs");

  }

  bkps = GNUNET_new_array (num_bkps,
                           struct DONAU_BlindedUniqueDonorIdentifierKeyPair);
  {
    json_t *bkp_obj;
    size_t index;

    json_array_foreach (budikeypairs,
                        index,
                        bkp_obj)
    {
      if (GNUNET_SYSERR ==
          parse_json_bkp (&bkps[index],
                          bkp_obj))
      {
        GNUNET_break_op (0);
        free_bkps (num_bkps,
                   bkps);
        return TALER_MHD_reply_with_error (rc->connection,
                                           MHD_HTTP_BAD_REQUEST,
                                           TALER_EC_GENERIC_PARAMETER_MALFORMED,
                                           "budikeypairs");
      }
    }
  }

  {
    enum GNUNET_DB_QueryStatus qs_charity;

    qs_charity = DH_plugin->lookup_charity (DH_plugin->cls,
                                            charity_id,
                                            &charity_meta);
    switch (qs_charity)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
    case GNUNET_DB_STATUS_SOFT_ERROR:
      GNUNET_break_op (0);
      free_bkps (num_bkps,
                 bkps);
      return TALER_MHD_reply_with_error (rc->connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
                                         NULL);
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      GNUNET_break_op (0);
      free_bkps (num_bkps,
                 bkps);
      return TALER_MHD_reply_with_error (
        rc->connection,
        MHD_HTTP_NOT_FOUND,
        TALER_EC_DONAU_CHARITY_NOT_FOUND,
        NULL);
      break;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      break;
    }
  }

  /* verify charity signature */
  if (GNUNET_OK !=
      DONAU_charity_bkp_verify (num_bkps,
                                bkps,
                                &charity_meta.charity_pub,
                                &charity_sig))
  {
    GNUNET_break_op (0);
    free_bkps (num_bkps,
               bkps);
    GNUNET_free (charity_meta.charity_name);
    GNUNET_free (charity_meta.charity_url);
    return TALER_MHD_reply_with_error (
      rc->connection,
      MHD_HTTP_FORBIDDEN,
      TALER_EC_DONAU_CHARITY_SIGNATURE_INVALID,
      "charity_sig");
  }

  GNUNET_free (charity_meta.charity_name);
  GNUNET_free (charity_meta.charity_url);
  {
    /* request already made? -> idempotent */
    enum GNUNET_DB_QueryStatus qs_check_receipts;
    struct DONAUDB_IssuedReceiptsMetaData check_receipts_meta;
    struct GNUNET_HashContext *hc;

    blind_signatures = json_array ();
    GNUNET_assert (NULL != blind_signatures);
    hc = GNUNET_CRYPTO_hash_context_start ();
    for (size_t i = 0; i < num_bkps; i++)
    {
      const struct GNUNET_CRYPTO_BlindedMessage *bm
        = bkps[i].blinded_udi.blinded_message;

      GNUNET_CRYPTO_hash_context_read (hc,
                                       &bkps[i].h_donation_unit_pub,
                                       sizeof (bkps[i].h_donation_unit_pub));
      switch (bm->cipher)
      {
      case GNUNET_CRYPTO_BSA_INVALID:
        GNUNET_assert (0);
        break;
      case GNUNET_CRYPTO_BSA_CS:
        GNUNET_CRYPTO_hash_context_read (
          hc,
          &bm->details.cs_blinded_message,
          sizeof (bm->details.cs_blinded_message));
        break;
      case GNUNET_CRYPTO_BSA_RSA:
        GNUNET_CRYPTO_hash_context_read (
          hc,
          bm->details.rsa_blinded_message.blinded_msg,
          bm->details.rsa_blinded_message.blinded_msg_size);
        break;
      }
    }
    GNUNET_CRYPTO_hash_context_read (hc,
                                     &charity_sig,
                                     sizeof (struct DONAU_CharitySignatureP));
    GNUNET_CRYPTO_hash_context_read (hc,
                                     &year,
                                     sizeof (uint64_t));
    GNUNET_CRYPTO_hash_context_finish (hc,
                                       &h_receipts.hash);

start:
    qs_check_receipts
      = DH_plugin->lookup_issued_receipts (DH_plugin->cls,
                                           &h_receipts,
                                           &check_receipts_meta);
    switch (qs_check_receipts)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
    case GNUNET_DB_STATUS_SOFT_ERROR:
      GNUNET_break (0);
      free_bkps (num_bkps,
                 bkps);
      return TALER_MHD_reply_with_error (rc->connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
                                         NULL);
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "request has not been made yet (first time)!\n");
      break; /* it's the first request from the charity, we can proceed */
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "request has been made already!\n");
      signatures_to_json (num_bkps,
                          check_receipts_meta.blinded_sigs,
                          blind_signatures);
      for (size_t i = 0; i < check_receipts_meta.num_sig; i++)
      {
        GNUNET_CRYPTO_blinded_sig_decref (
          check_receipts_meta.blinded_sigs[i].blinded_sig);
      }
      free_bkps (num_bkps,
                 bkps);
      return TALER_MHD_REPLY_JSON_PACK (
        rc->connection,
        MHD_HTTP_OK,
        TALER_JSON_pack_amount ("issued_amount",
                                &check_receipts_meta.amount),
        GNUNET_JSON_pack_array_steal ("blind_signatures",
                                      blind_signatures));
    }
  }

  /* calculate the sum of all receipts */

  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (DH_currency,
                                        &receipts_sum));
  for (size_t i = 0; i < num_bkps; i++)
  {
    struct DH_DonationUnitKey *dk;

    if (NULL == (dk = DH_keys_donation_unit_by_hash (
                   &bkps[i].h_donation_unit_pub)))
    {
      GNUNET_break_op (0);
      free_bkps (num_bkps,
                 bkps);
      return TALER_MHD_reply_with_error (rc->connection,
                                         MHD_HTTP_NOT_FOUND,
                                         TALER_EC_DONAU_GENERIC_KEYS_MISSING,
                                         NULL);
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "public key value: %lu.%u\n",
                dk->value.value,
                dk->value.fraction);
    GNUNET_assert (0 <= TALER_amount_add (&receipts_sum,
                                          &receipts_sum,
                                          &dk->value));
  }

  /* sign budis and send the signatures back */
  {
    struct DONAU_BlindedDonationUnitSignature du_sigs[num_bkps];
    struct DONAU_BkpSignData bkps_sign_data[num_bkps];
    enum TALER_ErrorCode batch_sign_ec;
    enum GNUNET_DB_QueryStatus qs_insert_ir;
    bool smaller_than_max_per_year = false;

    for (size_t i = 0; i < num_bkps; i++)
    {
      bkps_sign_data[i].h_donation_unit_pub = &bkps[i].h_donation_unit_pub;
      bkps_sign_data[i].budi = &bkps[i].blinded_udi;
    }
    batch_sign_ec = DH_keys_donation_unit_batch_sign (num_bkps,
                                                      bkps_sign_data,
                                                      du_sigs);
    if (TALER_EC_NONE != batch_sign_ec)
    {
      GNUNET_break_op (0);
      free_bkps (num_bkps,
                 bkps);
      return TALER_MHD_reply_with_error (rc->connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         batch_sign_ec,
                                         NULL);
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "made blind signatures!\n");
    free_bkps (num_bkps,
               bkps);

    /* save new receipts to date and save receipts Request (blinded signatures,
     * charity id, amount, hash over bkps) to make it idempotent*/
    qs_insert_ir = DH_plugin->insert_issued_receipt (
      DH_plugin->cls,
      num_bkps,
      du_sigs,
      (uint64_t) charity_id,
      &h_receipts,
      &receipts_sum,
      &smaller_than_max_per_year);
    switch (qs_insert_ir)
    {
    case GNUNET_DB_STATUS_HARD_ERROR:
    case GNUNET_DB_STATUS_SOFT_ERROR:
      GNUNET_break (0);
      return TALER_MHD_reply_with_error (rc->connection,
                                         MHD_HTTP_INTERNAL_SERVER_ERROR,
                                         TALER_EC_GENERIC_DB_FETCH_FAILED,
                                         NULL);
    case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
      GNUNET_assert (! second_time);
      second_time = true;
      goto start;
    case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
      if (! smaller_than_max_per_year)
      {
        GNUNET_break_op (0);
        return TALER_MHD_reply_with_error (rc->connection,
                                           MHD_HTTP_BAD_REQUEST,
                                           TALER_EC_DONAU_EXCEEDING_DONATION_LIMIT,
                                           NULL);
      }
      break;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "issue receipts request is saved! (idempotent)\n");

    signatures_to_json (num_bkps,
                        du_sigs,
                        blind_signatures);
    for (unsigned int i = 0; i<num_bkps; i++)
      GNUNET_CRYPTO_blinded_sig_decref (du_sigs[i].blinded_sig);
  }
  return TALER_MHD_REPLY_JSON_PACK (
    rc->connection,
    MHD_HTTP_OK,
    TALER_JSON_pack_amount ("issued_amount",
                            &receipts_sum),
    GNUNET_JSON_pack_array_steal ("blind_signatures",
                                  blind_signatures));
}


/* end of donau-httpd_batch-issue.c */
