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

#include "peers_tab.h"
#include "../../app.h"
#include "../../data/wallet_state.h"
#include "../../util/i18n.h"
#include "mining_tab_helpers.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../effects/low_spec.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 <string>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <unordered_set>

namespace dragonx {
namespace ui {

using namespace material;

// Track selected peer for ban action
static int s_selected_peer_idx = -1;
static int s_selected_banned_idx = -1;

// Helper: Extract IP without port
static std::string ExtractIP(const std::string& addr)
{
    if (addr.empty()) return addr;
    std::string ip = addr;
    if (ip[0] == '[') {
        auto pos = ip.rfind("]:");
        if (pos != std::string::npos) ip = ip.substr(1, pos - 1);
    } else {
        auto pos = ip.rfind(':');
        if (pos != std::string::npos) ip = ip.substr(0, pos);
    }
    return ip;
}

// Known seed/addnode IPs for the DragonX network.
// These are the official seed nodes that the daemon connects to on startup.
static bool IsSeedNode(const std::string& addr) {
    static const std::unordered_set<std::string> seeds = {
        "176.126.87.241",   // embedded daemon -addnode
        "94.72.112.24",     // node1.hush.is
        "37.60.252.160",    // node2.hush.is
        "176.57.70.185",    // node3.hush.is / node6.hush.is
        "185.213.209.89",   // node4.hush.is
        "137.74.4.198",     // node5.hush.is
        "18.193.113.121",   // node7.hush.is
        "38.60.224.94",     // node8.hush.is
    };
    return seeds.count(ExtractIP(addr)) > 0;
}

// Draw one label/value stat cell in an info card. The caption label sits on the first line
// (OnSurfaceMedium); the value sits below it (valueFont) — or an em-dash "—" in the disabled
// color when the value is null/empty. Matches the hand-positioned blocks it replaces exactly:
// value baseline is ry + capFont->LegacySize + spacingXs().
static void drawStatCell(ImDrawList* dl, float cx, float ry, ImFont* capFont, ImFont* valueFont,
                         const char* label, const char* valueText, ImU32 valueCol)
{
    dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), label);
    float valY = ry + capFont->LegacySize + Layout::spacingXs();
    if (valueText && valueText[0]) {
        dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), valueCol, valueText);
    } else {
        dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
    }
}

// Draw a subtle horizontal divider between info-card rows, inset to the card's rounded corners.
static void drawCardDivider(ImDrawList* dl, float leftX, float rightX, float y, float rounding, float dp)
{
    dl->AddLine(ImVec2(leftX + rounding * 0.5f, y),
                ImVec2(rightX - rounding * 0.5f, y),
                WithAlpha(OnSurface(), 15), 1.0f * dp);
}

