/*
  This file is part of Anastasis
  Copyright (C) 2020, 2021, 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/anastasis-db_pg.c
 * @brief shared database state and helpers for the Anastasis postgres backend
 * @author Christian Grothoff
 * @author Marcello Stanisci
 */
#include "platform.h"
#include "anastasis-db_pg.h"
#include "anastasis/anastasis-database/preflight.h"
#include "anastasis/anastasis-database/transaction.h"
#include "anastasis/anastasis-database/event.h"
#include <taler/taler_pq_lib.h>


/**
 * Prepared statements have been initialized in this edition.
 */
uint64_t ANASTASIS_DB_prep_gen_;

/**
 * Global database state.
 */
struct PostgresClosure *pg;


enum GNUNET_GenericReturnValue
ANASTASIS_DB_start (const char *name)
{
  struct GNUNET_PQ_ExecuteStatement es[] = {
    GNUNET_PQ_make_execute ("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };

  GNUNET_break (GNUNET_OK ==
                ANASTASIS_DB_preflight ());
  pg->transaction_name = name;
  if (GNUNET_OK !=
      GNUNET_PQ_exec_statements (pg->conn,
                                 es))
  {
    TALER_LOG_ERROR ("Failed to start transaction\n");
    GNUNET_break (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


void
ANASTASIS_DB_rollback (void)
{
  struct GNUNET_PQ_ExecuteStatement es[] = {
    GNUNET_PQ_make_execute ("ROLLBACK"),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };

  if (GNUNET_OK !=
      GNUNET_PQ_exec_statements (pg->conn,
                                 es))
  {
    TALER_LOG_ERROR ("Failed to rollback transaction\n");
    GNUNET_break (0);
  }
  pg->transaction_name = NULL;
}


enum GNUNET_DB_QueryStatus
ANASTASIS_DB_commit (void)
{
  enum GNUNET_DB_QueryStatus qs;
  struct GNUNET_PQ_QueryParam no_params[] = {
    GNUNET_PQ_query_param_end
  };

  PREPARE ("do_commit",
           "COMMIT");
  {
    PGresult *result;

    result = GNUNET_PQ_exec_prepared (pg->conn,
                                      "do_commit",
                                      no_params);
    qs = GNUNET_PQ_eval_result (pg->conn,
                                "do_commit",
                                result);
    /* PostgreSQL accepts COMMIT on a transaction it has already aborted: it
       silently rolls the transaction back and answers PGRES_COMMAND_OK with the
       command tag ROLLBACK.  GNUNET_PQ_eval_result() cannot tell that apart from
       a real commit, so every caller took the success path for a transaction
       whose writes are gone.  Report it as a soft error instead: sessions run
       SERIALIZABLE, so the overwhelmingly likely cause is an unhandled
       40001/40P01 for which retrying is right; a caller with a retry loop gives
       up after MAX_RETRIES and fails visibly, one without fails immediately.

       The *success* path deliberately keeps returning exactly what
       GNUNET_PQ_eval_prepared_non_select() returned before.  That function is
       GNUNET_PQ_exec_prepared() + GNUNET_PQ_eval_result() plus a
       strtol(PQcmdTuples()) step that only runs when the status is already
       GNUNET_DB_STATUS_SUCCESS_NO_RESULTS, and PQcmdTuples() on a COMMIT command
       tag is the empty string -- so that step yields 0, which is
       GNUNET_DB_STATUS_SUCCESS_NO_RESULTS again. */
    if ( (0 <= qs) &&
         (NULL != result) &&
         (0 == strcmp ("ROLLBACK",
                       PQcmdStatus (result))) )
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Transaction `%s' was rolled back by the database instead of committed; a previous statement must have failed without being handled\n",
                  pg->transaction_name);
      qs = GNUNET_DB_STATUS_SOFT_ERROR;
    }
    PQclear (result);
  }
  pg->transaction_name = NULL;
  return qs;
}


struct GNUNET_DB_EventHandler *
ANASTASIS_DB_event_listen (const struct GNUNET_DB_EventHeaderP *es,
                           struct GNUNET_TIME_Relative timeout,
                           GNUNET_DB_EventCallback cb,
                           void *cb_cls)
{
  return GNUNET_PQ_event_listen (pg->conn,
                                 es,
                                 timeout,
                                 cb,
                                 cb_cls);
}


void
ANASTASIS_DB_event_listen_cancel (struct GNUNET_DB_EventHandler *eh)
{
  GNUNET_PQ_event_listen_cancel (eh);
}


void
ANASTASIS_DB_event_notify (const struct GNUNET_DB_EventHeaderP *es,
                           const void *extra,
                           size_t extra_size)
{
  GNUNET_PQ_event_notify (pg->conn,
                          es,
                          extra,
                          extra_size);
}


/**
 * Function called each time we connect or reconnect to the
 * database. Gives the application a chance to run some
 * per-connection initialization logic.
 *
 * @param cls unused, NULL
 * @param pq database connection handle
 */
static void
reconnect_cb (void *cls,
              struct GNUNET_PQ_Context *pq)
{
#if AUTO_EXPLAIN
  /* Enable verbose logging to see where queries do not
     properly use indices */
  struct GNUNET_PQ_ExecuteStatement es[] = {
    GNUNET_PQ_make_try_execute ("LOAD 'auto_explain';"),
    GNUNET_PQ_make_try_execute ("SET auto_explain.log_min_duration=50;"),
    GNUNET_PQ_make_try_execute ("SET auto_explain.log_timing=TRUE;"),
    GNUNET_PQ_make_try_execute ("SET auto_explain.log_analyze=TRUE;"),
    /* https://wiki.postgresql.org/wiki/Serializable suggests to really
       force the default to 'serializable' if SSI is to be used. */
    GNUNET_PQ_make_try_execute (
      "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;"),
    GNUNET_PQ_make_try_execute ("SET enable_sort=OFF;"),
    GNUNET_PQ_make_try_execute ("SET enable_seqscan=OFF;"),
    GNUNET_PQ_make_execute ("SET search_path TO anastasis;"),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };
#else
  struct GNUNET_PQ_ExecuteStatement es[] = {
    GNUNET_PQ_make_execute ("SET search_path TO anastasis;"),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };
#endif

  (void) cls;
  if (GNUNET_OK !=
      GNUNET_PQ_exec_statements (pq,
                                 es))
  {
    GNUNET_break (0);
    return;
  }
  ANASTASIS_DB_prep_gen_++;
}


/**
 * Initialize the database connection.
 *
 * @param cfg configuration to use
 * @param check_current true to check if the database schema is current
 * @return NULL on failure
 */
static enum GNUNET_GenericReturnValue
do_init (const struct GNUNET_CONFIGURATION_Handle *cfg,
         bool check_current)
{
  pg = GNUNET_new (struct PostgresClosure);
  pg->cfg = cfg;
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "anastasis",
                                             "CURRENCY",
                                             &pg->currency))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "anastasis",
                               "CURRENCY");
    GNUNET_free (pg);
    return GNUNET_SYSERR;
  }
  pg->conn = GNUNET_PQ_init (pg->cfg,
                             "stasis-postgres",
                             &reconnect_cb,
                             pg);
  if (NULL == pg->conn)
  {
    GNUNET_break (0);
    ANASTASIS_DB_fini ();
    return GNUNET_SYSERR;
  }
  if (check_current &&
      (GNUNET_OK !=
       GNUNET_PQ_check_current (pg->conn,
                                "stasis-")) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Database schema is not up-to-date. Try running anastasis-dbinit or anastasis-dbconfig!\n");
    ANASTASIS_DB_fini ();
    return GNUNET_NO;
  }
  return GNUNET_OK;
}


enum GNUNET_GenericReturnValue
ANASTASIS_DB_init (const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  return do_init (cfg,
                  true);
}


enum GNUNET_GenericReturnValue
ANASTASIS_DB_init_admin (const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  return do_init (cfg,
                  false);
}


void
ANASTASIS_DB_fini (void)
{
  if (NULL == pg)
    return;
  if (NULL != pg->conn)
    GNUNET_PQ_disconnect (pg->conn);
  GNUNET_free (pg->currency);
  GNUNET_free (pg);
}


/* end of anastasis-db_pg.c */
