#include "pool.hpp"
#include "../config.hpp"
#include "../log.hpp"

#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <thread>
#include <chrono>

static OdbcPool g_pool;

OdbcPool& get_pool() { return g_pool; }

OdbcPool::~OdbcPool() { disconnect(); }

static void odbc_log_diag(SQLSMALLINT handle_type, SQLHANDLE handle, const char* ctx) {
    if (!handle) {
        logc::warn("ODBC %s failed (no handle)", ctx);
        return;
    }
    SQLSMALLINT rec = 0;
    bool any = false;
    while (true) {
        SQLCHAR state[6], msg[SQL_MAX_MESSAGE_LENGTH];
        SQLINTEGER native;
        SQLSMALLINT msg_len;
        SQLRETURN diag_rc = SQLGetDiagRec(handle_type, handle, ++rec,
            state, &native, msg, sizeof(msg), &msg_len);
        if (diag_rc != SQL_SUCCESS && diag_rc != SQL_SUCCESS_WITH_INFO) break;
        any = true;
        logc::warn("ODBC %s: %s - %s (%d)", ctx, state, msg, (int)native);
    }
    if (!any) {
        logc::warn("ODBC %s failed (no diag)", ctx);
    }
}

int OdbcPool::connect() {
    SQLRETURN rc;

    rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv_);
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) return -1;
    SQLSetEnvAttr(henv_, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);

    std::string server   = config::get("MSSQL_SERVER", "localhost");
    int         port     = config::get_int("MSSQL_PORT", 1433);
    std::string database = config::get("MSSQL_DATABASE", "eazybusiness");
    std::string user     = config::get("MSSQL_USER");
    std::string password = config::get("MSSQL_PASSWORD");
    bool encrypt = (config::get("MSSQL_ENCRYPT", "true") != "false");
    bool trust_cert = (config::get("MSSQL_TRUST_SERVER_CERTIFICATE", "true") != "false");

    if (user.empty()) {
        logc::warn("MSSQL_USER not set, skipping DB connection");
        return -1;
    }

    std::string conn_str =
        "DRIVER={ODBC Driver 18 for SQL Server};"
        "SERVER=" + server + "," + std::to_string(port) + ";"
        "DATABASE=" + database + ";"
        "UID=" + user + ";"
        "PWD=" + password + ";"
        "Encrypt=" + (encrypt ? std::string("yes") : std::string("Optional")) + ";"
        "TrustServerCertificate=" + (trust_cert ? std::string("yes") : std::string("no")) + ";";

    logc::info("ODBC connecting to %s:%d/%s as %s", server.c_str(), port, database.c_str(), user.c_str());

    const int POOL_SIZE = config::get_int("MSSQL_POOL_SIZE", 1);
    conns_.resize(POOL_SIZE);
    int connected = 0;

    for (int i = 0; i < POOL_SIZE; i++) {
        rc = SQLAllocHandle(SQL_HANDLE_DBC, henv_, &conns_[i].hdbc);
        if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) continue;

        rc = SQLDriverConnect(conns_[i].hdbc, nullptr,
            (SQLCHAR*)conn_str.c_str(), SQL_NTS,
            nullptr, 0, nullptr, SQL_DRIVER_COMPLETE);

        if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
            SQLAllocHandle(SQL_HANDLE_STMT, conns_[i].hdbc, &conns_[i].hstmt);
            SQLSetConnectAttr(conns_[i].hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
            connected++;
        } else {
            char ctx[32];
            std::snprintf(ctx, sizeof(ctx), "connect[%d]", i);
            odbc_log_diag(SQL_HANDLE_DBC, conns_[i].hdbc, ctx);
            SQLFreeHandle(SQL_HANDLE_DBC, conns_[i].hdbc);
            conns_[i].hdbc = SQL_NULL_HDBC;
        }
    }

    if (connected == 0) {
        logc::error("no ODBC connections established");
        return -1;
    }

    logc::success("ODBC pool: %d connections to %s/%s", connected, server.c_str(), database.c_str());
    return 0;
}

void OdbcPool::disconnect() {
    for (auto& c : conns_) {
        if (c.hstmt != SQL_NULL_HSTMT) SQLFreeHandle(SQL_HANDLE_STMT, c.hstmt);
        if (c.hdbc != SQL_NULL_HDBC) { SQLDisconnect(c.hdbc); SQLFreeHandle(SQL_HANDLE_DBC, c.hdbc); }
    }
    conns_.clear();
    if (henv_ != SQL_NULL_HENV) { SQLFreeHandle(SQL_HANDLE_ENV, henv_); henv_ = SQL_NULL_HENV; }
}

