/*
  This file is part of Anastasis
  Copyright (C) 2020-2022 Anastasis SARL

  Anastasis 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.

  Anastasis 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
  Anastasis; see the file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file stasis/insert_challenge_code.c
 * @brief Anastasis database: insert challenge code
 * @author Christian Grothoff
 */
#include "platform.h"
#include "anastasis-db_pg.h"
#include "anastasis/anastasis-database/insert_challenge_code.h"
#include "anastasis/anastasis-database/transaction.h"
#include "anastasis/anastasis-database/preflight.h"
#include <taler/taler_pq_lib.h>


/**
 * Create a new challenge code for a given challenge identified by the challenge
 * public key. The function will first check if there is already a valid code
 * for this challenge present and won't insert a new one in this case.
 *
 * @param truth_uuid the identifier for the challenge
 * @param rotation_period for how long is the code available
 * @param validity_period for how long is the code available
 * @param retry_counter amount of retries allowed
 * @param[out] retransmission_date when to next retransmit
 * @param[out] code set to the code which will be checked for later
 * @return transaction status,
 *        #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if we are out of valid tries,
 *        #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if @a code is now in the DB
 */
enum GNUNET_DB_QueryStatus
ANASTASIS_DB_insert_challenge_code (
  const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
  struct GNUNET_TIME_Relative rotation_period,
  struct GNUNET_TIME_Relative validity_period,
  uint32_t retry_counter,
  struct GNUNET_TIME_Timestamp *retransmission_date,
  uint64_t *code)
{
  struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
  struct GNUNET_TIME_Timestamp expiration_date;
  struct GNUNET_TIME_Absolute ex_rot;

  PREPARE ("insert_challenge_code_select_active",
           "SELECT "
           " code"
           ",retry_counter"
           ",retransmission_date"
           " FROM anastasis_challengecode"
           " WHERE truth_uuid=$1"
           "   AND expiration_date > $2"
           "   AND creation_date > $3"
           " ORDER BY creation_date DESC"
           " LIMIT 1;");
  PREPARE ("insert_challenge_code_insert",
           "INSERT INTO anastasis_challengecode "
           "(truth_uuid"
           ",code"
           ",creation_date"
           ",expiration_date"
           ",retry_counter"
           ") VALUES "
           "($1, $2, $3, $4, $5);");
  expiration_date = GNUNET_TIME_relative_to_timestamp (validity_period);
  ex_rot = GNUNET_TIME_absolute_subtract (now.abs_time,
                                          rotation_period);
  for (unsigned int retries = 0; retries<MAX_RETRIES; retries++)
  {
    if (GNUNET_OK !=
        ANASTASIS_DB_start (
          "insert_challenge_code"))
    {
      GNUNET_break (0);
      return GNUNET_DB_STATUS_HARD_ERROR;
    }

    {
      uint32_t old_retry_counter;
      struct GNUNET_PQ_QueryParam params[] = {
        GNUNET_PQ_query_param_auto_from_type (truth_uuid),
        GNUNET_PQ_query_param_timestamp (&now),
        GNUNET_PQ_query_param_absolute_time (&ex_rot),
        GNUNET_PQ_query_param_end
      };
      struct GNUNET_PQ_ResultSpec rs[] = {
        GNUNET_PQ_result_spec_uint64 ("code",
                                      code),
        GNUNET_PQ_result_spec_uint32 ("retry_counter",
                                      &old_retry_counter),
        GNUNET_PQ_result_spec_timestamp ("retransmission_date",
                                         retransmission_date),
        GNUNET_PQ_result_spec_end
      };
      enum GNUNET_DB_QueryStatus qs;

      qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
                                                     "insert_challenge_code_select_active",
                                                     params,
                                                     rs);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        GNUNET_break (0);
        ANASTASIS_DB_rollback ();
        return qs;
      case GNUNET_DB_STATUS_SOFT_ERROR:
        goto retry;
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "No active challenge found, creating a fresh one\n");
        break;
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        if (0 == old_retry_counter)
        {
          ANASTASIS_DB_rollback ();
          GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                      "Active challenge %llu has zero tries left, refusing to create another one\n",
                      (unsigned long long) *code);
          return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
        }
        ANASTASIS_DB_rollback ();
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Active challenge has %u tries left, returning old challenge %llu\n",
                    (unsigned int) old_retry_counter,
                    (unsigned long long) *code);
        return qs;
      }
    }

    *code = GNUNET_CRYPTO_random_u64 (ANASTASIS_PIN_MAX_VALUE);
    *retransmission_date = GNUNET_TIME_UNIT_ZERO_TS;
    {
      struct GNUNET_PQ_QueryParam params[] = {
        GNUNET_PQ_query_param_auto_from_type (truth_uuid),
        GNUNET_PQ_query_param_uint64 (code),
        GNUNET_PQ_query_param_timestamp (&now),
        GNUNET_PQ_query_param_timestamp (&expiration_date),
        GNUNET_PQ_query_param_uint32 (&retry_counter),
        GNUNET_PQ_query_param_end
      };
      enum GNUNET_DB_QueryStatus qs;

      qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
                                               "insert_challenge_code_insert",
                                               params);
      switch (qs)
      {
      case GNUNET_DB_STATUS_HARD_ERROR:
        ANASTASIS_DB_rollback ();
        return GNUNET_DB_STATUS_HARD_ERROR;
      case GNUNET_DB_STATUS_SOFT_ERROR:
        goto retry;
      case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
        GNUNET_break (0);
        ANASTASIS_DB_rollback ();
        return GNUNET_DB_STATUS_HARD_ERROR;
      case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Created fresh challenge with %u tries left\n",
                    (unsigned int) retry_counter);
        break;
      }
    }

    {
      enum GNUNET_DB_QueryStatus qs;

      qs = ANASTASIS_DB_commit ();
      if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
        goto retry;
      if (qs < 0)
        return qs;
    }
    return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
retry:
    ANASTASIS_DB_rollback ();
  }
  return GNUNET_DB_STATUS_SOFT_ERROR;
}


/* end of insert_challenge_code.c */
