#include "sqlite3database.h"
#include "sqlite3error.h"
#include "utils/scopeguard.h"
#include <ctime>
#include <stdexcept>
#include <sstream>

namespace myapp::persistence {

class sqlite3_database::library_init final : public std::enable_shared_from_this<library_init>
{
public:
    static std::shared_ptr<library_init> once()
    {
        static std::weak_ptr<library_init> ref;
        std::shared_ptr<library_init> init;

        if (!ref.expired())
        {
            init = ref.lock();
        }
        else
        {
            init = std::make_shared<library_init>();
            ref = init;
        }

        return init;
    }

    library_init()
    {
        {
            auto rc = ::sqlite3_config(SQLITE_CONFIG_URI, 1);
            throw_if_failed(rc);
        }
        {
            auto rc = ::sqlite3_initialize();
            throw_if_failed(rc);
        }
    }

    ~library_init()
    {
        ::sqlite3_shutdown();
    }

    library_init(library_init &&) noexcept = default;
    library_init& operator=(library_init &&) noexcept = default;
};

void sqlite3_database::execute_query(std::string const& query)
{
    char* errmsg = nullptr;
    auto rc = ::sqlite3_exec(db_.get(), query.c_str(), nullptr, nullptr, &errmsg);
    
    throw_if_failed(rc, errmsg);
}

void sqlite3_database::execute_query(std::string const& query, std::function<void(int, char**, char**)> callback)
{
    char* errmsg = nullptr;
    auto rc = ::sqlite3_exec(db_.get(), query.c_str(), [](void* ptr, int count, char** data, char** columns)
    {
        try
        {
            (*reinterpret_cast<decltype(&callback)>(ptr))(count, data, columns);
            return 0;
        }
        catch (...)
        {
            return 1;
        }
    }, reinterpret_cast<void*>(&callback), &errmsg);

    throw_if_failed(rc, errmsg);
}

static std::shared_ptr<sqlite3>
open_helper(std::string const& connection_string)
{
    sqlite3* db = nullptr;

    auto rc = ::sqlite3_open(connection_string.c_str(), &db);
    if (SQLITE_OK != rc) {
        ::sqlite3_close(db);
        throw sqlite3_error(rc);
    }

    return { db, &::sqlite3_close_v2 };
}

static std::shared_ptr<sqlite3_stmt>
prepare_stmt(sqlite3* db, const char* sql)
{
    sqlite3_stmt* stmt = nullptr;
    throw_if_failed(
        ::sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr));

    return { stmt, &::sqlite3_finalize };
}

static void
bind_param(sqlite3_stmt* stmt, std::string const& str, int pos)
{
    throw_if_failed(
        ::sqlite3_bind_text(stmt, pos, str.c_str(), -1, SQLITE_STATIC));
}

static void
bind_param(sqlite3_stmt* stmt, sqlite_int64 num, int pos)
{
    throw_if_failed(
        ::sqlite3_bind_int64(stmt, pos, num));
}

sqlite3_database::sqlite3_database(std::string connection_string)
  : library_init_{ library_init::once() }
  , db_{ open_helper(connection_string) }
  , insert_readout_{ nullptr }
{
    create_readout_table_if_not_exists();
    insert_readout_ = prepare_stmt(db_.get(), u8"INSERT INTO readouts VALUES (@ts, @topic, @msg);");
}

void sqlite3_database::create_readout_table_if_not_exists()
{
    execute_query(u8"CREATE TABLE IF NOT EXISTS readouts (timestamp INTEGER, topic TEXT, value TEXT);");
}

void sqlite3_database::save_readout(std::string const& topic, std::string const& value)
{
    auto reset_on_scope_exit = utils::scope_guard([stmt = insert_readout_.get()]
    {
        ::sqlite3_reset(stmt);
    });

    auto clear_bindings_on_scope_exit = utils::scope_guard([stmt = insert_readout_.get()]
    {
        ::sqlite3_clear_bindings(stmt);
    });

    bind_param(insert_readout_.get(), std::time(nullptr), 1);
    bind_param(insert_readout_.get(), topic, 2);
    bind_param(insert_readout_.get(), value, 3);

    auto rc = ::sqlite3_step(insert_readout_.get());
    if (rc != SQLITE_DONE)
        throw_if_failed(rc);
}

std::string sqlite3_database::last_readout(std::string const& topic)
{
    std::string value;
    execute_query(u8"SELECT value FROM readouts WHERE topic = '" + topic + "' ORDER BY timestamp DESC LIMIT 1;",
        [&value](int count, char** data, char** columns)
    {
        value = data[0];
    });

    return value;
}

std::string sqlite3_database::dump()
{
    std::stringstream ss;
    ss << u8"PRAGMA foreign_keys = OFF;" << std::endl;
    ss << u8"BEGIN TRANSACTION;" << std::endl;
    ss << u8"CREATE TABLE readouts(timestamp INTEGER, topic TEXT, value TEXT);" << std::endl;

    execute_query(u8"SELECT * FROM readouts;", [&ss](int count, char** data, char** columns)
    {
        if (count != 3)
            throw std::runtime_error("table schema is invalid.");

        ss << u8"INSERT INTO \"readouts\" VALUES(" << data[0] << u8", '" << data[1] << u8"', '" << data[2] << u8"');" << std::endl;
    });

    ss << u8"COMMIT;" << std::endl;
    return ss.str();
}

} /* namespace myapp::persistence */
