// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3

#include "explorer_tab.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../data/wallet_state.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "mining_tab_helpers.h"
#include "../explorer/explorer_block_cache.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../layout.h"
#include "../notifications.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"

#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <ctime>
#include <cmath>
#include <algorithm>
#include <cctype>
#include <atomic>
#include <map>
#include <set>

namespace dragonx {
namespace ui {

using json = nlohmann::json;
using namespace material;

// ─── Static state ───────────────────────────────────────────────

// Search
static char s_search_buf[128] = {};
static bool s_search_loading = false;
static std::string s_search_error;
static double s_search_change_time = -1.0;   // GetTime() of the last edit (debounce for live search)
static std::string s_search_last_query;      // last query actually run (avoids re-running it)
static constexpr double kSearchDebounceSeconds = 0.35;
// When > 0, the recent-blocks list is anchored here (a search result) instead of the chain tip.
// Searching re-anchors the LIST rather than opening the detail modal — that stays for row clicks.
static int s_search_target_height = -1;

// A query that performSearch() can resolve to a concrete result: a block height (all digits) or a
// complete 64-char block hash / txid. Partial hex is intentionally NOT resolvable, so live search
// stays quiet (no "invalid query") until the user finishes typing or pasting a full hash.
static bool isResolvableQuery(const std::string& q) {
    if (q.empty()) return false;
    if (std::all_of(q.begin(), q.end(), ::isdigit)) return true;
    return q.size() == 64 && std::all_of(q.begin(), q.end(), [](char c) {
        return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
    });
}

using BlockSummary = ExplorerBlockSummary;
static ExplorerBlockCache s_recent_block_cache;
static std::set<int> s_pending_block_heights;
static int s_recent_page = 0;
static int s_recent_max_page = 0;
// Fuzzy search: a non-numeric / partial query filters the list to cached blocks whose hash (or
// height text) contains it. Results are memoized per query so we don't hit SQLite every frame.
static std::string s_active_search_query;
static std::string s_fuzzy_query_cached;
static std::vector<BlockSummary> s_fuzzy_results;
static constexpr int kMaxFuzzyResults = 100;
static std::atomic<int> s_pending_block_fetches{0};
static bool s_recent_disk_cache_validation_pending = false;

// Block detail modal
static bool s_show_detail_modal = false;
static bool s_detail_loading = false;
static int s_detail_height = 0;
static std::string s_detail_hash;
static int64_t s_detail_time = 0;
static int s_detail_confirmations = 0;
static int s_detail_size = 0;
static double s_detail_difficulty = 0.0;
static std::string s_detail_bits;
static std::string s_detail_merkle;
static std::string s_detail_prev_hash;
static std::string s_detail_next_hash;
static std::vector<std::string> s_detail_txids;

// Expanded transaction detail
static int s_expanded_tx_idx = -1;
static bool s_tx_loading = false;
static json s_tx_detail;

// Mempool info
static int s_mempool_tx_count = 0;
static int64_t s_mempool_size = 0;
static bool s_mempool_loading = false;

// ─── Helpers ────────────────────────────────────────────────────

static const char* relativeTime(int64_t timestamp) {
    static char buf[64];
    int64_t now = std::time(nullptr);
    int64_t diff = now - timestamp;
    if (diff < 0) diff = 0;
    if (diff < 60)
        snprintf(buf, sizeof(buf), TR("grpa_sec_ago"), (long long)diff);
    else if (diff < 3600)
        snprintf(buf, sizeof(buf), TR("grpa_min_ago"), (long long)(diff / 60));
    else if (diff < 86400)
        snprintf(buf, sizeof(buf), TR("grpa_hr_ago"), (long long)(diff / 3600));
    else
        snprintf(buf, sizeof(buf), TR("grpa_days_ago"), (long long)(diff / 86400));
    return buf;
}

static std::string truncateHash(const std::string& hash, int front, int back) {
    return util::truncateMiddle(hash, front, back);
}

// Adaptive hash truncation based on available pixel width
static std::string truncateHashToFit(const std::string& hash, ImFont* font, float maxWidth) {
    ImVec2 fullSz = font->CalcTextSizeA(font->LegacySize, 10000.0f, 0.0f, hash.c_str());
    if (fullSz.x <= maxWidth) return hash;
    // Binary search for the right truncation
    int front = 6, back = 4;
    for (int f = (int)hash.size() / 2; f >= 4; f -= 2) {
        int b = std::max(4, f - 2);
        std::string t = hash.substr(0, f) + "..." + hash.substr(hash.size() - b);
        ImVec2 sz = font->CalcTextSizeA(font->LegacySize, 10000.0f, 0.0f, t.c_str());
        if (sz.x <= maxWidth) { front = f; back = b; break; }
    }
    return truncateHash(hash, front, back);
}

static void validateRecentBlockCache(App* app, const WalletState& state) {
    if (s_recent_disk_cache_validation_pending) return;

    auto validation = s_recent_block_cache.prepareValidation(state.sync.blocks, state.sync.best_blockhash);
    if (!validation.needed) return;

    auto* worker = app ? app->worker() : nullptr;
    auto* rpc = app ? app->rpc() : nullptr;
    if (!worker || !rpc || !rpc->isConnected()) return;

    int currentHeight = state.sync.blocks;
    std::string currentHash = state.sync.best_blockhash;
    s_recent_disk_cache_validation_pending = true;
    worker->post([rpc, validation, currentHeight, currentHash = std::move(currentHash)]() mutable -> rpc::RPCWorker::MainCb {
        std::string actualHash;
        try {
            rpc::RPCClient::TraceScope trace("Explorer tab / Cache validation");
            actualHash = rpc->call("getblockhash", {validation.height}).get<std::string>();
        } catch (...) {}

        return [validation, currentHeight, currentHash = std::move(currentHash),
                actualHash = std::move(actualHash)]() mutable {
            s_recent_disk_cache_validation_pending = false;
            s_recent_block_cache.applySavedTipValidation(validation, actualHash, currentHeight, currentHash);
        };
    });
}

static std::string formatSize(int bytes) {
    char b[32];
    if (bytes >= 1048576)
        snprintf(b, sizeof(b), "%.2f MB", bytes / (1024.0 * 1024.0));
    else if (bytes >= 1024)
        snprintf(b, sizeof(b), "%.2f KB", bytes / 1024.0);
    else
        snprintf(b, sizeof(b), "%d B", bytes);
    return b;
}

// Copy icon button — draws a small icon that copies text to clipboard on click
static void copyButton(const char* id, const std::string& text, float x, float y) {
    ImFont* iconFont = Type().iconSmall();
    ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 100.0f, 0.0f, ICON_MD_CONTENT_COPY);
    float btnSize = iconSz.x + Layout::spacingSm() * 2;
    ImGui::SetCursorScreenPos(ImVec2(x, y));
    ImGui::PushID(id);
    ImGui::InvisibleButton("##copy", ImVec2(btnSize, iconSz.y + Layout::spacingXs()));
    bool hovered = ImGui::IsItemHovered();
    bool clicked = ImGui::IsItemClicked(0);
    ImDrawList* dl = ImGui::GetWindowDrawList();
    dl->AddText(iconFont, iconFont->LegacySize,
        ImVec2(x + Layout::spacingSm(), y),
        hovered ? Primary() : OnSurfaceMedium(), ICON_MD_CONTENT_COPY);
    if (hovered) material::Tooltip("%s", TR("click_to_copy"));
    if (clicked) {
        ImGui::SetClipboardText(text.c_str());
        Notifications::instance().success(TR("copied_to_clipboard"));
    }
    ImGui::PopID();
}

// Draw a label: value pair on one line inside a card
static void drawLabelValue(ImDrawList* dl, float x, float y, float labelW,
                           const char* label, const char* value,
                           ImFont* labelFont, ImFont* valueFont) {
    dl->AddText(labelFont, labelFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), label);
    dl->AddText(valueFont, valueFont->LegacySize, ImVec2(x + labelW, y), OnSurface(), value);
}

