#include "explorer_block_cache.h"

#include "../../util/logger.h"
#include "../../util/platform.h"

#include <nlohmann/json.hpp>
#include <sqlite3.h>

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <utility>

namespace fs = std::filesystem;
using json = nlohmann::json;

namespace dragonx {
namespace ui {

namespace {

constexpr int kCacheSchemaVersion = 1;

struct Statement {
    sqlite3_stmt* handle = nullptr;

    Statement(sqlite3* db, const char* sql)
    {
        if (sqlite3_prepare_v2(db, sql, -1, &handle, nullptr) != SQLITE_OK) {
            handle = nullptr;
        }
    }

    ~Statement()
    {
        if (handle) sqlite3_finalize(handle);
    }

    Statement(const Statement&) = delete;
    Statement& operator=(const Statement&) = delete;
};

bool bindText(sqlite3_stmt* statement, int index, const std::string& value)
{
    return sqlite3_bind_text(statement, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK;
}

bool blockSummaryFromJson(const json& source, ExplorerBlockSummary& block)
{
    if (!source.is_object()) return false;
    try {
        block.height = source.value("height", 0);
        block.hash = source.value("hash", std::string());
        block.tx_count = source.value("tx_count", 0);
        block.size = source.value("size", 0);
        block.time = source.value("time", static_cast<std::int64_t>(0));
        block.difficulty = source.value("difficulty", 0.0);
    } catch (...) {
        return false;
    }
    return block.height > 0 && !block.hash.empty();
}

} // namespace

ExplorerBlockCache::ExplorerBlockCache()
    : ExplorerBlockCache(defaultDatabasePath(), defaultLegacyJsonPath())
{
}

ExplorerBlockCache::ExplorerBlockCache(std::string databasePath, std::string legacyJsonPath)
    : database_path_(std::move(databasePath)), legacy_json_path_(std::move(legacyJsonPath))
{
}

ExplorerBlockCache::~ExplorerBlockCache()
{
    close();
}

std::string ExplorerBlockCache::defaultDatabasePath()
{
    return (fs::path(util::Platform::getConfigDir()) / "explorer_blocks.sqlite").string();
}

std::string ExplorerBlockCache::defaultLegacyJsonPath()
{
    return (fs::path(util::Platform::getConfigDir()) / "explorer_blocks_cache.json").string();
}

bool ExplorerBlockCache::ensureOpen()
{
    if (db_) return true;

    try {
        fs::path path(database_path_);
        if (!path.parent_path().empty()) fs::create_directories(path.parent_path());
    } catch (const std::exception& e) {
        DEBUG_LOGF("Failed to create explorer cache directory: %s\n", e.what());
        return false;
    }

    sqlite3* openedDb = nullptr;
    if (sqlite3_open(database_path_.c_str(), &openedDb) != SQLITE_OK) {
        DEBUG_LOGF("Failed to open explorer block cache: %s\n",
                   openedDb ? sqlite3_errmsg(openedDb) : "unknown error");
        if (openedDb) sqlite3_close(openedDb);
        return false;
    }

    db_ = openedDb;
    sqlite3_busy_timeout(db_, 2000);
    exec("PRAGMA journal_mode=WAL");
    exec("PRAGMA synchronous=NORMAL");

    if (!createSchema()) {
        close();
        return false;
    }

    migrateLegacyJsonIfNeeded();
    return true;
}

std::map<int, ExplorerBlockSummary> ExplorerBlockCache::loadRange(int minHeight, int maxHeight)
{
    std::map<int, ExplorerBlockSummary> blocks;
    if (minHeight > maxHeight) std::swap(minHeight, maxHeight);
    if (minHeight < 1 || maxHeight < 1 || !ensureOpen()) return blocks;

    Statement statement(db_,
        "SELECT height, hash, tx_count, size, time, difficulty "
        "FROM explorer_blocks WHERE height BETWEEN ? AND ? ORDER BY height DESC");
    if (!statement.handle) return blocks;

    sqlite3_bind_int(statement.handle, 1, minHeight);
    sqlite3_bind_int(statement.handle, 2, maxHeight);

    while (sqlite3_step(statement.handle) == SQLITE_ROW) {
        ExplorerBlockSummary block;
        block.height = sqlite3_column_int(statement.handle, 0);
        const unsigned char* hashText = sqlite3_column_text(statement.handle, 1);
        if (hashText) block.hash = reinterpret_cast<const char*>(hashText);
        block.tx_count = sqlite3_column_int(statement.handle, 2);
        block.size = sqlite3_column_int(statement.handle, 3);
        block.time = static_cast<std::int64_t>(sqlite3_column_int64(statement.handle, 4));
        block.difficulty = sqlite3_column_double(statement.handle, 5);
        if (block.height > 0 && !block.hash.empty()) {
            blocks[block.height] = std::move(block);
        }
    }

    return blocks;
}

std::vector<ExplorerBlockSummary> ExplorerBlockCache::searchBlocks(const std::string& query, int limit)
{
    std::vector<ExplorerBlockSummary> results;
    if (query.empty() || limit < 1 || !ensureOpen()) return results;

    // Escape LIKE wildcards in the user input so '%' / '_' are matched literally.
    std::string escaped;
    escaped.reserve(query.size());
    for (char c : query) {
        if (c == '%' || c == '_' || c == '\\') escaped.push_back('\\');
        escaped.push_back(c);
    }
    std::string pattern = "%" + escaped + "%";

    Statement statement(db_,
        "SELECT height, hash, tx_count, size, time, difficulty "
        "FROM explorer_blocks "
        "WHERE CAST(height AS TEXT) LIKE ?1 ESCAPE '\\' OR hash LIKE ?1 ESCAPE '\\' "
        "ORDER BY height DESC LIMIT ?2");
    if (!statement.handle) return results;

    sqlite3_bind_text(statement.handle, 1, pattern.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_int(statement.handle, 2, limit);

    while (sqlite3_step(statement.handle) == SQLITE_ROW) {
        ExplorerBlockSummary block;
        block.height = sqlite3_column_int(statement.handle, 0);
        const unsigned char* hashText = sqlite3_column_text(statement.handle, 1);
        if (hashText) block.hash = reinterpret_cast<const char*>(hashText);
        block.tx_count = sqlite3_column_int(statement.handle, 2);
        block.size = sqlite3_column_int(statement.handle, 3);
        block.time = static_cast<std::int64_t>(sqlite3_column_int64(statement.handle, 4));
        block.difficulty = sqlite3_column_double(statement.handle, 5);
        if (block.height > 0 && !block.hash.empty()) results.push_back(std::move(block));
    }

    return results;
}

bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block)
{
    if (block.height < 1 || block.hash.empty() || !ensureOpen()) return false;

    Statement statement(db_,
        "INSERT OR REPLACE INTO explorer_blocks "
        "(height, hash, tx_count, size, time, difficulty) VALUES (?, ?, ?, ?, ?, ?)");
    if (!statement.handle) return false;

    sqlite3_bind_int(statement.handle, 1, block.height);
    if (!bindText(statement.handle, 2, block.hash)) return false;
    sqlite3_bind_int(statement.handle, 3, block.tx_count);
    sqlite3_bind_int(statement.handle, 4, block.size);
    sqlite3_bind_int64(statement.handle, 5, static_cast<sqlite3_int64>(block.time));
    sqlite3_bind_double(statement.handle, 6, block.difficulty);

    return sqlite3_step(statement.handle) == SQLITE_DONE;
}

int ExplorerBlockCache::cachedBlockCount()
{
    if (!ensureOpen()) return 0;
    Statement statement(db_, "SELECT COUNT(*) FROM explorer_blocks");
    if (!statement.handle || sqlite3_step(statement.handle) != SQLITE_ROW) return 0;
    return sqlite3_column_int(statement.handle, 0);
}

void ExplorerBlockCache::clearBlocks()
{
    if (!ensureOpen()) return;
    exec("DELETE FROM explorer_blocks");
}

ExplorerBlockCache::SavedTipValidation ExplorerBlockCache::prepareValidation(
    int currentHeight, const std::string& currentBestHash)
{
    SavedTipValidation validation;
    if (currentHeight <= 0 || !ensureOpen()) return validation;

    int savedHeight = getMetaInt("tip_height", 0);
    std::string savedHash = getMetaValue("tip_hash");

    if (savedHeight <= 0 || savedHash.empty()) {
        if (!currentBestHash.empty()) updateTip(currentHeight, currentBestHash);
        return validation;
    }

    if (currentHeight < savedHeight) {
        clearBlocks();
        if (!currentBestHash.empty()) updateTip(currentHeight, currentBestHash);
        else updateTip(0, std::string());
        return validation;
    }

    if (currentHeight == savedHeight) {
        if (currentBestHash.empty()) return validation;
        if (currentBestHash != savedHash) clearBlocks();
        updateTip(currentHeight, currentBestHash);
        return validation;
    }

    if (currentBestHash.empty()) return validation;

    validation.needed = true;
    validation.height = savedHeight;
    validation.expectedHash = savedHash;
    return validation;
}

void ExplorerBlockCache::applySavedTipValidation(const SavedTipValidation& validation,
                                                 const std::string& actualHash,
                                                 int currentHeight,
                                                 const std::string& currentBestHash)
{
    if (!validation.needed || !ensureOpen()) return;
    if (actualHash.empty()) return;

    if (actualHash != validation.expectedHash) {
        clearBlocks();
    }

    if (currentHeight > 0 && !currentBestHash.empty()) {
        updateTip(currentHeight, currentBestHash);
    }
}

void ExplorerBlockCache::updateTip(int height, const std::string& hash)
{
    if (!ensureOpen()) return;
    setMetaValue("tip_height", std::to_string(std::max(0, height)));
    setMetaValue("tip_hash", hash);
}

bool ExplorerBlockCache::exec(const char* sql)
{
    if (!db_) return false;
    char* error = nullptr;
    int result = sqlite3_exec(db_, sql, nullptr, nullptr, &error);
    if (result != SQLITE_OK) {
        DEBUG_LOGF("Explorer block cache SQL error: %s\n", error ? error : sqlite3_errmsg(db_));
        if (error) sqlite3_free(error);
        return false;
    }
    return true;
}

std::string ExplorerBlockCache::getMetaValue(const std::string& key)
{
    if (!ensureOpen()) return {};

    Statement statement(db_, "SELECT value FROM explorer_cache_meta WHERE key = ?");
    if (!statement.handle) return {};
    if (!bindText(statement.handle, 1, key)) return {};
    if (sqlite3_step(statement.handle) != SQLITE_ROW) return {};

    const unsigned char* valueText = sqlite3_column_text(statement.handle, 0);
    return valueText ? reinterpret_cast<const char*>(valueText) : std::string();
}

int ExplorerBlockCache::getMetaInt(const std::string& key, int fallback)
{
    std::string value = getMetaValue(key);
    if (value.empty()) return fallback;
    try {
        return std::stoi(value);
    } catch (...) {
        return fallback;
    }
}

void ExplorerBlockCache::setMetaValue(const std::string& key, const std::string& value)
{
    if (!ensureOpen()) return;

    Statement statement(db_,
        "INSERT OR REPLACE INTO explorer_cache_meta (key, value) VALUES (?, ?)");
    if (!statement.handle) return;
    if (!bindText(statement.handle, 1, key)) return;
    if (!bindText(statement.handle, 2, value)) return;
    sqlite3_step(statement.handle);
}

bool ExplorerBlockCache::createSchema()
{
    return exec("CREATE TABLE IF NOT EXISTS explorer_blocks ("
                "height INTEGER PRIMARY KEY,"
                "hash TEXT NOT NULL,"
                "tx_count INTEGER NOT NULL,"
                "size INTEGER NOT NULL,"
                "time INTEGER NOT NULL,"
                "difficulty REAL NOT NULL)") &&
           exec("CREATE TABLE IF NOT EXISTS explorer_cache_meta ("
                "key TEXT PRIMARY KEY,"
                "value TEXT NOT NULL)") &&
           exec("INSERT OR IGNORE INTO explorer_cache_meta (key, value) VALUES "
                "('schema_version', '1')");
}

void ExplorerBlockCache::migrateLegacyJsonIfNeeded()
{
    if (!db_ || getMetaValue("json_migrated") == "1") return;

    bool migrated = false;
    try {
        if (!legacy_json_path_.empty() && fs::exists(legacy_json_path_)) {
            std::ifstream file(legacy_json_path_);
            json cache;
            file >> cache;

            if (cache.is_object() && cache.value("version", 0) == kCacheSchemaVersion) {
                exec("BEGIN IMMEDIATE TRANSACTION");
                if (cache.contains("blocks") && cache["blocks"].is_array()) {
                    for (const auto& entry : cache["blocks"]) {
                        ExplorerBlockSummary block;
                        if (blockSummaryFromJson(entry, block)) {
                            storeBlock(block);
                        }
                    }
                }
                exec("COMMIT");

                int tipHeight = cache.value("tip_height", 0);
                std::string tipHash = cache.value("tip_hash", std::string());
                if (tipHeight > 0 && !tipHash.empty()) {
                    updateTip(tipHeight, tipHash);
                }
            }
        }
        migrated = true;
    } catch (const std::exception& e) {
        exec("ROLLBACK");
        DEBUG_LOGF("Failed to migrate explorer JSON cache: %s\n", e.what());
        migrated = true;
    }

    if (migrated) setMetaValue("json_migrated", "1");
}

void ExplorerBlockCache::close()
{
    if (!db_) return;
    sqlite3_close(db_);
    db_ = nullptr;
}

} // namespace ui
} // namespace dragonx
