/*
 * Copyright (C) Tildeslash Ltd. All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3.
 * 
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 *
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL.
 */


#include "Config.h"

#include <stdio.h>
#include <string.h>
#ifdef HAVE_STDATOMIC_H
#include <stdatomic.h>
#else
#define _Atomic(x) volatile x
#endif

#include "PostgresqlAdapter.h"
#include "StringBuffer.h"
#include "ConnectionDelegate.h"


/**
 * Implementation of the Connection/Delegate interface for PostgreSQL.
 *
 * Note: Due to design limitations in both libpq and PostgreSQL's
 * architecture, this implementation does not support the superior
 * memory-bounded streaming behavior we use with MySQL and Oracle.
 *
 * The core issue is that libpq's PQexec() buffers entire result sets in
 * client memory before returning, even when the PostgreSQL server is
 * streaming tuples over the socket. This is a design choice, not a
 * fundamental protocol limitation - libpq simply waits to receive all
 * rows before giving control back to the application. This means:
 * - Result sets are fully buffered on the client side
 * - For queries requiring server-side materialization (ORDER BY, GROUP BY,
 *   etc.), the result set is allocated TWICE: once on the server and once
 *   in libpq, potentially doubling memory consumption
 * - Large queries cannot be aborted mid-execution to free server resources
 *
 * While PostgreSQL does support server-side cursors (DECLARE/FETCH), they
 * are poorly suited for a general-purpose connection library: they require
 * an explicit transaction context, each fetch is a separate query round-trip,
 * and they lack the transparent, efficient cursor support that MySQL and
 * Oracle provide at the protocol level.
 *
 * libpq's PQsetSingleRowMode() only addresses client-side buffering and
 * does nothing for server-side memory usage or query abort capability.
 *
 * On the positive side, this simpler execution model means libzdb works
 * reliably with PostgreSQL connection proxies (pgBouncer, PgPool-II, etc.)
 * when using simple queries. Note that prepared statements may still
 * experience compatibility issues with certain proxy configurations.
 *
 * @file
 */

/* ----------------------------------------------------------- Definitions */


#define T ConnectionDelegate_T
struct T {
	PGconn *db;
        PGresult *res;
        StringBuffer_T sb;
        Connection_T delegator;
	ExecStatusType lastError;
};
static _Atomic(uint32_t) kStatementID = 0;
extern const struct Rop_T postgresqlrops;
extern const struct Pop_T postgresqlpops;


/* ------------------------------------------------------- Private methods */


static bool _doConnect(T C, char **error) {
#define ERROR(e) do {*error = Str_dup(e); goto error;} while (0)
        URL_T url = Connection_getURL(C->delegator);
        /* User */
        if (URL_getUser(url))
                StringBuffer_append(C->sb, "user='%s' ", URL_getUser(url));
        else if (URL_getParameter(url, "user"))
                StringBuffer_append(C->sb, "user='%s' ", URL_getParameter(url, "user"));
        else
                ERROR("no username specified in URL");
        /* Password */
        if (URL_getPassword(url))
                StringBuffer_append(C->sb, "password='%s' ", URL_getPassword(url));
        else if (URL_getParameter(url, "password"))
                StringBuffer_append(C->sb, "password='%s' ", URL_getParameter(url, "password"));
        else if (! URL_getParameter(url, "unix-socket"))
                ERROR("no password specified in URL");
        /* Host */
        if (URL_getParameter(url, "unix-socket")) {
                if (URL_getParameter(url, "unix-socket")[0] != '/')
                        ERROR("invalid unix-socket directory");
                StringBuffer_append(C->sb, "host='%s' ", URL_getParameter(url, "unix-socket"));
        } else if (URL_getHost(url)) {
                StringBuffer_append(C->sb, "host='%s' ", URL_getHost(url));
                /* Port */
                if (URL_getPort(url) > 0)
                        StringBuffer_append(C->sb, "port=%d ", URL_getPort(url));
                else
                        ERROR("no port specified in URL");
        } else
                ERROR("no host specified in URL");
        /* Database name */
        if (URL_getPath(url))
                StringBuffer_append(C->sb, "dbname='%s' ", URL_getPath(url) + 1);
        else
                ERROR("no database specified in URL");
        /* SSL Options */
        StringBuffer_append(C->sb, "sslmode='%s' ", Str_parseBool(URL_getParameter(url, "use-ssl")) ? "require" : "disable");
        if (URL_getParameter(url, "ssl-ca")) {
                StringBuffer_append(C->sb, "sslrootcert='%s' ", URL_getParameter(url, "ssl-ca"));
        }
        if (URL_getParameter(url, "ssl-cert")) {
                StringBuffer_append(C->sb, "sslcert='%s' ", URL_getParameter(url, "ssl-cert"));
        }
        if (URL_getParameter(url, "ssl-key")) {
                StringBuffer_append(C->sb, "sslkey='%s' ", URL_getParameter(url, "ssl-key"));
        }
        /* Other Options */
        if (URL_getParameter(url, "connect-timeout")) {
                StringBuffer_append(C->sb, "connect_timeout=%d ", Str_parseInt(URL_getParameter(url, "connect-timeout")));
        } else
                StringBuffer_append(C->sb, "connect_timeout=%lld ", SQL_DEFAULT_TIMEOUT/MSEC_PER_SEC);
        if (URL_getParameter(url, "application-name"))
                StringBuffer_append(C->sb, "application_name='%s' ", URL_getParameter(url, "application-name"));
        /* Connect */
        C->db = PQconnectdb(StringBuffer_toString(C->sb));
        if (PQstatus(C->db) == CONNECTION_OK)
                return true;
        *error = Str_dup(PQerrorMessage(C->db));
error:
        return false;
}


