/*
   This file is part of TALER
   Copyright (C) 2014--2025 Taler Systems SA

   TALER is free software; you can redistribute it and/or modify it under the
   terms of the GNU 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 General Public License for more details.

   You should have received a copy of the GNU General Public License along with
   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

/**
 * @file pg.c
 * @brief Low-level (statement-level) Postgres database access for the exchange
 * @author Florian Dold
 * @author Christian Grothoff
 * @author Sree Harsha Totakura
 * @author Marcello Stanisci
 * @author Özgür Kesim
 */
#include <poll.h>
#include <pthread.h>
#include <libpq-fe.h>
struct TALER_EXCHANGEDB_PostgresContext;
#define GNUNET_PQ_RECONNECT_CALLBACK_CLOSURE \
        struct TALER_EXCHANGEDB_PostgresContext
#include "helper.h"
#include "exchangedb_lib.h"
#include "exchange-database/preflight.h"

/**
 * Set to 1 to enable Postgres auto_explain module. This will
 * slow down things a _lot_, but also provide extensive logging
 * in the Postgres database logger for performance analysis.
 */
#define AUTO_EXPLAIN 0

/**
 * Counts how often we have established a fresh @e conn
 * to the database. Used to re-prepare statements.
 */
unsigned long long TEH_PG_prep_gen_;

/**
 * Function called each time we connect or reconnect to the
 * database. Gives the application a chance to run some
 * per-connection initialization logic.
 *
 * @param pg database conntext in the exchange
 * @param pq database connection handle
 */
static void
reconnect_cb (struct TALER_EXCHANGEDB_PostgresContext *pg,
              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_try_execute ("SET search_path TO exchange;"),
    /* Mergejoin causes issues, see Postgres #18380 */
    GNUNET_PQ_make_try_execute ("SET enable_mergejoin=OFF;"),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };
#else
  struct GNUNET_PQ_ExecuteStatement es[] = {
    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;"),
    /* Mergejoin causes issues, see Postgres #18380 */
    GNUNET_PQ_make_try_execute ("SET enable_mergejoin=OFF;"),
    GNUNET_PQ_make_try_execute ("SET search_path TO exchange;"),
    GNUNET_PQ_EXECUTE_STATEMENT_END
  };
#endif

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


/**
 * Connect to the db if the connection does not exist yet.
 *
 * @param[in,out] pg the database state
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
internal_setup (struct TALER_EXCHANGEDB_PostgresContext *pg)
{
  struct GNUNET_PQ_Context *db_conn;

  if (NULL != pg->conn)
    return GNUNET_OK;
  db_conn = GNUNET_PQ_init (pg->cfg,
                            "exchangedb-postgres",
                            &reconnect_cb,
                            pg);
  if (NULL == db_conn)
    return GNUNET_SYSERR;
  if (0 == TEH_PG_prep_gen_)
  {
    GNUNET_PQ_disconnect (db_conn);
    return GNUNET_SYSERR;
  }
  pg->conn = db_conn;
  return GNUNET_OK;
}


/**
 * 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 struct TALER_EXCHANGEDB_PostgresContext *
do_connect (const struct GNUNET_CONFIGURATION_Handle *cfg,
            bool check_current)
{
  struct TALER_EXCHANGEDB_PostgresContext *pg;
  unsigned long long dpl;

  pg = GNUNET_new (struct TALER_EXCHANGEDB_PostgresContext);
  pg->cfg = cfg;
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_filename (cfg,
                                               "exchangedb-postgres",
                                               "SQL_DIR",
                                               &pg->sql_dir))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "exchangedb-postgres",
                               "SQL_DIR");
    goto fail;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_string (cfg,
                                             "exchange",
                                             "BASE_URL",
                                             &pg->exchange_url))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "exchange",
                               "BASE_URL");
    goto fail;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "exchangedb",
                                           "IDLE_RESERVE_EXPIRATION_TIME",
                                           &pg->idle_reserve_expiration_time))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "exchangedb",
                               "IDLE_RESERVE_EXPIRATION_TIME");
    goto fail;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "exchangedb",
                                           "MAX_AML_PROGRAM_RUNTIME",
                                           &pg->max_aml_program_runtime))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "exchangedb",
                               "MAX_AML_PROGRAM_RUNTIME");
    goto fail;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "exchangedb",
                                           "LEGAL_RESERVE_EXPIRATION_TIME",
                                           &pg->legal_reserve_expiration_time))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                               "exchangedb",
                               "LEGAL_RESERVE_EXPIRATION_TIME");
    goto fail;
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_time (cfg,
                                           "exchangedb",
                                           "AGGREGATOR_SHIFT",
                                           &pg->aggregator_shift))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
                               "exchangedb",
                               "AGGREGATOR_SHIFT");
  }
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_get_value_number (cfg,
                                             "exchangedb",
                                             "DEFAULT_PURSE_LIMIT",
                                             &dpl))
  {
    GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
                               "exchangedb",
                               "DEFAULT_PURSE_LIMIT");
    pg->def_purse_limit = 1;
  }
  else
  {
    pg->def_purse_limit = (uint32_t) dpl;
  }

  if (GNUNET_OK !=
      TALER_config_get_currency (cfg,
                                 "exchange",
                                 &pg->currency))
  {
    goto fail;
  }
  if (GNUNET_OK !=
      internal_setup (pg))
  {
    goto fail;
  }
  if (check_current &&
      (GNUNET_OK !=
       GNUNET_PQ_check_current (pg->conn,
                                "exchange-")) )
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Database schema is not up-to-date. Try running taler-exchange-dbinit or taler-exchange-dbconfig!\n");
    goto fail;
  }
  return pg;

fail:
  TALER_EXCHANGEDB_disconnect (pg);
  return NULL;
}


struct TALER_EXCHANGEDB_PostgresContext *
TALER_EXCHANGEDB_connect (
  const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  return do_connect (cfg,
                     true);
}


struct TALER_EXCHANGEDB_PostgresContext *
TALER_EXCHANGEDB_connect_admin (
  const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  return do_connect (cfg,
                     false);
}


void
TALER_EXCHANGEDB_disconnect (struct TALER_EXCHANGEDB_PostgresContext *pg)
{
  if (NULL == pg)
    return;
  if (NULL != pg->conn)
  {
    GNUNET_PQ_disconnect (pg->conn);
    pg->conn = NULL;
  }
  GNUNET_free (pg->exchange_url);
  GNUNET_free (pg->sql_dir);
  GNUNET_free (pg->currency);
  GNUNET_free (pg);
}


/* end of plugin_exchangedb_postgres.c */