// ─── Async RPC Helpers ──────────────────────────────────────────
//  All RPC work is dispatched to the RPCWorker background thread.
//  Results are delivered back to the UI thread via MainCb lambdas.

static void applyBlockDetailResult(const json& result, const std::string& error) {
    s_detail_loading = false;
    s_search_loading = false;

    if (!error.empty()) {
        s_search_error = error;
        s_show_detail_modal = false;
        return;
    }

    if (result.is_null()) {
        s_search_error = TR("explorer_invalid_response");
        s_show_detail_modal = false;
        return;
    }

    s_detail_height = result.value("height", 0);
    s_detail_hash = result.value("hash", "");
    s_detail_time = result.value("time", (int64_t)0);
    s_detail_confirmations = result.value("confirmations", 0);
    s_detail_size = result.value("size", 0);
    s_detail_difficulty = result.value("difficulty", 0.0);
    s_detail_bits = result.value("bits", "");
    s_detail_merkle = result.value("merkleroot", "");
    s_detail_prev_hash = result.value("previousblockhash", "");
    s_detail_next_hash = result.value("nextblockhash", "");

    s_detail_txids.clear();
    if (result.contains("tx") && result["tx"].is_array()) {
        for (const auto& tx : result["tx"]) {
            if (tx.is_string()) s_detail_txids.push_back(tx.get<std::string>());
        }
    }

    s_show_detail_modal = true;
    s_expanded_tx_idx = -1;
    s_search_error.clear();
}

static void fetchBlockDetail(App* app, int height) {
    auto* worker = app->worker();
    auto* rpc = app->rpc();
    if (!worker || !rpc) return;
    s_detail_loading = true;
    s_search_loading = true;
    s_search_error.clear();
    worker->post([rpc, height]() -> rpc::RPCWorker::MainCb {
        json result;
        std::string error;
        try {
            rpc::RPCClient::TraceScope trace("Explorer tab / Block detail");
            auto hashResult = rpc->call("getblockhash", {height});
            std::string hash = hashResult.get<std::string>();
            result = rpc->call("getblock", {hash});
        } catch (const std::exception& e) { error = e.what(); }
        return [result, error]() {
            applyBlockDetailResult(result, error);
        };
    });
}


static bool fetchRecentBlock(App* app, int height) {
    auto* worker = app->worker();
    auto* rpc = app->rpc();
    if (height < 1 || !worker || !rpc) return false;
    if (s_pending_block_heights.find(height) != s_pending_block_heights.end()) return false;

    s_pending_block_heights.insert(height);
    s_pending_block_fetches.fetch_add(1);

    worker->post([rpc, height]() -> rpc::RPCWorker::MainCb {
        BlockSummary block;
        try {
            rpc::RPCClient::TraceScope trace("Explorer tab / Recent blocks");
            auto hashResult = rpc->call("getblockhash", {height});
            auto hash = hashResult.get<std::string>();
            auto result = rpc->call("getblock", {hash});
            block.height = result.value("height", 0);
            block.hash = result.value("hash", "");
            block.time = result.value("time", (int64_t)0);
            block.size = result.value("size", 0);
            block.difficulty = result.value("difficulty", 0.0);
            if (result.contains("tx") && result["tx"].is_array())
                block.tx_count = static_cast<int>(result["tx"].size());
        } catch (...) {}

        return [height, block = std::move(block)]() mutable {
            if (block.height > 0) {
                s_recent_block_cache.storeBlock(block);
            }
            s_pending_block_heights.erase(height);
            if (s_pending_block_fetches.load() > 0) {
                s_pending_block_fetches.fetch_sub(1);
            }
        };
    });
    return true;
}

static void fetchMempoolInfo(App* app) {
    auto* worker = app->worker();
    auto* rpc = app->rpc();
    if (!worker || !rpc || s_mempool_loading) return;
    s_mempool_loading = true;
    worker->post([rpc]() -> rpc::RPCWorker::MainCb {
        int txCount = 0;
        int64_t bytes = 0;
        try {
            rpc::RPCClient::TraceScope trace("Explorer tab / Mempool summary");
            auto result = rpc->call("getmempoolinfo", json::array());
            txCount = result.value("size", 0);
            bytes = result.value("bytes", (int64_t)0);
        } catch (...) {}
        return [txCount, bytes]() {
            s_mempool_tx_count = txCount;
            s_mempool_size = bytes;
            s_mempool_loading = false;
        };
    });
}

static void fetchTxDetail(App* app, const std::string& txid) {
    auto* worker = app->worker();
    auto* rpc = app->rpc();
    if (!worker || !rpc) return;
    s_tx_loading = true;
    s_tx_detail = json();
    worker->post([rpc, txid]() -> rpc::RPCWorker::MainCb {
        json result;
        try {
            rpc::RPCClient::TraceScope trace("Explorer tab / Transaction detail");
            result = rpc->call("getrawtransaction", {txid, 1});
        }
        catch (...) {}
        return [result]() {
            s_tx_loading = false;
            s_tx_detail = result;
        };
    });
}

// Resolve a 64-char hash to a block height and re-anchor the list there (no modal). If the hash is a
// txid rather than a block, fall back to the inline transaction view (also not a modal).
static void navigateToHash(App* app, const std::string& hash) {
    auto* worker = app->worker();
    auto* rpc = app->rpc();
    if (!worker || !rpc || !rpc->isConnected()) return;
    s_search_loading = true;
    s_search_error.clear();
    worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
        json blockResult, txResult;
        bool gotBlock = false, gotTx = false;
        try {
            rpc::RPCClient::TraceScope trace("Explorer tab / Search");
            blockResult = rpc->call("getblock", {hash});
            gotBlock = true;
        } catch (...) {}
        if (!gotBlock) {
            try {
                rpc::RPCClient::TraceScope trace("Explorer tab / Search");
                txResult = rpc->call("getrawtransaction", {hash, 1});
                gotTx = true;
            } catch (...) {}
        }
        return [blockResult, txResult, gotBlock, gotTx]() {
            s_search_loading = false;
            if (gotBlock) {
                int h = blockResult.value("height", -1);
                if (h > 0) { s_search_target_height = h; s_recent_page = 0; }
                s_show_detail_modal = false;
            } else if (gotTx && !txResult.is_null()) {
                // A txid: show it inline (existing tx detail view), not a modal.
                s_tx_detail = txResult;
                s_tx_loading = false;
                s_expanded_tx_idx = 0;
                s_show_detail_modal = false;
            } else {
                s_search_error = TR("explorer_hash_not_found");
            }
        };
    });
}