OdbcPool::Connection* OdbcPool::checkout_raw() {
    for (int attempt = 0; attempt < 300; attempt++) {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            for (auto& c : conns_) {
                if (!c.in_use && c.hdbc != SQL_NULL_HDBC) {
                    c.in_use = true;
                    return &c;
                }
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    return nullptr;
}

OdbcPool::ConnGuard OdbcPool::checkout() {
    return ConnGuard(checkout_raw(), this);
}

void OdbcPool::release(Connection* c) {
    if (!c) return;
    if (c->in_transaction) {
        SQLEndTran(SQL_HANDLE_DBC, c->hdbc, SQL_ROLLBACK);
        SQLSetConnectAttr(c->hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
        c->in_transaction = false;
        logc::warn("ODBC: rolled back uncommitted transaction on connection release");
    }
    std::lock_guard<std::mutex> lock(mutex_);
    c->in_use = false;
}

static bool bind_params(SQLHSTMT hstmt, const std::vector<Param>& params, std::vector<SQLLEN>& indicators) {
    indicators.assign(params.size(), 0);

    // Stable placeholder for null numeric parameters.
    static const int64_t null_placeholder = 0;

    for (size_t i = 0; i < params.size(); i++) {
        const auto& p = params[i];
        SQLUSMALLINT param_num = static_cast<SQLUSMALLINT>(i + 1);
        SQLPOINTER val_ptr = (SQLPOINTER)&null_placeholder;
        SQLLEN buf_len = 0;
        SQLSMALLINT c_type = SQL_C_CHAR;
        SQLSMALLINT sql_type = SQL_VARCHAR;
        SQLULEN column_size = 1;

        switch (p.type) {
            case ParamType::Int:
                c_type = SQL_C_SLONG;
                sql_type = SQL_INTEGER;
                val_ptr = (SQLPOINTER)&p.int_val;
                buf_len = sizeof(SQLINTEGER);
                column_size = sizeof(SQLINTEGER);
                indicators[i] = buf_len;
                break;
            case ParamType::BigInt:
                c_type = SQL_C_SBIGINT;
                sql_type = SQL_BIGINT;
                val_ptr = (SQLPOINTER)&p.int_val;
                buf_len = sizeof(SQLBIGINT);
                column_size = sizeof(SQLBIGINT);
                indicators[i] = buf_len;
                break;
            case ParamType::Float:
            case ParamType::Double:
                c_type = SQL_C_DOUBLE;
                sql_type = SQL_DOUBLE;
                val_ptr = (SQLPOINTER)&p.dbl_val;
                buf_len = sizeof(SQLDOUBLE);
                column_size = sizeof(SQLDOUBLE);
                indicators[i] = buf_len;
                break;
            case ParamType::NVarChar:
            case ParamType::DateTime:
                // UTF-8 SQL_C_CHAR + SQL_WVARCHAR works with msodbcsql18 for nvarchar columns.
                c_type = SQL_C_CHAR;
                sql_type = SQL_WVARCHAR;
                column_size = p.str_val.empty() ? 1 : p.str_val.size();
                if (!p.is_null) {
                    val_ptr = (SQLPOINTER)p.str_val.c_str();
                    buf_len = static_cast<SQLLEN>(p.str_val.size());
                    indicators[i] = SQL_NTS;
                }
                break;
            case ParamType::Bit:
                c_type = SQL_C_BIT;
                sql_type = SQL_BIT;
                val_ptr = (SQLPOINTER)&p.int_val;
                buf_len = 1;
                column_size = 1;
                indicators[i] = 1;
                break;
        }

        if (p.is_null) {
            indicators[i] = SQL_NULL_DATA;
        }

        SQLRETURN rc = SQLBindParameter(hstmt, param_num, SQL_PARAM_INPUT,
            c_type, sql_type, column_size, 0, val_ptr, buf_len, &indicators[i]);
        if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
            return false;
        }
    }
    return true;
}

static bool fetch_results(SQLHSTMT hstmt, ResultSet& out) {
    out.clear();

    while (true) {
        SQLSMALLINT col_count = 0;
        SQLNumResultCols(hstmt, &col_count);

        if (col_count > 0) {
            std::vector<SQLSMALLINT> col_types(col_count);
            for (SQLSMALLINT col = 0; col < col_count; col++) {
                SQLSMALLINT data_type;
                SQLDescribeCol(hstmt, col + 1, nullptr, 0, nullptr, &data_type, nullptr, nullptr, nullptr);
                col_types[col] = data_type;
            }

            while (true) {
                SQLRETURN rc = SQLFetch(hstmt);
                if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) break;

                Row row;
                for (SQLSMALLINT col = 0; col < col_count; col++) {
                    Cell cell;
                    SQLLEN ind;
                    SQLSMALLINT sql_type = col_types[col];

                    if (sql_type == SQL_BINARY || sql_type == SQL_VARBINARY ||
                        sql_type == SQL_LONGVARBINARY) {
                        std::vector<uint8_t> blob_data;
                        unsigned char chunk[8192];
                        while (true) {
                            rc = SQLGetData(hstmt, col + 1, SQL_C_BINARY, chunk, sizeof(chunk), &ind);
                            if (ind == SQL_NULL_DATA) { break; }
                            if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
                                SQLLEN copy_len = std::min(ind, (SQLLEN)sizeof(chunk));
                                blob_data.insert(blob_data.end(), chunk, chunk + copy_len);
                            }
                            if (rc == SQL_SUCCESS) break;
                            if (rc != SQL_SUCCESS_WITH_INFO) break;
                        }
                        if (!blob_data.empty()) {
                            cell.type = CellType::Blob;
                            cell.blob = std::move(blob_data);
                        }
                    } else {
                        char buf[4096];
                        rc = SQLGetData(hstmt, col + 1, SQL_C_CHAR, buf, sizeof(buf) - 1, &ind);
                        if (ind == SQL_NULL_DATA) {
                            cell.type = CellType::Null;
                        } else if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) {
                            cell.type = CellType::String;
                            cell.str = std::string(buf, std::min((SQLLEN)(sizeof(buf)-1), ind));
                        }
                    }
                    row.push_back(std::move(cell));
                }
                out.push_back(std::move(row));
            }
            if (!out.empty()) return true;
        }

        SQLRETURN rc = SQLMoreResults(hstmt);
        if (rc == SQL_NO_DATA) break;
        if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) break;
    }
    return true;
}

