/*
  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");
  qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
                                           "do_commit",
                                           no_params);
  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 */