// Search re-anchors the recent-blocks LIST to the queried block instead of popping the detail modal.
static void performSearch(App* app, const std::string& query) {
    if (query.empty()) { s_search_target_height = -1; return; }

    // Integer = block height → just point the list at it.
    if (std::all_of(query.begin(), query.end(), ::isdigit)) {
        long h = 0;
        try { h = std::stol(query); } catch (...) { return; }
        int tip = app->getWalletState().sync.blocks;
        if (h < 1) h = 1;
        if (tip > 0 && h > tip) h = tip;
        s_search_target_height = static_cast<int>(h);
        s_recent_page = 0;
        s_search_error.clear();
        return;
    }

    // 64-char hex = block hash or txid → resolve to a height and re-anchor (or inline tx).
    bool isHex64 = query.size() == 64 &&
        std::all_of(query.begin(), query.end(), [](char c) {
            return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
        });
    if (isHex64) {
        // A hash lookup needs the daemon; navigateToHash() would otherwise return silently.
        if (!app->rpc() || !app->rpc()->isConnected()) {
            s_search_error = TR("explorer_not_connected");
            return;
        }
        navigateToHash(app, query);
        return;
    }

    s_search_error = TR("explorer_invalid_query");
}

// ─── Render Sections ────────────────────────────────────────────

static void renderSearchBar(App* app, float availWidth) {
    auto& S = schema::UI();
    float pad = Layout::cardInnerPadding();

    ImFont* capFont = Type().caption();
    float navBtnSz = std::max(24.0f * Layout::dpiScale(), capFont->LegacySize + 8.0f * Layout::dpiScale());
    if (s_recent_page > s_recent_max_page) s_recent_page = s_recent_max_page;
    if (s_recent_page < 0) s_recent_page = 0;
    char pageBuf[32];
    snprintf(pageBuf, sizeof(pageBuf), "%d / %d", s_recent_page + 1, s_recent_max_page + 1);
    float pageW = capFont->CalcTextSizeA(capFont->LegacySize, 1000.0f, 0.0f, pageBuf).x;
    float navGap = Layout::spacingXs();
    float navW = navBtnSz * 2.0f + pageW + navGap * 2.0f;

    float inputW = std::min(
        S.drawElement("tabs.explorer", "search-input-width").size * Layout::dpiScale(),
        availWidth * 0.65f);
    float btnW = S.drawElement("tabs.explorer", "search-button-width").size * Layout::dpiScale();
    float barH = S.drawElement("tabs.explorer", "search-bar-height").size * Layout::dpiScale();

    // Clamp so search bar never overflows
    float maxInputW = availWidth - btnW - navW - pad * 4 - Type().iconMed()->LegacySize;
    if (inputW > maxInputW) inputW = maxInputW;
    if (inputW < 80.0f) inputW = 80.0f;

    ImGui::Spacing();
    float rowStartX = ImGui::GetCursorScreenPos().x;

    // Icon
    ImGui::PushFont(Type().iconMed());
    float iconH = Type().iconMed()->LegacySize;
    float cursorY = ImGui::GetCursorScreenPos().y;
    // Vertically center icon with input
    ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, cursorY + (barH - iconH) * 0.5f));
    ImGui::TextUnformatted(ICON_MD_SEARCH);
    ImGui::PopFont();
    ImGui::SameLine();

    // Input — match height to barH
    ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, cursorY));
    ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
        ImVec2(ImGui::GetStyle().FramePadding.x, (barH - ImGui::GetFontSize()) * 0.5f));
    ImGui::SetNextItemWidth(inputW);
    bool enter = ImGui::InputText("##ExplorerSearch", s_search_buf, sizeof(s_search_buf),
        ImGuiInputTextFlags_EnterReturnsTrue);
    bool edited = ImGui::IsItemEdited();  // must read immediately after the input item
    ImGui::PopStyleVar();

    ImGui::SameLine();

    // Search button — same height as input
    ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, cursorY));
    bool clicked = material::TactileButton(TR("explorer_search"),
        ImVec2(btnW, barH));

    std::string query(s_search_buf);
    while (!query.empty() && query.front() == ' ') query.erase(query.begin());
    while (!query.empty() && query.back() == ' ') query.pop_back();
    s_active_search_query = query;  // drives the fuzzy list filter in renderRecentBlocks

    if (edited) {
        s_search_change_time = ImGui::GetTime();  // (re)start the debounce on each keystroke
        s_search_error.clear();                    // don't show a stale error mid-type
        if (query.empty()) {
            // Cleared the box → drop the anchor and return to recent (tip) blocks immediately.
            s_search_target_height = -1;
            s_search_last_query.clear();
            s_search_change_time = -1.0;
        }
    }

    bool doSearch = enter || clicked;
    // Live search: once the user pauses and the query is resolvable, re-anchor the list (no modal)
    // without waiting for Enter.
    if (!doSearch && s_search_change_time >= 0.0 &&
        (ImGui::GetTime() - s_search_change_time) >= kSearchDebounceSeconds &&
        isResolvableQuery(query) && query != s_search_last_query) {
        doSearch = true;
    }

    if (doSearch && !query.empty()) {
        s_search_change_time = -1.0;     // consume the debounce
        s_search_last_query = query;
        performSearch(app, query);       // numeric is offline-friendly; hash lookup checks rpc itself
    }

    // Loading spinner
    if (s_search_loading) {
        ImGui::SameLine();
        ImGui::TextDisabled("%s", TR("loading"));
    }

    float navX = rowStartX + availWidth - navW;
    float navY = cursorY + std::max(0.0f, (barH - navBtnSz) * 0.5f);
    ImGui::SetCursorScreenPos(ImVec2(navX, navY));
    ImGui::BeginDisabled(s_recent_page <= 0);
    ImGui::PushID("RecentBlocksSearchPrev");
    if (TactileButton(ICON_MD_CHEVRON_LEFT, ImVec2(navBtnSz, navBtnSz), Type().iconSmall())) {
        --s_recent_page;
    }
    ImGui::PopID();
    ImGui::EndDisabled();
    ImGui::SameLine(0, navGap);
    ImVec2 pagePos = ImGui::GetCursorScreenPos();
    ImGui::GetWindowDrawList()->AddText(capFont, capFont->LegacySize,
        ImVec2(pagePos.x, pagePos.y + (navBtnSz - capFont->LegacySize) * 0.5f), OnSurfaceMedium(), pageBuf);
    ImGui::Dummy(ImVec2(pageW, navBtnSz));
    ImGui::SameLine(0, navGap);
    ImGui::BeginDisabled(s_recent_page >= s_recent_max_page);
    ImGui::PushID("RecentBlocksSearchNext");
    if (TactileButton(ICON_MD_CHEVRON_RIGHT, ImVec2(navBtnSz, navBtnSz), Type().iconSmall())) {
        ++s_recent_page;
    }
    ImGui::PopID();
    ImGui::EndDisabled();

    ImGui::SetCursorScreenPos(ImVec2(rowStartX, cursorY + barH));

    // Error
    if (!s_search_error.empty()) {
        ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
        ImGui::TextWrapped("%s", s_search_error.c_str());
        ImGui::PopStyleColor();
    }

    ImGui::Spacing();
}