bool OdbcPool::execute(Connection* c, const std::string& sql, const std::vector<Param>& params, ResultSet& out) {
    if (!c) {
        logc::warn("ODBC execute: no connection");
        return false;
    }

    SQLRETURN rc = SQLPrepare(c->hstmt, (SQLCHAR*)sql.c_str(), SQL_NTS);
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
        odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "prepare");
        return false;
    }

    std::vector<SQLLEN> indicators;
    if (!params.empty() && !bind_params(c->hstmt, params, indicators)) {
        odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "bind");
        return false;
    }

    rc = SQLExecute(c->hstmt);
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO && rc != SQL_NO_DATA) {
        odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "execute");
        return false;
    }

    fetch_results(c->hstmt, out);

    SQLFreeStmt(c->hstmt, SQL_UNBIND);
    SQLFreeStmt(c->hstmt, SQL_CLOSE);
    return true;
}

bool OdbcPool::execute(Connection* c, const std::string& sql, ResultSet& out) {
    return execute(c, sql, {}, out);
}

bool OdbcPool::execute(const std::string& sql, const std::vector<Param>& params, ResultSet& out) {
    ConnGuard g = checkout();
    if (!g) {
        logc::warn("ODBC execute: pool exhausted");
        return false;
    }
    bool ok = execute(g.get(), sql, params, out);
    return ok;
}

bool OdbcPool::execute(const std::string& sql, ResultSet& out) {
    return execute(sql, {}, out);
}

int64_t OdbcPool::execute_scalar(const std::string& sql, const std::vector<Param>& params, int64_t fallback) {
    ResultSet rs;
    if (!execute(sql, params, rs)) {
        logc::warn("ODBC scalar query failed");
        return fallback;
    }
    if (rs.empty() || rs[0].empty()) return fallback;
    const auto& cell = rs[0][0];
    if (cell.type == CellType::Int64) return cell.i64;
    if (cell.type == CellType::String) {
        try { return std::stoll(cell.str); } catch (...) {}
    }
    return fallback;
}

bool OdbcPool::begin(Connection* c) {
    if (!c) return false;
    if (c->in_transaction) {
        logc::warn("ODBC begin: connection already in transaction");
        return false;
    }
    SQLRETURN rc = SQLSetConnectAttr(c->hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_OFF, 0);
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
        odbc_log_diag(SQL_HANDLE_DBC, c->hdbc, "begin");
        return false;
    }
    c->in_transaction = true;
    return true;
}

bool OdbcPool::commit(Connection* c) {
    if (!c || !c->in_transaction) return false;
    SQLRETURN rc = SQLEndTran(SQL_HANDLE_DBC, c->hdbc, SQL_COMMIT);
    SQLSetConnectAttr(c->hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
    c->in_transaction = false;
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
        odbc_log_diag(SQL_HANDLE_DBC, c->hdbc, "commit");
        return false;
    }
    return true;
}

bool OdbcPool::rollback(Connection* c) {
    if (!c || !c->in_transaction) return true;
    SQLRETURN rc = SQLEndTran(SQL_HANDLE_DBC, c->hdbc, SQL_ROLLBACK);
    SQLSetConnectAttr(c->hdbc, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, 0);
    c->in_transaction = false;
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
        odbc_log_diag(SQL_HANDLE_DBC, c->hdbc, "rollback");
        return false;
    }
    return true;
}