void RenderPeersTab(App* app)
{
    auto& S = schema::UI();
    auto peerTable = S.table("tabs.peers", "peer-table");
    auto bannedTable = S.table("tabs.peers", "banned-table");
    const auto& state = app->getWalletState();

    // Scrollable child to contain all content within available space
    ImVec2 peersAvail = ImGui::GetContentRegionAvail();
    // NoScrollWithMouse: the inner ##PeersList owns wheel scrolling (via ApplySmoothScroll). Without
    // this, a wheel over the list would scroll BOTH the list (smooth-scroll) and this outer container
    // (ImGui forwards the NoScrollWithMouse child's wheel to its scrollable ancestor) — a double-scroll.
    // Safe because the peer panel is sized to fill the remaining height, so this outer never overflows.
    ImGui::BeginChild("##PeersScroll", peersAvail, false,
                      ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);

    // Responsive: scale factors per frame
    float availWidth = ImGui::GetContentRegionAvail().x;
    float hs = Layout::hScale(availWidth);
    float pad = Layout::cardInnerPadding();
    float gap = Layout::cardGap();

    ImDrawList* dl = ImGui::GetWindowDrawList();
    GlassPanelSpec glassSpec;
    glassSpec.rounding = Layout::glassRounding();
    ImFont* ovFont  = Type().overline();
    ImFont* capFont = Type().caption();
    ImFont* sub1    = Type().subtitle1();
    ImFont* body2   = Type().body2();

    char buf[128];

    // ================================================================
    // BLOCKCHAIN & PEERS CARDS — Side by side
    // ================================================================
    float infoCardsH = 0;
    {
        const auto& mining = state.mining;

        // Compute peer stats
        int totalPeers = (int)state.peers.size();
        int inboundCount = 0;
        int outboundCount = 0;
        double totalPing = 0;
        int64_t totalBytesSent = 0, totalBytesRecv = 0;
        int tlsCount = 0;
        for (const auto& p : state.peers) {
            if (p.inbound) inboundCount++;
            else outboundCount++;
            totalPing += p.pingtime;
            totalBytesSent += p.bytessent;
            totalBytesRecv += p.bytesrecv;
            if (!p.tls_cipher.empty()) tlsCount++;
        }
        double avgPing = totalPeers > 0 ? (totalPing / totalPeers) * 1000.0 : 0;

        // Format bytes helper
        auto fmtBytes = [](int64_t bytes) -> std::string {
            char b[32];
            if (bytes >= 1073741824LL)
                snprintf(b, sizeof(b), "%.1f GB", bytes / 1073741824.0);
            else if (bytes >= 1048576LL)
                snprintf(b, sizeof(b), "%.1f MB", bytes / 1048576.0);
            else if (bytes >= 1024LL)
                snprintf(b, sizeof(b), "%.0f KB", bytes / 1024.0);
            else
                snprintf(b, sizeof(b), "%lld B", (long long)bytes);
            return b;
        };

        // Blockchain card: 5 rows, Peers card: 4 rows (2 cols per row)
        float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize;
        float headerH = ovFont->LegacySize + Layout::spacingSm();
        float dividerH = 1.0f * Layout::dpiScale();
        // Use 5 rows for blockchain card (peers card will have empty space at bottom)
        float cardInnerH = pad * 0.5f + headerH + rowH * 5 + (Layout::spacingSm() + dividerH) * 4 + pad * 0.5f;
        infoCardsH = cardInnerH;

        float cardW = (availWidth - gap) * 0.5f;
        ImVec2 basePos = ImGui::GetCursorScreenPos();
        float dp = Layout::dpiScale();

        // ================================================================
        // BLOCKCHAIN CARD (left)
        // ================================================================
        {
            ImVec2 cardMin = basePos;
            ImVec2 cardMax(cardMin.x + cardW, cardMin.y + infoCardsH);
            DrawGlassPanel(dl, cardMin, cardMax, glassSpec);

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

            float colW = (cardW - pad * 2) / 2.0f;
            float leftX  = cardMin.x + pad;
            float rightX = cardMin.x + pad + colW;
            float ry = cardMin.y + pad * 0.5f + headerH;

            // Advance ry past a row and draw a divider before the next one. Keeps the row
            // stepping + divider in one place (was a per-row copy + a duplicate divider lambda).
            auto stepRow = [&]() {
                ry += rowH + Layout::spacingSm() * 0.5f;
                drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
                ry += Layout::spacingSm() * 0.5f + dividerH;
            };

            // Row 1: Blocks | Longest Chain
            {
                // Blocks — bespoke: block number + "(X left)" in a second color
                float cx = leftX;
                dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
                int blocks = state.sync.blocks;
                if (blocks > 0) {
                    int chainTip = state.longestchain > 0 ? state.longestchain : state.sync.headers;
                    int blocksLeft = chainTip - blocks;
                    if (blocksLeft < 0) blocksLeft = 0;
                    if (blocksLeft > 0) {
                        float valY = ry + capFont->LegacySize + Layout::spacingXs();
                        // Draw block number in normal color
                        char blockStr[32];
                        snprintf(blockStr, sizeof(blockStr), "%d ", blocks);
                        ImVec2 numSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, blockStr);
                        dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), blockStr);
                        // Draw "(X left)" in warning color
                        char leftStr[32];
                        snprintf(leftStr, sizeof(leftStr), TR("peers_blocks_left"), blocksLeft);
                        dl->AddText(capFont, capFont->LegacySize,
                                    ImVec2(cx + numSz.x, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
                                    Warning(), leftStr);
                    } else {
                        snprintf(buf, sizeof(buf), "%d", blocks);
                        dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
                    }
                } else {
                    dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
                }

                // Longest Chain
                if (state.longestchain > 0) {
                    snprintf(buf, sizeof(buf), "%d", state.longestchain);
                    int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
                    ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
                    drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), buf, chainCol);
                } else {
                    drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), nullptr, OnSurface());
                }
            }

            stepRow();

            // Row 2: Hashrate | Difficulty
            {
                std::string hr = mining.networkHashrate > 0 ? FormatHashrate(mining.networkHashrate) : std::string();
                drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_hashrate"),
                             hr.empty() ? nullptr : hr.c_str(), Success());

                if (mining.difficulty > 0) snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
                drawStatCell(dl, rightX, ry, capFont, sub1, TR("difficulty"),
                             mining.difficulty > 0 ? buf : nullptr, OnSurface());
            }

            stepRow();

            // Row 3: Notarized | Protocol
            {
                if (state.notarized > 0) snprintf(buf, sizeof(buf), "%d", state.notarized);
                drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_notarized"),
                             state.notarized > 0 ? buf : nullptr, OnSurface());

                if (state.protocol_version > 0) snprintf(buf, sizeof(buf), "%d", state.protocol_version);
                drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_protocol"),
                             state.protocol_version > 0 ? buf : nullptr, OnSurface());
            }

            stepRow();

            // Row 4: Version | Memory
            {
                if (state.daemon_version > 0) {
                    int major = state.daemon_version / 1000000;
                    int minor = (state.daemon_version / 10000) % 100;
                    int patch = (state.daemon_version / 100) % 100;
                    snprintf(buf, sizeof(buf), "%d.%d.%d", major, minor, patch);
                }
                drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_version"),
                             state.daemon_version > 0 ? buf : nullptr, OnSurface());

                double memMb = state.mining.daemon_memory_mb;
                if (memMb > 0) {
                    if (memMb >= 1024.0)
                        snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
                    else
                        snprintf(buf, sizeof(buf), "%.0f MB", memMb);
                }
                drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_memory"),
                             memMb > 0 ? buf : nullptr, OnSurface());
            }

            stepRow();

            // Row 5: Longest Chain | Best Block
            {
                // Longest Chain
                if (state.longestchain > 0) {
                    snprintf(buf, sizeof(buf), "%d", state.longestchain);
                    // Color green if local matches longest, warning if behind
                    int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
                    ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
                    drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), buf, chainCol);
                } else {
                    drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), nullptr, OnSurface());
                }

                // Best Block — bespoke: truncated hash with click-to-copy + hover underline
                float cx = rightX;
                float valY = ry + capFont->LegacySize + Layout::spacingXs();
                dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
                if (!state.sync.best_blockhash.empty()) {
                    // Truncate hash to fit: first 6 + "..." + last 6
                    std::string hash = state.sync.best_blockhash;
                    std::string truncHash;
                    if (hash.length() > 15) {
                        truncHash = hash.substr(0, 6) + "..." + hash.substr(hash.length() - 6);
                    } else {
                        truncHash = hash;
                    }
                    dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), truncHash.c_str());

                    // Click to copy full hash
                    ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str());
                    ImVec2 savedCursor = ImGui::GetCursorScreenPos();
                    std::string copyTip = std::string(TR("peers_click_copy")) + " " + hash;
                    if (material::CopyableTextOverlay("##BestBlockCopy", ImVec2(cx, valY), hashSz,
                                                      sub1->LegacySize, hash.c_str(), copyTip.c_str(),
                                                      ImVec2(Layout::spacingSm(), 2 * dp))) {
                        ui::Notifications::instance().info(TR("peers_hash_copied"));
                    }
                    ImGui::SetCursorScreenPos(savedCursor);
                } else {
                    dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
                }
            }
        }

        // ================================================================
        // PEERS CARD (right)
        // ================================================================
        {
            ImVec2 cardMin(basePos.x + cardW + gap, basePos.y);
            ImVec2 cardMax(cardMin.x + cardW, cardMin.y + infoCardsH);
            DrawGlassPanel(dl, cardMin, cardMax, glassSpec);

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

            float colW = (cardW - pad * 2) / 2.0f;
            float leftX  = cardMin.x + pad;
            float rightX = cardMin.x + pad + colW;
            float ry = cardMin.y + pad * 0.5f + headerH;

            // Advance ry past a row and draw a divider before the next one (see blockchain card).
            auto stepRow = [&]() {
                ry += rowH + Layout::spacingSm() * 0.5f;
                drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
                ry += Layout::spacingSm() * 0.5f + dividerH;
            };

            // Row 1: Connected | In/Out
            {
                snprintf(buf, sizeof(buf), "%d", totalPeers);
                drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_connected"), buf, OnSurface());

                snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
                drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_in_out"), buf, OnSurface());
            }

            stepRow();

            // Row 2: TLS | Avg Ping
            {
                // TLS — bespoke: appends a check icon when every peer is TLS
                float cx = leftX;
                dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
                float valY = ry + capFont->LegacySize + Layout::spacingXs();
                if (totalPeers > 0) {
                    snprintf(buf, sizeof(buf), "%d / %d", tlsCount, totalPeers);
                    ImU32 tlsCol = (tlsCount == totalPeers) ? Success() : OnSurface();
                    dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), tlsCol, buf);
                    if (tlsCount == totalPeers) {
                        ImFont* iconFont = Type().iconSmall();
                        ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf);
                        dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4 * dp, valY), Success(), ICON_MD_CHECK);
                    }
                } else {
                    dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
                }

                // Avg Ping — color by latency
                ImU32 pingCol;
                if (avgPing < 100) pingCol = Success();
                else if (avgPing < 500) pingCol = Warning();
                else pingCol = Error();
                snprintf(buf, sizeof(buf), "%.0f ms", avgPing);
                drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_avg_ping"), buf, pingCol);
            }

            stepRow();

            // Row 3: Received | Sent
            {
                std::string recvStr = fmtBytes(totalBytesRecv);
                drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_received"), recvStr.c_str(), OnSurface());

                std::string sentStr = fmtBytes(totalBytesSent);
                drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_sent"), sentStr.c_str(), OnSurface());
            }

            stepRow();

            // Row 4: P2P Port | Banned
            {
                if (state.p2p_port > 0) snprintf(buf, sizeof(buf), "%d", state.p2p_port);
                drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_p2p_port"),
                             state.p2p_port > 0 ? buf : nullptr, OnSurface());

                size_t bannedCount = state.bannedPeers.size();
                snprintf(buf, sizeof(buf), "%zu", bannedCount);
                ImU32 bannedCol = (bannedCount > 0) ? Warning() : OnSurface();
                drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_banned"), buf, bannedCol);
            }
        }

        ImGui::SetCursorScreenPos(basePos);
        ImGui::Dummy(ImVec2(availWidth, infoCardsH));
        ImGui::Dummy(ImVec2(0, gap));
    }

    // ================================================================
    // Compute remaining space for peer list + footer
    // ================================================================
    float footerH = ImGui::GetFrameHeight() + Layout::spacingSm();
    float toggleH = body2->LegacySize + Layout::spacingMd() * 2 + Layout::spacingSm();
    float remainForPeers = std::max(60.0f, peersAvail.y - (ImGui::GetCursorScreenPos().y - ImGui::GetWindowPos().y) - footerH - Layout::spacingSm());
    float peerPanelHeight = remainForPeers - toggleH;
    peerPanelHeight = std::max(S.drawElement("tabs.peers", "peer-panel-min-height").size, peerPanelHeight);

    // ================================================================
    // PEERS — Single glass card with Connected / Banned toggle
    // ================================================================
    static bool s_show_banned = false;
    {
        // Toggle header: "Connected (N)" / "Banned (N)"
        float toggleY = ImGui::GetCursorScreenPos().y;
        {
            char connLabel[64], banLabel[64];
            // Format strings use %d — cast size_t down to int (a 64-bit size_t passed to %d
            // is a format/type mismatch / UB; peer counts comfortably fit in int).
            snprintf(connLabel, sizeof(connLabel), TR("peers_connected_count"), (int)state.peers.size());
            snprintf(banLabel, sizeof(banLabel), TR("peers_banned_count"), (int)state.bannedPeers.size());

            ImVec2 connSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, connLabel);
            ImVec2 banSz  = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, banLabel);

            float tabGap = Layout::spacingXl();
            float tabStartX = ImGui::GetCursorScreenPos().x + pad;
            float dp = Layout::dpiScale();

            // Connected tab
            ImVec2 connPos(tabStartX, toggleY);
            ImU32 connCol = s_show_banned ? OnSurfaceDisabled() : OnSurface();
            dl->AddText(body2, body2->LegacySize, ImVec2(connPos.x, connPos.y + Layout::spacingMd() * 0.5f), connCol, connLabel);
            if (!s_show_banned) {
                float underY = connPos.y + Layout::spacingMd() * 0.5f + body2->LegacySize + 3.0f * dp;
                dl->AddRectFilled(ImVec2(connPos.x, underY), ImVec2(connPos.x + connSz.x, underY + 2.0f * dp), Primary(), 1.0f * dp);
            }
            ImGui::SetCursorScreenPos(connPos);
            if (ImGui::InvisibleButton("##tabConn", ImVec2(connSz.x, toggleH))) {
                s_show_banned = false;
            }
            if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);

            // Banned tab
            float banX = tabStartX + connSz.x + tabGap;
            ImVec2 banPos(banX, toggleY);
            ImU32 banCol = s_show_banned ? OnSurface() : OnSurfaceDisabled();
            dl->AddText(body2, body2->LegacySize, ImVec2(banPos.x, banPos.y + Layout::spacingMd() * 0.5f), banCol, banLabel);
            if (s_show_banned) {
                float underY = banPos.y + Layout::spacingMd() * 0.5f + body2->LegacySize + 3.0f * dp;
                dl->AddRectFilled(ImVec2(banPos.x, underY), ImVec2(banPos.x + banSz.x, underY + 2.0f * dp), Primary(), 1.0f * dp);
            }
            ImGui::SetCursorScreenPos(banPos);
            if (ImGui::InvisibleButton("##tabBan", ImVec2(banSz.x, toggleH))) {
                s_show_banned = true;
            }
            if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);

            // Refresh button — top-right, glass panel style (similar to mining button)
            {
                bool isRefreshing = app->isPeerRefreshInProgress();
                ImFont* iconFont = Type().iconMed();
                float iconSz = iconFont->LegacySize;
                const char* label = isRefreshing ? TR("peers_refreshing") : TR("peers_refresh");
                ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
                // Use the wider label so button doesn't resize between states
                ImVec2 altSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0,
                    isRefreshing ? TR("peers_refresh") : TR("peers_refreshing"));
                float maxLblW = std::max(lblSz.x, altSz.x);
                float padH = Layout::spacingSm();
                float padV = Layout::spacingSm();
                float btnW = padH + iconSz + Layout::spacingXs() + maxLblW + padH;
                float btnH = std::max(iconSz, lblSz.y) + padV * 2;
                float btnX = ImGui::GetWindowPos().x + availWidth - btnW - Layout::spacingSm();
                float btnY = toggleY + (toggleH - btnH) * 0.5f;
                ImVec2 bMin(btnX, btnY);
                ImVec2 bMax(btnX + btnW, btnY + btnH);

                bool btnHovered = material::IsRectHovered(bMin, bMax);

                // Glass panel background
                GlassPanelSpec btnGlass;
                btnGlass.rounding = Layout::glassRounding();
                if (isRefreshing) {
                    float pulse = effects::isLowSpecMode()
                        ? 0.5f
                        : 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0);
                    btnGlass.fillAlpha = (int)(15 + 25 * pulse);
                } else {
                    btnGlass.fillAlpha = btnHovered ? 30 : 18;
                }
                DrawGlassPanel(dl, bMin, bMax, btnGlass);

                // Hover highlight
                if (btnHovered && !isRefreshing) {
                    dl->AddRectFilled(bMin, bMax, WithAlpha(Primary(), 20), btnGlass.rounding);
                }

                // Icon: spinner while refreshing, refresh icon otherwise
                float cx = bMin.x + padH + iconSz * 0.5f;
                float cy = bMin.y + btnH * 0.5f;

                if (isRefreshing) {
                    // Spinning arc spinner (same style as mining toggle)
                    float spinnerR = iconSz * 0.5f;
                    float thickness = std::max(1.5f, spinnerR * 0.18f);
                    float time = (float)ImGui::GetTime();

                    // Track circle (faint)
                    dl->AddCircle(ImVec2(cx, cy), spinnerR, WithAlpha(Primary(), 40), 0, thickness);

                    // Animated arc
                    float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f);
                    float cycleTime = fmodf(time, 1.333f);
                    float arcLength = (cycleTime < 0.666f)
                        ? (cycleTime / 0.666f) * 0.75f + 0.1f
                        : ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f;
                    float startAngle = rotation - IM_PI * 0.5f;
                    float endAngle = startAngle + IM_PI * 2.0f * arcLength;
                    int segments = (int)(32 * arcLength) + 1;
                    float angleStep = (endAngle - startAngle) / segments;
                    ImU32 arcCol = Primary();
                    for (int si = 0; si < segments; si++) {
                        float a1 = startAngle + angleStep * si;
                        float a2 = startAngle + angleStep * (si + 1);
                        ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR);
                        ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR);
                        dl->AddLine(p1, p2, arcCol, thickness);
                    }
                } else {
                    // Static refresh icon
                    ImU32 iconCol = btnHovered ? OnSurface() : OnSurfaceMedium();
                    ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
                    dl->AddText(iconFont, iconFont->LegacySize,
                                ImVec2(cx - iSz.x * 0.5f, cy - iSz.y * 0.5f),
                                iconCol, ICON_MD_REFRESH);
                }

                // Label to the right of icon
                {
                    ImU32 lblCol;
                    if (isRefreshing) {
                        float pulse = effects::isLowSpecMode()
                            ? 0.7f
                            : 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0);
                        lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse));
                    } else {
                        lblCol = btnHovered ? OnSurface() : WithAlpha(OnSurface(), 160);
                    }
                    float labelAreaX = bMin.x + padH + iconSz + Layout::spacingXs();
                    float lblX = labelAreaX + (maxLblW - lblSz.x) * 0.5f;
                    float lblY = cy - lblSz.y * 0.5f;
                    dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label);
                }

                // Invisible button for click handling
                ImGui::SetCursorScreenPos(bMin);
                ImGui::PushID("##peersRefresh");
                if (ImGui::InvisibleButton("##btn", ImVec2(btnW, btnH))) {
                    if (!isRefreshing) {
                        app->refreshPeerInfo();
                        app->refreshNow();
                    }
                }
                if (ImGui::IsItemHovered()) {
                    ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                    if (!isRefreshing)
                        material::Tooltip("%s", TR("peers_refresh_tooltip"));
                }
                ImGui::PopID();
            }

            ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x, toggleY + toggleH));
        }

        // Glass panel background
        ImVec2 panelMin = ImGui::GetCursorScreenPos();
        ImVec2 panelMax(panelMin.x + availWidth, panelMin.y + peerPanelHeight);
        DrawGlassPanel(dl, panelMin, panelMax, glassSpec);

        // Scroll-edge mask state
        float listScrollY = 0.0f, listScrollMaxY = 0.0f;
        int listParentVtx = dl->VtxBuffer.Size;

        ImGui::BeginChild("##PeersList", ImVec2(availWidth, peerPanelHeight), false,
                          ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
        ApplySmoothScroll();
        ImDrawList* listChildDL = ImGui::GetWindowDrawList();
        int listChildVtx = listChildDL->VtxBuffer.Size;
        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

        if (!s_show_banned) {
            // ---- Connected Peers ----
            if (!app->isConnected()) {
                material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected"));
            } else if (state.peers.empty()) {
                material::DrawEmptyState(ICON_MD_WIFI_TETHERING, TR("peers_no_connected"));
            } else {
                const float dp = ui::Layout::dpiScale();
                float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg();
                float rowInset = Layout::spacingLg();
                float innerW = ImGui::GetContentRegionAvail().x - rowInset * 2;
                listScrollY    = ImGui::GetScrollY();
                listScrollMaxY = ImGui::GetScrollMaxY();

                for (size_t i = 0; i < state.peers.size(); i++) {
                    const auto& peer = state.peers[i];
                    bool is_selected = (s_selected_peer_idx == static_cast<int>(i));

                    ImGui::PushID(static_cast<int>(i));
                    ImVec2 rawRowPos = ImGui::GetCursorScreenPos();
                    ImVec2 rowPos(rawRowPos.x + rowInset, rawRowPos.y);
                    ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);

                    if (is_selected) {
                        dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "row-selection-rounding").size * dp);
                        dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size * dp, rowEnd.y), Primary(), S.drawElement("tabs.peers", "row-accent-rounding").size * dp);
                    }

                    bool hovered = material::IsRectHovered(rowPos, rowEnd);
                    if (hovered && !is_selected) {
                        dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "row-selection-rounding").size * dp);
                        ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                    }

                    float cx = rowPos.x + pad;
                    float cy = rowPos.y + Layout::spacingSm();

                    double ping_ms = peer.pingtime * 1000.0;
                    ImU32 dotCol;
                    if (ping_ms < 100)       dotCol = Success();
                    else if (ping_ms < 500)  dotCol = Warning();
                    else                     dotCol = Error();
                    float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs;
                    dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size * dp, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);

                    float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size * dp;
                    // Reserve the line-1 trailing zone (ping + direction pill live on the far right,
                    // the nearest being the ping at innerW - pingW - spacingXl*3). A long IPv6+port
                    // addr must clip before it so it can't run under those. Mirror the ping formula
                    // conservatively and leave a gap.
                    float line1RightLimit = rowPos.x + innerW - Layout::spacingXl() * 3 -
                                            capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "0000ms").x -
                                            Layout::spacingSm();
                    dl->PushClipRect(ImVec2(addrX, rowPos.y), ImVec2(line1RightLimit, rowEnd.y), true);
                    dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str());
                    dl->PopClipRect();

                    // Seed node icon — rendered right after the IP address, but never past the
                    // reserved trailing zone (a long addr would otherwise push it into the ping text).
                    if (IsSeedNode(peer.addr)) {
                        ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str());
                        ImFont* iconFont = Type().iconSmall();
                        float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f;
                        float seedIconX = std::min(addrX + addrSz.x + Layout::spacingSm(),
                                                   line1RightLimit - iconFont->LegacySize);
                        dl->AddText(iconFont, iconFont->LegacySize, ImVec2(seedIconX, iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
                    }

                    {
                        const char* dirLabel = peer.inbound ? TR("peers_dir_in") : TR("peers_dir_out");
                        ImU32 dirBg = peer.inbound ? WithAlpha(Success(), 30) : WithAlpha(Secondary(), 30);
                        ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200);
                        ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel);
                        float dirX = rowPos.x + innerW - dirSz.x - Layout::spacingXl();
                        ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size * dp);
                        ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size * dp, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size * dp);
                        dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size * dp);
                        dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2 * dp), dirFg, dirLabel);
                    }

                    {
                        char pingBuf[32];
                        snprintf(pingBuf, sizeof(pingBuf), "%.0fms", ping_ms);
                        ImVec2 pingSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, pingBuf);
                        float pingX = rowPos.x + innerW - pingSz.x - Layout::spacingXl() * 3;
                        dl->AddText(capFont, capFont->LegacySize, ImVec2(pingX, cy + 2), dotCol, pingBuf);
                    }

                    float cy2 = cy + body2->LegacySize + Layout::spacingXs();
                    float subverX = cx + S.drawElement("tabs.peers", "address-x-offset").size * dp;

                    // Reserve the line-2 trailing widths up front so an untrusted, arbitrarily long
                    // remote subver cannot push the TLS badge / ban-score off the row. Order right→left:
                    // ban-score pinned far right, then the TLS/no-TLS badge, then the clipped subver text.
                    float line2RightLimit = rowPos.x + innerW - Layout::spacingLg();
                    float banScoreLeftX   = line2RightLimit;
                    char banBuf[16];
                    ImU32 banCol = 0;
                    bool haveBanScore = peer.banscore > 0;
                    if (haveBanScore) {
                        snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
                        banCol = peer.banscore > 50 ? Error() : Warning();
                        float banW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf).x;
                        banScoreLeftX = line2RightLimit - banW;
                        // The TLS/no-TLS badge sits to the LEFT of the ban score with a gap.
                        line2RightLimit = banScoreLeftX - Layout::spacingLg();
                    }

                    float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
                    float noTlsW    = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("peers_no_tls")).x;
                    float badgeW    = peer.tls_cipher.empty() ? noTlsW : tlsBadgeW;
                    // Hard right boundary for the subver text column, leaving room for the badge.
                    float subverMaxX = line2RightLimit - badgeW - Layout::spacingSm();
                    if (subverMaxX < subverX) subverMaxX = subverX;

                    float verW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, peer.subver.c_str()).x;
                    dl->PushClipRect(ImVec2(subverX, rowPos.y), ImVec2(subverMaxX, rowEnd.y), true);
                    dl->AddText(capFont, capFont->LegacySize, ImVec2(subverX, cy2),
                                OnSurfaceDisabled(), peer.subver.c_str());
                    dl->PopClipRect();

                    // Pin the badge just after the (clipped) subver, but never past the reserved zone.
                    float badgeX = std::min(subverX + verW + Layout::spacingSm(), subverMaxX + Layout::spacingSm());
                    if (!peer.tls_cipher.empty()) {
                        ImU32 tlsBg = WithAlpha(Success(), 25);
                        ImU32 tlsFg = WithAlpha(Success(), 200);
                        ImVec2 tlsMin(badgeX, cy2);
                        ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2 * dp);
                        dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size * dp);
                        dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4 * dp, cy2 + 1 * dp), tlsFg, "TLS");
                    } else {
                        dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
                                    WithAlpha(Error(), 140), TR("peers_no_tls"));
                    }

                    if (haveBanScore) {
                        dl->AddText(capFont, capFont->LegacySize,
                                    ImVec2(banScoreLeftX, cy2), banCol, banBuf);
                    }

                    ImGui::InvisibleButton("##peerRow", ImVec2(innerW, rowH));
                    if (ImGui::IsItemClicked(0)) {
                        s_selected_peer_idx = static_cast<int>(i);
                    }

                    const auto& acrylicTheme = GetCurrentAcrylicTheme();
                    if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme.menu)) {
                        ImGui::Text(TR("peers_peer_label"), peer.addr.c_str());
                        ImGui::Separator();
                        if (ImGui::MenuItem(TR("copy_address"))) {
                            ImGui::SetClipboardText(peer.addr.c_str());
                        }
                        if (ImGui::MenuItem(TR("peers_copy_ip"))) {
                            ImGui::SetClipboardText(ExtractIP(peer.addr).c_str());
                        }
                        ImGui::Separator();
                        if (ImGui::MenuItem(TR("peers_ban_24h"))) {
                            app->banPeer(ExtractIP(peer.addr), 86400);
                        }
                        effects::ImGuiAcrylic::EndAcrylicPopup();
                    }

                    if (ImGui::IsItemHovered()) {
                        material::BeginTooltip();
                        ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(8, 3));
                        if (ImGui::BeginTable("##PeerTT", 2, ImGuiTableFlags_SizingFixedFit)) {
                            auto TTRow = [](const char* label, const char* value) {
                                ImGui::TableNextRow();
                                ImGui::TableNextColumn();
                                ImGui::TextDisabled("%s", label);
                                ImGui::TableNextColumn();
                                ImGui::Text("%s", value);
                            };
                            char ttBuf[128];
                            snprintf(ttBuf, sizeof(ttBuf), "%d", peer.id);
                            TTRow(TR("peers_tt_id"), ttBuf);
                            TTRow(TR("peers_tt_services"), peer.services.c_str());
                            snprintf(ttBuf, sizeof(ttBuf), "%d", peer.startingheight);
                            TTRow(TR("peers_tt_start_height"), ttBuf);
                            // %lld + (long long) — %ld truncates int64_t on Windows (LLP64).
                            snprintf(ttBuf, sizeof(ttBuf), "%lld bytes", (long long)peer.bytessent);
                            TTRow(TR("peers_tt_sent"), ttBuf);
                            snprintf(ttBuf, sizeof(ttBuf), "%lld bytes", (long long)peer.bytesrecv);
                            TTRow(TR("peers_tt_received"), ttBuf);
                            snprintf(ttBuf, sizeof(ttBuf), "%d / %d", peer.synced_headers, peer.synced_blocks);
                            TTRow(TR("peers_tt_synced"), ttBuf);
                            if (!peer.tls_cipher.empty())
                                TTRow(TR("peers_tt_tls_cipher"), peer.tls_cipher.c_str());
                            ImGui::EndTable();
                        }
                        ImGui::PopStyleVar();
                        material::EndTooltip();
                    }

                    if (i < state.peers.size() - 1) {
                        ImVec2 divStart = ImGui::GetCursorScreenPos();
                        dl->AddLine(ImVec2(divStart.x + pad + 18 * dp, divStart.y),
                                    ImVec2(divStart.x + innerW - pad, divStart.y),
                                    IM_COL32(255, 255, 255, 15));
                    }

                    ImGui::PopID();
                }
            }
        } else {
            // ---- Banned Peers ----
            if (!app->isConnected()) {
                material::DrawEmptyState(ICON_MD_CLOUD_OFF, TR("not_connected"));
            } else if (state.bannedPeers.empty()) {
                material::DrawEmptyState(ICON_MD_BLOCK, TR("peers_no_banned"));
            } else {
                const float dp = ui::Layout::dpiScale();
                float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size * dp;
                float rowInsetB = pad;
                float innerW = ImGui::GetContentRegionAvail().x - rowInsetB * 2;
                listScrollY    = ImGui::GetScrollY();
                listScrollMaxY = ImGui::GetScrollMaxY();

                for (size_t i = 0; i < state.bannedPeers.size(); i++) {
                    const auto& banned = state.bannedPeers[i];
                    bool is_selected = (s_selected_banned_idx == static_cast<int>(i));

                    ImGui::PushID(static_cast<int>(i));
                    ImVec2 rawRowPosB = ImGui::GetCursorScreenPos();
                    ImVec2 rowPos(rawRowPosB.x + rowInsetB, rawRowPosB.y);
                    ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);

                    if (is_selected) {
                        dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "banned-row-rounding").size * dp);
                        dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size * dp, rowEnd.y), WithAlpha(Error(), 200), S.drawElement("tabs.peers", "banned-accent-rounding").size * dp);
                    }

                    if (material::IsRectHovered(rowPos, rowEnd) && !is_selected) {
                        dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "banned-row-rounding").size * dp);
                    }

                    float cx = rowPos.x + pad;
                    float cy = rowPos.y + Layout::spacingXs();

                    float banDotR = S.drawElement("tabs.peers", "ban-dot-radius-base").size + S.drawElement("tabs.peers", "ban-dot-radius-scale").size * hs;
                    dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ban-dot-x-offset").size * dp, cy + capFont->LegacySize * 0.4f), banDotR, WithAlpha(Error(), 200));

                    dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "banned-address-x-offset").size * dp, cy),
                                OnSurfaceDisabled(), banned.address.c_str());

                    std::string banUntil = banned.getBannedUntilString();
                    ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banUntil.c_str());
                    dl->AddText(capFont, capFont->LegacySize,
                                ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingXl() * 5, cy),
                                OnSurfaceDisabled(), banUntil.c_str());

                    float btnX = rowPos.x + innerW - Layout::spacingXl() * S.drawElement("tabs.peers", "unban-btn-right-offset-multiplier").size;
                    ImGui::SetCursorScreenPos(ImVec2(btnX, cy - 1));
                    if (TactileSmallButton(TR("peers_unban"), S.resolveFont("button"))) {
                        app->unbanPeer(banned.address);
                    }

                    ImGui::SetCursorScreenPos(rowPos);
                    ImGui::InvisibleButton("##bannedRow", ImVec2(innerW - S.drawElement("tabs.peers", "banned-row-btn-reserve").size * dp, rowH));
                    if (ImGui::IsItemClicked(0)) {
                        s_selected_banned_idx = static_cast<int>(i);
                    }

                    const auto& acrylicTheme2 = GetCurrentAcrylicTheme();
                    if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme2.menu)) {
                        if (ImGui::MenuItem(TR("copy_address"))) {
                            ImGui::SetClipboardText(banned.address.c_str());
                        }
                        if (ImGui::MenuItem(TR("peers_unban"))) {
                            app->unbanPeer(banned.address);
                        }
                        effects::ImGuiAcrylic::EndAcrylicPopup();
                    }

                    ImGui::SetCursorScreenPos(ImVec2(rawRowPosB.x, rowEnd.y));

                    if (i < state.bannedPeers.size() - 1) {
                        ImVec2 divStart = ImGui::GetCursorScreenPos();
                        dl->AddLine(ImVec2(divStart.x + pad + 8 * dp, divStart.y),
                                    ImVec2(divStart.x + innerW - pad, divStart.y),
                                    IM_COL32(255, 255, 255, 15));
                    }

                    ImGui::PopID();
                }
            }
        }

        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
        ImGui::EndChild();

        // CSS-style clipping mask
        {
            float fadeFont = s_show_banned ? capFont->LegacySize : body2->LegacySize;
            float fadeZone = std::min(fadeFont * 3.0f, peerPanelHeight * 0.18f);
            ApplyScrollEdgeMask(dl, listParentVtx, listChildDL, listChildVtx,
                                panelMin.y, panelMax.y, fadeZone, listScrollY, listScrollMaxY);
        }

        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
    }

    // ================================================================
    // Footer — Clear Bans (material styled)
    // ================================================================
    if (s_show_banned && !state.bannedPeers.empty()) {
        ImGui::BeginDisabled(!app->isConnected());
        if (TactileSmallButton(TR("peers_clear_all_bans"), S.resolveFont("button"))) {
            app->clearBans();
        }
        ImGui::EndDisabled();
    }

    ImGui::EndChild(); // ##PeersScroll
}

} // namespace ui
} // namespace dragonx