static void renderChainStats(App* app, float availWidth) {
    const auto& state = app->getWalletState();
    float pad = Layout::cardInnerPadding();
    float gap = Layout::cardGap();
    float dp = Layout::dpiScale();

    ImDrawList* dl = ImGui::GetWindowDrawList();
    GlassPanelSpec glassSpec;
    glassSpec.rounding = Layout::glassRounding();

    ImFont* ovFont = Type().overline();
    ImFont* capFont = Type().caption();
    ImFont* sub1 = Type().subtitle1();
    ImFont* heroFont = Type().h2();
    ImFont* iconFont = Type().iconSmall();

    float headerH = ovFont->LegacySize + Layout::spacingSm();
    float heroLineH = capFont->LegacySize + Layout::spacingXs() + heroFont->LegacySize;
    float hashLineH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
    float cardH = std::max(148.0f * dp,
        pad * 1.5f + headerH + heroLineH + hashLineH + Layout::spacingLg() * 2.0f);

    bool stacked = availWidth < 760.0f * dp;
    float chainW = stacked ? availWidth : (availWidth - gap) * 0.60f;
    float metricsW = stacked ? availWidth : availWidth - chainW - gap;

    ImVec2 basePos = ImGui::GetCursorScreenPos();
    char buf[128];

    auto drawStatusPill = [&](const ImVec2& cardMin, float cardW) {
        bool connected = app->rpc() && app->rpc()->isConnected();
        ImU32 pillCol = connected ? Success() : Error();
        std::string statusText;

        if (!connected) {
            statusText = TR("not_connected");
        } else if (state.sync.syncing && state.sync.headers > 0) {
            snprintf(buf, sizeof(buf), "%s %.1f%%", TR("syncing"), state.sync.verification_progress * 100.0);
            statusText = buf;
            pillCol = Warning();
        } else {
            statusText = TR("connected");
        }

        float maxTextW = cardW * 0.34f;
        statusText = truncateHashToFit(statusText, capFont, maxTextW);
        ImVec2 pillPad(Layout::spacingSm(), Layout::spacingXs());
        ImVec2 pillSz = material::PillSize(statusText.c_str(), capFont, pillPad);
        ImVec2 pillMin(cardMin.x + cardW - pad - pillSz.x, cardMin.y + pad * 0.5f);
        material::DrawPill(dl, pillMin, statusText.c_str(), capFont, pillCol,
                           WithAlpha(pillCol, 32), WithAlpha(pillCol, 110), pillPad);
    };

    auto drawChainTip = [&](const ImVec2& cardMin, float cardW) {
        ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
        DrawGlassPanel(dl, cardMin, cardMax, glassSpec);

        dl->AddText(ovFont, ovFont->LegacySize,
            ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_chain_stats"));
        drawStatusPill(cardMin, cardW);

        // Distribute the two stat blocks (Height, Best Block) evenly through the
        // content region so the card matches the density of the sibling 2x2 metric
        // grid instead of pinning Height to the top and Best Block to the bottom
        // edge with a large empty gap between them.
        float contentTop = cardMin.y + pad * 0.5f + headerH;
        float contentBottom = cardMax.y - pad;
        float blockGap = std::max(Layout::spacingMd(),
            (contentBottom - contentTop - heroLineH - hashLineH) / 3.0f);

        float labelY = contentTop + blockGap;
        dl->AddText(capFont, capFont->LegacySize,
            ImVec2(cardMin.x + pad, labelY), OnSurfaceMedium(), TR("explorer_block_height"));

        snprintf(buf, sizeof(buf), "%d", state.sync.blocks);
        float valueY = labelY + capFont->LegacySize + Layout::spacingXs();
        DrawTextShadow(dl, heroFont, heroFont->LegacySize,
            ImVec2(cardMin.x + pad, valueY), OnSurface(), buf);

        if (state.sync.syncing && state.sync.headers > 0) {
            float progress = std::clamp(static_cast<float>(state.sync.verification_progress), 0.0f, 1.0f);
            float barY = valueY + heroFont->LegacySize + Layout::spacingMd();
            float barH = std::max(3.0f * dp, 3.0f);
            float barW = cardW - pad * 2.0f;
            dl->AddRectFilled(ImVec2(cardMin.x + pad, barY),
                ImVec2(cardMin.x + pad + barW, barY + barH), WithAlpha(OnSurface(), 18), barH * 0.5f);
            dl->AddRectFilled(ImVec2(cardMin.x + pad, barY),
                ImVec2(cardMin.x + pad + barW * progress, barY + barH), WithAlpha(Warning(), 180), barH * 0.5f);
        }

        float hashLabelY = labelY + heroLineH + blockGap;
        dl->AddText(capFont, capFont->LegacySize,
            ImVec2(cardMin.x + pad, hashLabelY), OnSurfaceMedium(), TR("peers_best_block"));

        float hashY = hashLabelY + capFont->LegacySize + Layout::spacingXs();
        float copyW = iconFont->LegacySize + Layout::spacingSm() * 2.0f;
        float hashMaxW = cardW - pad * 2.0f - (state.sync.best_blockhash.empty() ? 0.0f : copyW + Layout::spacingSm());
        std::string hashDisp = state.sync.best_blockhash.empty()
            ? std::string("--")
            : truncateHashToFit(state.sync.best_blockhash, sub1, hashMaxW);
        dl->AddText(sub1, sub1->LegacySize,
            ImVec2(cardMin.x + pad, hashY), OnSurface(), hashDisp.c_str());

        if (!state.sync.best_blockhash.empty()) {
            ImVec2 savedCursor = ImGui::GetCursorScreenPos();
            copyButton("ChainTipBestHash", state.sync.best_blockhash,
                cardMax.x - pad - copyW, hashY);
            ImGui::SetCursorScreenPos(savedCursor);
        }
    };

    auto drawMetricGrid = [&](const ImVec2& cardMin, float cardW) {
        ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
        DrawGlassPanel(dl, cardMin, cardMax, glassSpec);

        struct MetricSpec {
            const char* icon;
            const char* label;
            std::string value;
            std::string detail;
            ImU32 accent;
        };

        snprintf(buf, sizeof(buf), "%.4f", state.mining.difficulty);
        std::string difficulty = buf;
        snprintf(buf, sizeof(buf), "%d", state.notarized);
        std::string notarized = buf;
        snprintf(buf, sizeof(buf), "%d", s_mempool_tx_count);
        std::string mempoolTxs = s_mempool_loading ? std::string("...") : std::string(buf);

        MetricSpec metrics[4] = {
            { ICON_MD_SPEED, TR("difficulty"), difficulty, "", Warning() },
            { ICON_MD_SHOW_CHART, TR("peers_hashrate"), FormatHashrate(state.mining.networkHashrate), "", Success() },
            { ICON_MD_CONFIRMATION_NUMBER, TR("peers_notarized"), notarized, "", Primary() },
            { ICON_MD_STORAGE, TR("explorer_mempool"), mempoolTxs, s_mempool_loading ? std::string("") : formatSize(static_cast<int>(s_mempool_size)), Secondary() },
        };

        float gridX = cardMin.x + pad;
        float gridY = cardMin.y + pad;
        float gridW = cardW - pad * 2.0f;
        float gridH = cardH - pad * 2.0f;
        float cellW = gridW * 0.5f;
        float cellH = gridH * 0.5f;
        ImU32 dividerCol = WithAlpha(OnSurface(), 22);

        dl->AddLine(ImVec2(gridX + cellW, gridY), ImVec2(gridX + cellW, gridY + gridH), dividerCol, 1.0f * dp);
        dl->AddLine(ImVec2(gridX, gridY + cellH), ImVec2(gridX + gridW, gridY + cellH), dividerCol, 1.0f * dp);

        for (int i = 0; i < 4; ++i) {
            int col = i % 2;
            int row = i / 2;
            ImVec2 cellMin(gridX + col * cellW, gridY + row * cellH);
            ImVec2 cellMax(cellMin.x + cellW, cellMin.y + cellH);
            float inset = Layout::spacingSm();
            float textX = cellMin.x + inset;
            float textY = cellMin.y + inset;

            dl->AddText(iconFont, iconFont->LegacySize,
                ImVec2(cellMax.x - inset - iconFont->LegacySize, textY),
                WithAlpha(metrics[i].accent, 180), metrics[i].icon);
            dl->AddText(capFont, capFont->LegacySize,
                ImVec2(textX, textY), OnSurfaceMedium(), metrics[i].label);

            float valueY = textY + capFont->LegacySize + Layout::spacingXs();
            float valueMaxW = cellW - inset * 2.0f;
            std::string value = truncateHashToFit(metrics[i].value, sub1, valueMaxW);
            dl->AddText(sub1, sub1->LegacySize,
                ImVec2(textX, valueY), OnSurface(), value.c_str());

            if (!metrics[i].detail.empty()) {
                std::string detail = truncateHashToFit(metrics[i].detail, capFont, valueMaxW);
                dl->AddText(capFont, capFont->LegacySize,
                    ImVec2(textX, valueY + sub1->LegacySize + Layout::spacingXs()),
                    OnSurfaceDisabled(), detail.c_str());
            }
        }
    };

    drawChainTip(basePos, chainW);

    if (stacked) {
        drawMetricGrid(ImVec2(basePos.x, basePos.y + cardH + gap), metricsW);
        ImGui::Dummy(ImVec2(availWidth, cardH * 2.0f + gap + Layout::spacingMd()));
    } else {
        drawMetricGrid(ImVec2(basePos.x + chainW + gap, basePos.y), metricsW);
        ImGui::Dummy(ImVec2(availWidth, cardH + Layout::spacingMd()));
    }
}

static void renderRecentBlocks(App* app, float availWidth) {
    auto& S = schema::UI();
    float pad = Layout::cardInnerPadding();
    float dp = Layout::dpiScale();
    const auto& state = app->getWalletState();
    validateRecentBlockCache(app, state);

    ImDrawList* dl = ImGui::GetWindowDrawList();
    GlassPanelSpec glassSpec;
    glassSpec.rounding = Layout::glassRounding();

    ImFont* ovFont = Type().overline();
    ImFont* capFont = Type().caption();
    ImFont* body2 = Type().body2();
    ImFont* sub1 = Type().subtitle1();

    float baseRowH = S.drawElement("tabs.explorer", "row-height").size * dp;
    float rowRound = S.drawElement("tabs.explorer", "row-rounding").size * dp;
    float headerH = ovFont->LegacySize + Layout::spacingSm() + pad * 0.5f;

    // Stretch card to fill the remaining tab height; rows scroll inside.
    float maxRows = 10.0f;
    float contentH = capFont->LegacySize + Layout::spacingXs() + baseRowH * maxRows;
    float minTableH = headerH + contentH + pad;
    float remainingH = ImGui::GetContentRegionAvail().y;
    float tableH = std::max(minTableH, remainingH - Layout::spacingSm());

    ImVec2 cardMin = ImGui::GetCursorScreenPos();
    ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + tableH);
    DrawGlassPanel(dl, cardMin, cardMax, glassSpec);

    dl->AddText(ovFont, ovFont->LegacySize,
        ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("explorer_recent_blocks"));

    float hdrY = cardMin.y + pad * 0.5f + ovFont->LegacySize + Layout::spacingSm();
    float rowAreaTop = hdrY + capFont->LegacySize + Layout::spacingXs();
    float rowAreaH = cardMax.y - rowAreaTop - pad * 0.5f;
    int rowsPerPage = std::max(1, (int)std::ceil(rowAreaH / std::max(1.0f, baseRowH)));
    float rowH = baseRowH;

    // Anchor the list at a search result when one is active, otherwise at the chain tip.
    int anchorTip = (s_search_target_height > 0 && s_search_target_height <= state.sync.blocks)
                        ? s_search_target_height : state.sync.blocks;

    int maxPage = (anchorTip > 0) ? (anchorTip - 1) / rowsPerPage : 0;
    s_recent_max_page = maxPage;
    if (s_recent_page > maxPage) s_recent_page = maxPage;
    if (s_recent_page < 0) s_recent_page = 0;

    int startHeight = anchorTip - s_recent_page * rowsPerPage;
    std::vector<int> pageHeights;
    pageHeights.reserve(rowsPerPage);
    for (int i = 0; i < rowsPerPage; ++i) {
        int height = startHeight - i;
        if (height < 1) break;
        pageHeights.push_back(height);
    }

    std::map<int, BlockSummary> cachedBlocks;
    if (!pageHeights.empty()) {
        cachedBlocks = s_recent_block_cache.loadRange(pageHeights.back(), pageHeights.front());
    }

    // Fuzzy mode: a non-numeric, non-full-hash query filters the list to cached blocks whose hash
    // (or height text) contains it. Exact queries (a height or a full 64-char hash) still navigate
    // via performSearch/the anchor above. Results are memoized per query (no per-frame SQLite).
    const std::string& fq = s_active_search_query;
    bool fqDigits = !fq.empty() && std::all_of(fq.begin(), fq.end(), ::isdigit);
    bool fqFullHash = fq.size() == 64 && std::all_of(fq.begin(), fq.end(), [](char c) {
        return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
    });
    bool fuzzyMode = !fq.empty() && !fqDigits && !fqFullHash;
    if (fuzzyMode) {
        if (fq != s_fuzzy_query_cached) {
            s_fuzzy_results = s_recent_block_cache.searchBlocks(fq, kMaxFuzzyResults);
            s_fuzzy_query_cached = fq;
        }
        s_recent_max_page = 0;  // fuzzy results aren't paginated; they scroll
    }

    bool canFetchBlocks = !fuzzyMode && state.sync.blocks > 0 &&
        !s_show_detail_modal && !s_detail_loading && !s_tx_loading;
    if (canFetchBlocks) {
        bool currentPageComplete = true;
        bool scheduledFetch = false;
        auto* rpc = app->rpc();
        if (rpc && rpc->isConnected()) {
            for (int height : pageHeights) {
                if (cachedBlocks.find(height) == cachedBlocks.end()) {
                    currentPageComplete = false;
                    scheduledFetch = fetchRecentBlock(app, height) || scheduledFetch;
                }
            }
            if (currentPageComplete && s_recent_page < maxPage) {
                std::vector<int> nextPageHeights;
                nextPageHeights.reserve(rowsPerPage);
                int nextStartHeight = anchorTip - (s_recent_page + 1) * rowsPerPage;
                for (int i = 0; i < rowsPerPage; ++i) {
                    int height = nextStartHeight - i;
                    if (height < 1) break;
                    nextPageHeights.push_back(height);
                }
                std::map<int, BlockSummary> nextCachedBlocks;
                if (!nextPageHeights.empty()) {
                    nextCachedBlocks = s_recent_block_cache.loadRange(nextPageHeights.back(), nextPageHeights.front());
                }
                for (int height : nextPageHeights) {
                    if (nextCachedBlocks.find(height) == nextCachedBlocks.end()) {
                        scheduledFetch = fetchRecentBlock(app, height) || scheduledFetch;
                    }
                }
            }
        }
        if (scheduledFetch) {
            fetchMempoolInfo(app);
        }
    }

    struct RecentBlockRow {
        int height = 0;
        const BlockSummary* block = nullptr;
    };

    std::vector<RecentBlockRow> blocks;
    if (fuzzyMode) {
        blocks.reserve(s_fuzzy_results.size());
        for (const auto& b : s_fuzzy_results) {
            blocks.push_back({b.height, &b});
        }
    } else {
        blocks.reserve(pageHeights.size());
        for (int height : pageHeights) {
            auto it = cachedBlocks.find(height);
            blocks.push_back({height,
                              it != cachedBlocks.end() ? &it->second : nullptr});
        }
    }
    // Responsive column layout — give height more room, use remaining for data
    float innerW = availWidth - pad * 2;
    float colHeight = pad;
    float colTxs   = colHeight + innerW * 0.14f;
    float colSize  = colHeight + innerW * 0.24f;
    float colDiff  = colHeight + innerW * 0.38f;
    float colHash  = colHeight + innerW * 0.56f;
    float colTime  = colHeight + innerW * 0.82f;

    dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colHeight, hdrY), OnSurfaceMedium(), TR("explorer_block_height"));
    dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colTxs, hdrY), OnSurfaceMedium(), TR("explorer_block_txs"));
    dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colSize, hdrY), OnSurfaceMedium(), TR("explorer_block_size"));
    dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colDiff, hdrY), OnSurfaceMedium(), TR("difficulty"));
    dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colHash, hdrY), OnSurfaceMedium(), TR("explorer_block_hash"));
    dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMin.x + colTime, hdrY), OnSurfaceMedium(), TR("explorer_block_time"));

    // Scrollable child region for rows
    ImGui::SetCursorScreenPos(ImVec2(cardMin.x, rowAreaTop));

    int parentVtx = dl->VtxBuffer.Size;
    ImGui::BeginChild("##BlockRows", ImVec2(availWidth, rowAreaH), false,
        ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
    ApplySmoothScroll();

    ImDrawList* childDL = ImGui::GetWindowDrawList();
    int childVtx = childDL->VtxBuffer.Size;
    float scrollY = ImGui::GetScrollY();
    float scrollMaxY = ImGui::GetScrollMaxY();

    char buf[128];
    float rowInset = 2 * dp;

    if (blocks.empty()) {
        ImGui::SetCursorPosY(Layout::spacingMd());
        ImGui::TextDisabled("%s", fuzzyMode ? TR("explorer_no_results") : TR("loading"));
    } else {
        for (size_t i = 0; i < blocks.size(); i++) {
            const auto& row = blocks[i];
            const auto* bs = row.block;
            int blockHeight = bs ? bs->height : row.height;
            ImVec2 rowPos = ImGui::GetCursorScreenPos();
            float rowW = ImGui::GetContentRegionAvail().x - rowInset * 2;

            ImGui::SetCursorScreenPos(ImVec2(rowPos.x + rowInset, rowPos.y));

            // InvisibleButton for proper interaction
            ImGui::PushID(static_cast<int>(i));
            ImGui::InvisibleButton("##blkRow", ImVec2(rowW, rowH));
            bool hovered = ImGui::IsItemHovered();
            bool clicked = ImGui::IsItemClicked(0);
            ImGui::PopID();

            ImVec2 rowMin(rowPos.x + rowInset, rowPos.y);
            ImVec2 rowMax(rowMin.x + rowW, rowMin.y + rowH);

            // Subtle alternating row background for readability
            if (i % 2 == 0) {
                childDL->AddRectFilled(rowMin, rowMax, WithAlpha(OnSurface(), 6), rowRound);
            }

            // Hover highlight — clear clickable feedback
            if (hovered && bs) {
                childDL->AddRectFilled(rowMin, rowMax, WithAlpha(Primary(), 20), rowRound);
                childDL->AddRect(rowMin, rowMax, WithAlpha(Primary(), 40), rowRound);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
            }

            // Selected highlight
            if (bs && s_show_detail_modal && s_detail_height == bs->height) {
                childDL->AddRectFilled(rowMin, rowMax, WithAlpha(Primary(), 25), rowRound);
            }

            // Click opens block detail modal
            if (bs && clicked && app->rpc() && app->rpc()->isConnected()) {
                fetchBlockDetail(app, bs->height);
            }

            float textY = rowPos.y + (rowH - sub1->LegacySize) * 0.5f;
            float textY2 = rowPos.y + (rowH - body2->LegacySize) * 0.5f;

            // Height — emphasized with larger font and primary color
            snprintf(buf, sizeof(buf), "#%d", blockHeight);
            childDL->AddText(sub1, sub1->LegacySize, ImVec2(cardMin.x + colHeight, textY),
                (hovered && bs) ? Primary() : schema::UI().resolveColor("var(--primary-light)"), buf);

            // Tx count
            if (bs) snprintf(buf, sizeof(buf), "%d tx", bs->tx_count);
            else snprintf(buf, sizeof(buf), "...");
            childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colTxs, textY2), OnSurface(), buf);

            // Size
            std::string sizeStr = bs ? formatSize(bs->size) : "...";
            childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colSize, textY2), OnSurface(), sizeStr.c_str());

            // Difficulty
            if (bs) snprintf(buf, sizeof(buf), "%.2f", bs->difficulty);
            else snprintf(buf, sizeof(buf), "...");
            childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colDiff, textY2), OnSurface(), buf);

            // Hash — adaptive to available column width
            float hashMaxW = (colTime - colHash) - Layout::spacingSm();
            std::string hashDisp = bs ? truncateHashToFit(bs->hash, body2, hashMaxW) : "...";
            childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colHash, textY2),
                OnSurfaceMedium(), hashDisp.c_str());

            // Time
            const char* timeStr = bs ? relativeTime(bs->time) : "...";
            childDL->AddText(body2, body2->LegacySize, ImVec2(cardMin.x + colTime, textY2), OnSurfaceMedium(), timeStr);
        }
    }

    ImGui::EndChild();

    float fadeZone = S.drawElement("tabs.explorer", "scroll-fade-zone").size * dp;
    ApplyScrollEdgeMask(dl, parentVtx, childDL, childVtx,
                        rowAreaTop, rowAreaTop + rowAreaH, fadeZone, scrollY, scrollMaxY);

    ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
    ImGui::Dummy(ImVec2(availWidth, Layout::spacingMd()));
}