/* -------------------------------------------------------- Delegate Methods */


static void _free(T *C) {
        assert(C && *C);
        if ((*C)->res)
                PQclear((*C)->res);
        if ((*C)->db)
                PQfinish((*C)->db);
        StringBuffer_free(&((*C)->sb));
        FREE(*C);
}


static T _new(Connection_T delegator, char **error) {
	T C;
	assert(delegator);
        assert(error);
        NEW(C);
        C->delegator = delegator;
        C->sb = StringBuffer_create(STRLEN);
        if (! _doConnect(C, error))
                _free(&C);
	return C;
}


static bool _ping(T C) {
        assert(C);
        PQclear(C->res);
        C->res = PQexec(C->db, "");
        return (PQresultStatus(C->res) == PGRES_EMPTY_QUERY);
}


static void _setQueryTimeout(T C, int ms) {
        assert(C);
        StringBuffer_set(C->sb, "SET statement_timeout TO %d;", ms);
        PQclear(PQexec(C->db, StringBuffer_toString(C->sb)));
}


static bool _beginTransactionType(T C, TRANSACTION_TYPE type) {
        assert(C);
        const char *sql;
        switch (type) {
                case TRANSACTION_READ_COMMITTED:
                        sql = "BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;";
                        break;
                case TRANSACTION_REPEATABLE_READ:
                        sql = "BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;";
                        break;
                case TRANSACTION_SERIALIZABLE:
                        sql = "BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;";
                        break;
                default:
                        sql = "BEGIN TRANSACTION;";
        }
        PQclear(C->res);
        C->res = PQexec(C->db, sql);
        C->lastError = PQresultStatus(C->res);
        return (C->lastError == PGRES_COMMAND_OK);
}


static bool _commit(T C) {
	assert(C);
        PQclear(C->res);
        C->res = PQexec(C->db, "COMMIT TRANSACTION;");
        C->lastError = PQresultStatus(C->res);
        return (C->lastError == PGRES_COMMAND_OK);
}


static bool _rollback(T C) {
	assert(C);
        PQclear(C->res);
        C->res = PQexec(C->db, "ROLLBACK TRANSACTION;");
        C->lastError = PQresultStatus(C->res);
        return (C->lastError == PGRES_COMMAND_OK);
}


static long long _lastRowId(T C) {
        assert(C);
        // NA: See Connection_lastRowId documentation
        return (long long)PQoidValue(C->res);
}


static long long _rowsChanged(T C) {
        assert(C);
        char *changes = PQcmdTuples(C->res);
        return changes ? Str_parseLLong(changes) : 0;
}


static bool _execute(T C, const char *sql, va_list ap) {
	assert(C);
        PQclear(C->res);
        va_list ap_copy;
        va_copy(ap_copy, ap);
        StringBuffer_vset(C->sb, sql, ap_copy);
        va_end(ap_copy);
        C->res = PQexec(C->db, StringBuffer_toString(C->sb));
        C->lastError = PQresultStatus(C->res);
        return (C->lastError == PGRES_COMMAND_OK);
}


static ResultSet_T _executeQuery(T C, const char *sql, va_list ap) {
	assert(C);
        PQclear(C->res);
        va_list ap_copy;
        va_copy(ap_copy, ap);
        StringBuffer_vset(C->sb, sql, ap_copy);
        va_end(ap_copy);
        C->res = PQexec(C->db, StringBuffer_toString(C->sb));
        C->lastError = PQresultStatus(C->res);
        if (C->lastError == PGRES_TUPLES_OK)
                return ResultSet_new(PostgresqlResultSet_new(C->delegator, C->res), (Rop_T)&postgresqlrops);
        return NULL;
}


static PreparedStatement_T _prepareStatement(T C, const char *sql, va_list ap) {
        assert(C);
        assert(sql);
        PQclear(C->res);
        va_list ap_copy;
        va_copy(ap_copy, ap);
        StringBuffer_vset(C->sb, sql, ap_copy);
        va_end(ap_copy);
        int paramCount = StringBuffer_prepare4postgres(C->sb);
        uint32_t t = kStatementID++; // increment is atomic
        char *name = Str_cat("__libzdb-%d", t);
        C->res = PQprepare(C->db, name, StringBuffer_toString(C->sb), 0, NULL);
        C->lastError = C->res ? PQresultStatus(C->res) : PGRES_FATAL_ERROR;
        if (C->lastError == PGRES_EMPTY_QUERY || C->lastError == PGRES_COMMAND_OK || C->lastError == PGRES_TUPLES_OK)
		return PreparedStatement_new(PostgresqlPreparedStatement_new(C->delegator, C->db, name, paramCount), (Pop_T)&postgresqlpops);
        FREE(name);
        return NULL;
}


static const char *_getLastError(T C) {
	assert(C);
        return _getSQLErrorMessage(C->res);
}


static int _getLastErrorCode(T C) {
        assert(C);
        // Return SQLSTATE if available, otherwise 0
        // We intentionally do NOT mix in PQresultStatus codes
        return _getSQLStateErrorCode(C->res);
}

/* ------------------------------------------------------------------------- */


const struct Cop_T postgresqlcops = {
        .name                   = "postgresql",
        .new                    = _new,
        .free                   = _free,
        .ping                   = _ping,
        .setQueryTimeout        = _setQueryTimeout,
        .beginTransactionType   = _beginTransactionType,
        .commit                 = _commit,
        .rollback               = _rollback,
        .lastRowId              = _lastRowId,
        .rowsChanged            = _rowsChanged,
        .execute                = _execute,
        .executeQuery           = _executeQuery,
        .prepareStatement       = _prepareStatement,
        .getLastError           = _getLastError,
        .getLastErrorCode       = _getLastErrorCode
};