static void renderBlockDetailModal(App* app) {
    if (!s_show_detail_modal && !s_detail_loading) return;

    auto& S = schema::UI();
    float pad = Layout::cardInnerPadding();

    ImFont* ovFont = Type().overline();
    ImFont* capFont = Type().caption();
    ImFont* sub1 = Type().subtitle1();
    ImFont* body2 = Type().body2();

    float modalW = S.drawElement("tabs.explorer", "detail-modal-width").sizeOr(700.0f);

    if (s_detail_loading && !s_show_detail_modal) {
        // Show a loading modal while fetching
        OverlayDialogSpec lov;
        lov.title = TR("loading"); lov.p_open = &s_detail_loading;
        lov.style = OverlayStyle::BlurFloat;
        lov.cardWidth = 300.0f; lov.idSuffix = "explorerloading";
        if (BeginOverlayDialog(lov)) {
            ImGui::Spacing();
            char loadBuf[64];
            snprintf(loadBuf, sizeof(loadBuf), "%s%s", TR("loading"), material::LoadingDots());
            ImGui::TextDisabled("%s", loadBuf);
            ImGui::Spacing();
            EndOverlayDialog();
        }
        return;
    }

    OverlayDialogSpec ov;
    ov.title = TR("explorer_block_detail"); ov.p_open = &s_show_detail_modal;
    ov.style = OverlayStyle::BlurFloat;   // floating content on the blur, plain heading
    ov.cardWidth = modalW; ov.cardBottomViewportRatio = 0.90f;   // keep authored width
    if (!BeginOverlayDialog(ov))
        return;

    float contentW = ImGui::GetContentRegionAvail().x;
    float hashMaxW = contentW - pad * 2 - Type().iconSmall()->LegacySize;
    char buf[256];

    // ── Header: "Block #123456" + nav buttons ──
    {
        ImGui::PushFont(sub1);
        snprintf(buf, sizeof(buf), "%s #%d", TR("explorer_block_detail"), s_detail_height);
        ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", buf);
        ImGui::PopFont();

        // Nav buttons on same line
        ImGui::SameLine(contentW - Layout::spacingXl() * 3);

        // Disable prev/next while a fetch is in flight so navigation can't stack fetches.
        ImGui::BeginDisabled(s_detail_loading);

        // Prev
        if (s_detail_height > 1) {
            ImGui::PushFont(Type().iconMed());
            ImGui::PushID("prevBlock");
            if (material::TactileSmallButton(ICON_MD_CHEVRON_LEFT)) {
                if (app->rpc() && app->rpc()->isConnected())
                    fetchBlockDetail(app, s_detail_height - 1);
            }
            if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("explorer_prev_block"));
            ImGui::PopID();
            ImGui::PopFont();
            ImGui::SameLine();
        }

        // Next
        if (!s_detail_next_hash.empty()) {
            ImGui::PushFont(Type().iconMed());
            ImGui::PushID("nextBlock");
            if (material::TactileSmallButton(ICON_MD_CHEVRON_RIGHT)) {
                if (app->rpc() && app->rpc()->isConnected())
                    fetchBlockDetail(app, s_detail_height + 1);
            }
            if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("explorer_next_block"));
            ImGui::PopID();
            ImGui::PopFont();
        }

        ImGui::EndDisabled();
    }

    ImGui::Spacing();

    // Show loading state inside the modal when navigating between blocks
    if (s_detail_loading) {
        snprintf(buf, sizeof(buf), "%s%s", TR("loading"), material::LoadingDots());
        ImGui::TextDisabled("%s", buf);
        EndOverlayDialog();
        return;
    }

    // ── Block Hash ──
    {
        ImGui::PushFont(capFont);
        ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("explorer_block_hash"));
        ImGui::PopFont();

        std::string hashDisp = truncateHashToFit(s_detail_hash, body2, hashMaxW);
        ImGui::PushFont(body2);
        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor("var(--secondary-light)")));
        ImGui::TextWrapped("%s", hashDisp.c_str());
        ImGui::PopStyleColor();
        ImGui::PopFont();

        ImGui::SameLine();
        copyButton("cpHash", s_detail_hash,
            ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y);

        if (ImGui::IsItemHovered())
            material::Tooltip("%s", s_detail_hash.c_str());
    }

    ImGui::Spacing();

    // ── Info grid ──
    ImDrawList* dl = ImGui::GetWindowDrawList();
    float dp = Layout::dpiScale();
    float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
    float labelW = S.drawElement("tabs.explorer", "label-column").size * dp;
    {
        ImVec2 gridPos = ImGui::GetCursorScreenPos();
        float gx = gridPos.x;
        float gy = gridPos.y;
        float halfW = contentW * 0.5f;

        // Row 1: Timestamp | Confirmations
        drawLabelValue(dl, gx, gy, labelW, TR("block_timestamp"), "", capFont, sub1);
        if (s_detail_time > 0) {
            const std::string tstr = dragonx::util::formatClockDateTime(s_detail_time, /*withSeconds=*/true);
            dl->AddText(sub1, sub1->LegacySize, ImVec2(gx + labelW, gy), OnSurface(), tstr.c_str());
        }
        dl->AddText(capFont, capFont->LegacySize, ImVec2(gx + halfW, gy), OnSurfaceMedium(), TR("confirmations"));
        snprintf(buf, sizeof(buf), "%d", s_detail_confirmations);
        dl->AddText(sub1, sub1->LegacySize, ImVec2(gx + halfW + labelW * 0.7f, gy), OnSurface(), buf);
        gy += rowH + Layout::spacingSm();

        // Row 2: Size | Difficulty
        drawLabelValue(dl, gx, gy, labelW, TR("block_size"),
            formatSize(s_detail_size).c_str(), capFont, sub1);
        dl->AddText(capFont, capFont->LegacySize, ImVec2(gx + halfW, gy), OnSurfaceMedium(), TR("difficulty"));
        snprintf(buf, sizeof(buf), "%.4f", s_detail_difficulty);
        dl->AddText(sub1, sub1->LegacySize, ImVec2(gx + halfW + labelW * 0.7f, gy), OnSurface(), buf);
        gy += rowH + Layout::spacingSm();

        // Row 3: Bits
        drawLabelValue(dl, gx, gy, labelW, TR("block_bits"), s_detail_bits.c_str(), capFont, sub1);
        gy += rowH + Layout::spacingSm();

        // Row 4: Merkle Root
        dl->AddText(capFont, capFont->LegacySize, ImVec2(gx, gy), OnSurfaceMedium(), TR("explorer_block_merkle"));
        gy += capFont->LegacySize + Layout::spacingXs();
        {
            float merkleMaxW = contentW - Type().iconSmall()->LegacySize - Layout::spacingMd();
            std::string merkleTrunc = truncateHashToFit(s_detail_merkle, body2, merkleMaxW);
            dl->AddText(body2, body2->LegacySize, ImVec2(gx, gy),
                OnSurfaceMedium(), merkleTrunc.c_str());
        }
        gy += body2->LegacySize + Layout::spacingXs();

        // Advance ImGui cursor past the drawlist content
        ImGui::SetCursorScreenPos(ImVec2(gridPos.x, gy));
    }

    // Copy merkle button
    copyButton("cpMerkle", s_detail_merkle,
        ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x - Layout::spacingXl(),
        ImGui::GetCursorScreenPos().y - body2->LegacySize - Layout::spacingSm());

    ImGui::Spacing();

    // ── Transactions ──
    {
        ImGui::PushFont(ovFont);
        snprintf(buf, sizeof(buf), "%s (%d)", TR("explorer_block_txs"), (int)s_detail_txids.size());
        ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", buf);
        ImGui::PopFont();

        ImGui::Spacing();

        float txRowH = S.drawElement("tabs.explorer", "tx-row-height").size * dp;
        ImU32 linkCol = schema::UI().resolveColor("var(--secondary-light)");

        for (int i = 0; i < (int)s_detail_txids.size(); i++) {
            const auto& txid = s_detail_txids[i];
            bool isExpanded = (s_expanded_tx_idx == i);

            ImGui::PushID(i);

            // Expand/collapse icon + txid on one row via InvisibleButton
            ImFont* iconFont = Type().iconSmall();
            const char* expandIcon = isExpanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;

            ImVec2 rowStart = ImGui::GetCursorScreenPos();
            float txContentW = ImGui::GetContentRegionAvail().x;

            ImGui::InvisibleButton("##txRow", ImVec2(txContentW, txRowH));
            bool rowHovered = ImGui::IsItemHovered();
            bool rowClicked = ImGui::IsItemClicked(0);
            bool rightClicked = ImGui::IsItemClicked(1);

            ImDrawList* txDL = ImGui::GetWindowDrawList();

            // Hover highlight
            if (rowHovered) {
                txDL->AddRectFilled(rowStart,
                    ImVec2(rowStart.x + txContentW, rowStart.y + txRowH),
                    WithAlpha(OnSurface(), 10),
                    S.drawElement("tabs.explorer", "row-rounding").size * dp);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                material::Tooltip("%s", txid.c_str());
            }

            // Draw icon + text
            float iconY = rowStart.y + (txRowH - iconFont->LegacySize) * 0.5f;
            txDL->AddText(iconFont, iconFont->LegacySize,
                ImVec2(rowStart.x, iconY), OnSurfaceMedium(), expandIcon);

            float txTextX = rowStart.x + iconFont->LegacySize + Layout::spacingXs();
            float txMaxW = txContentW - iconFont->LegacySize - Layout::spacingXl() * 3;
            std::string txTrunc = truncateHashToFit(txid, body2, txMaxW);
            float textY = rowStart.y + (txRowH - body2->LegacySize) * 0.5f;
            txDL->AddText(body2, body2->LegacySize, ImVec2(txTextX, textY), linkCol, txTrunc.c_str());

            // Copy icon at end of row
            float copyX = rowStart.x + txContentW - Layout::spacingXl();
            if (rowHovered) {
                float copyY = rowStart.y + (txRowH - iconFont->LegacySize) * 0.5f;
                txDL->AddText(iconFont, iconFont->LegacySize,
                    ImVec2(copyX, copyY), OnSurfaceMedium(), ICON_MD_CONTENT_COPY);
            }

            if (rowClicked) {
                if (isExpanded) {
                    s_expanded_tx_idx = -1;
                } else {
                    s_expanded_tx_idx = i;
                    fetchTxDetail(app, txid);
                }
            }
            if (rightClicked) {
                ImGui::SetClipboardText(txid.c_str());
                Notifications::instance().success(TR("copied_to_clipboard"));
            }

            // ── Expanded transaction detail ──
            if (isExpanded) {
                float indent = Layout::spacingXl();
                if (s_tx_loading) {
                    ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent);
                    ImGui::TextDisabled("%s", TR("loading"));
                } else if (!s_tx_detail.is_null()) {
                    ImGui::Indent(indent);

                    // Full TxID with copy
                    ImGui::PushFont(capFont);
                    ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", TR("explorer_txid"));
                    ImGui::PopFont();
                    ImGui::PushFont(body2);
                    ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(linkCol));
                    ImGui::TextWrapped("%s", txid.c_str());
                    ImGui::PopStyleColor();
                    ImGui::PopFont();

                    // Size
                    if (s_tx_detail.contains("size")) {
                        ImGui::PushFont(capFont);
                        ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()),
                            "%s: %s", TR("explorer_tx_size"),
                            formatSize(s_tx_detail.value("size", 0)).c_str());
                        ImGui::PopFont();
                    }

                    // Outputs
                    if (s_tx_detail.contains("vout") && s_tx_detail["vout"].is_array()) {
                        ImGui::PushFont(capFont);
                        snprintf(buf, sizeof(buf), "%s (%d):", TR("explorer_tx_outputs"),
                            (int)s_tx_detail["vout"].size());
                        ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()), "%s", buf);
                        ImGui::PopFont();

                        ImGui::PushFont(body2);
                        for (const auto& vout : s_tx_detail["vout"]) {
                            double val = vout.value("value", 0.0);
                            int n = vout.value("n", 0);
                            std::string addr = TR("shielded");
                            if (vout.contains("scriptPubKey") && vout["scriptPubKey"].contains("addresses") &&
                                vout["scriptPubKey"]["addresses"].is_array() && !vout["scriptPubKey"]["addresses"].empty()) {
                                addr = vout["scriptPubKey"]["addresses"][0].get<std::string>();
                            }
                            float addrMaxW = ImGui::GetContentRegionAvail().x * 0.5f;
                            std::string addrDisp = truncateHashToFit(addr, body2, addrMaxW);
                            snprintf(buf, sizeof(buf), "[%d] %s  %.8f DRGX", n, addrDisp.c_str(), val);
                            ImGui::TextUnformatted(buf);
                        }
                        ImGui::PopFont();
                    }

                    ImGui::Spacing();
                    ImGui::Unindent(indent);
                }
            }

            ImGui::PopID();
        }

        if (s_detail_txids.size() > 100) {
            snprintf(buf, sizeof(buf), TR("grpa_showing_first_100_of"), (int)s_detail_txids.size());
            ImGui::TextDisabled("%s", buf);
        }
    }

    EndOverlayDialog();
}

// ─── Main Entry Point ──────────────────────────────────────────

void RenderExplorerTab(App* app)
{
    ImVec2 avail = ImGui::GetContentRegionAvail();
    ImGui::BeginChild("##ExplorerScroll", avail, false,
        ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);

    float availWidth = ImGui::GetContentRegionAvail().x;

    renderChainStats(app, availWidth);
    renderSearchBar(app, availWidth);
    renderRecentBlocks(app, availWidth);

    ImGui::EndChild();

    // Block detail modal — rendered outside the scroll region so it's fullscreen
    renderBlockDetailModal(app);
}

} // namespace ui
} // namespace dragonx
