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

#include "balance_components.h"
#include "balance_tab_helpers.h"
#include "balance_address_list.h"
#include "balance_recent_tx.h"
#include "key_export_dialog.h"
#include "qr_popup_dialog.h"
#include "address_label_dialog.h"
#include "address_transfer_dialog.h"
#include "mining_tab_helpers.h"
#include "send_tab.h"

#include "../../app.h"
#include "../../config/settings.h"
#include "../../data/portfolio.h"
#include "../../config/version.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../theme.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../effects/imgui_acrylic.h"
#include "../sidebar.h"
#include "../notifications.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"

#include <algorithm>
#include <cstring>
#include <cstdio>
#include <ctime>
#include <cmath>
#include <string>
#include <vector>

namespace dragonx {
namespace ui {

void UpdateBalanceLerp(App* app) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
    const auto& state = app->state();
    float dt = ImGui::GetIO().DeltaTime;
    float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
    auto lerp = [](double& disp, double target, float dt, float spd) {
        double diff = target - disp;
        if (std::abs(diff) < 1e-9) { disp = target; return; }
        disp += diff * (double)(dt * spd);
        if (std::abs(target - disp) < 1e-9) disp = target;
    };
    lerp(s_dispTotal,       app->getTotalBalance(),       dt, speed);
    lerp(s_dispShielded,    app->getShieldedBalance(),    dt, speed);
    lerp(s_dispTransparent, app->getTransparentBalance(), dt, speed);
    lerp(s_dispUnconfirmed, state.unconfirmed_balance,    dt, speed);
}

// Render compact hero line: logo + balance + USD + mining on one line
void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride) {
    using namespace material;
    char buf[64];
    const float dp = Layout::dpiScale();

    // Coin logo
    ImTextureID logoTex = app->getCoinLogoTexture();
    ImFont* sub1 = Type().subtitle1();
    float logoSz = sub1->LegacySize + 4.0f * dp;
    float lineH = (heroHeightOverride >= 0.0f) ? heroHeightOverride : logoSz;
    if (logoTex != 0) {
        ImVec2 pos = ImGui::GetCursorScreenPos();
        dl->AddImage(logoTex,
            ImVec2(pos.x, pos.y),
            ImVec2(pos.x + lineH, pos.y + lineH),
            ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
        ImGui::Dummy(ImVec2(lineH + Layout::spacingSm(), lineH));
        ImGui::SameLine();
    }

    // Total balance
    snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
    ImVec2 pos = ImGui::GetCursorScreenPos();
    float fontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride : sub1->LegacySize;
    DrawTextShadow(dl, sub1, fontSize, pos, OnSurface(), buf);
    ImVec2 sz = sub1->CalcTextSizeA(fontSize, 10000.0f, 0.0f, buf);
    ImGui::Dummy(ImVec2(sz.x, lineH));
    ImGui::SameLine();

    // Ticker
    ImFont* capFont = Type().caption();
    float tickerFontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride * 0.6f : capFont->LegacySize;
    float tickerY = pos.y + lineH - tickerFontSize;
    dl->AddText(capFont, tickerFontSize,
                ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
                OnSurfaceMedium(), DRAGONX_TICKER);
    ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
    ImGui::SameLine();

    // USD value
    const auto& state = app->state();
    double usd_value = state.getBalanceUSD();
    if (usd_value > 0.0)
        snprintf(buf, sizeof(buf), "$%.2f", usd_value);
    else
        snprintf(buf, sizeof(buf), "$--.--");
    dl->AddText(capFont, tickerFontSize,
                ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
                OnSurfaceDisabled(), buf);

    ImGui::NewLine();
}

// Render the shared address list section (used by all layouts)
void RenderSharedAddressList(App* app, float listH, float availW,
                                     float glassRound, float hs, float vs, float reserveBelow) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const float dp = Layout::dpiScale();
    const auto addrBtn   = S.button("tabs.balance", "address-button");
    const auto actionBtn = S.button("tabs.balance", "action-button");
    const auto searchIn  = S.input("tabs.balance", "search-input");
    const auto addrTable = S.table("tabs.balance", "address-table");
    const auto& state = app->state();

    Type().text(TypeStyle::H6, TR("your_addresses"));
    ImGui::Spacing();

    // ---- Persistent state ----
    static char addr_search[128] = "";
    static bool s_hideZeroBalances = true;
    static bool s_showHidden = false;

    // Drag state
    static int  s_dragIdx = -1;         // row being dragged (-1 = none)
    static float s_dragOffsetY = 0.0f;  // mouse offset from row top
    static float s_dragStartY = 0.0f;   // mouse Y at drag start
    static bool  s_dragActive = false;  // drag distance threshold passed
    static int   s_dropTargetIdx = -1;  // row hovered during drag
    static int   s_dropMode = 0;        // 0 = transfer (released over a row's CENTER), 1 = reorder
                                        // (released over a row's top/bottom EDGE). Disambiguates the
                                        // two drag gestures by WHERE on the target row you release.

    // Copy feedback
    static int   s_copiedRow = -1;
    static float s_copiedTime = 0.0f;

    // ---- Build and filter address rows ----
    std::vector<AddressListInput> rowInputs;
    rowInputs.reserve(state.z_addresses.size() + state.t_addresses.size());

    auto addRows = [&](const auto& addrs, bool isZ) {
        for (const auto& a : addrs) {
            std::string addrLabel = app->getAddressLabel(a.address);
            bool isHidden = app->isAddressHidden(a.address);
            bool isFav = app->isAddressFavorite(a.address);
            bool isMining = app->isMiningAddress(a.address);
            rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
                                 addrLabel, app->getAddressIcon(a.address),
                                 app->getAddressSortOrder(a.address)});
        }
    };
    addRows(state.z_addresses, true);
    addRows(state.t_addresses, false);
    auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden);

    // ---- Toolbar: search + checkboxes + create buttons ----
    float avail = ImGui::GetContentRegionAvail().x;
    float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
    float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
    float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
    ImGui::SetNextItemWidth(searchW);
    ImGui::InputTextWithHint("##AddrSearch", TR("filter"), addr_search, sizeof(addr_search));
    ImGui::SameLine(0, Layout::spacingLg());
    ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
    {
        int hc = app->getHiddenAddressCount();
        if (hc > 0) {
            ImGui::SameLine(0, Layout::spacingLg());
            char hlbl[64];
            snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
            ImGui::Checkbox(hlbl, &s_showHidden);
        } else {
            s_showHidden = false;
        }
    }

    float buttonWidth = ((addrBtn.width > 0) ? addrBtn.width : 140.0f) * dp;
    float spacing = ((addrBtn.gap > 0) ? addrBtn.gap : 8.0f) * dp;
    float totalButtonsWidth = buttonWidth * 2 + spacing;
    float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size,
                                          S.drawElement("tabs.balance", "buttons-position").size * hs);
    ImGui::SameLine(std::max(kMinButtonsPosition,
                             ImGui::GetWindowWidth() - totalButtonsWidth -
                             S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f)));

    bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
    ImGui::BeginDisabled(sharedAddrSyncing);
    if (s_generating_z_address) {
        char genLabel[64];
        snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), material::LoadingDots());
        TactileButton(genLabel, ImVec2(buttonWidth, 0),
                      S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
    } else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
                      S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
        s_generating_z_address = true;
        app->createNewZAddress([](const std::string& addr) {
            s_generating_z_address = false;
            DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
        });
    }
    ImGui::SameLine();
    if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0),
                      S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
        app->createNewTAddress([](const std::string& addr) {
            DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
        });
    }
    ImGui::EndDisabled();

    ImGui::Dummy(ImVec2(0, Layout::spacingXs()));

    // ---- Glass panel container ----
    float addrListH = listH;
    // Cap the card to the space that actually remains here (measured AFTER the title + toolbar are laid
    // out, so no chrome modelling is needed) minus what the caller reserves for the section below it
    // (recent-tx). Without this, a fixed dp-scaled listH grows ~1.5x at high font scale and evicts the
    // Recent Transactions list off the bottom of the fixed, non-scrolling tab host.
    if (reserveBelow > 0.0f) {
        float maxH = ImGui::GetContentRegionAvail().y - reserveBelow;
        if (maxH < addrListH) addrListH = maxH;
    }
    if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;

    ImDrawList* dlPanel = ImGui::GetWindowDrawList();
    ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
    ImVec2 listPanelMax(listPanelMin.x + availW, listPanelMin.y + addrListH);
    GlassPanelSpec addrGlassSpec;
    addrGlassSpec.rounding = glassRound;
    DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);

    float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
    int addrParentVtx = dlPanel->VtxBuffer.Size;

    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
    ImGui::BeginChild("AddressList", ImVec2(availW, addrListH), false,
                      ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
    ApplySmoothScroll();
    ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
    int addrChildVtx = addrChildDL->VtxBuffer.Size;

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

    // ---- Empty states ----
    if (!app->isConnected()) {
        ImGui::Dummy(ImVec2(0, 16 * dp));
        float cw = ImGui::GetContentRegionAvail().x;
        ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
        ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
        ImGui::TextDisabled("%s", TR("not_connected"));
    } else if (rows.empty()) {
        float cw = ImGui::GetContentRegionAvail().x;
        float ch = ImGui::GetContentRegionAvail().y;
        if (ch < 60.0f * dp) ch = 60.0f * dp;
        const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
        ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
        ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
        ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
        ImGui::TextDisabled("%s", emptyMsg);
    } else {
        // ---- PASS 1: Compute row layout ----
        ImDrawList* dl = ImGui::GetWindowDrawList();
        ImFont* capFont = Type().caption();
        ImFont* body2 = Type().body2();
        float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd() + Layout::spacingMd();
        static int selected_row = -1;
        addrScrollY    = ImGui::GetScrollY();
        addrScrollMaxY = ImGui::GetScrollMaxY();
        ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
        ImU32 goldCol  = S.resolveColor("var(--accent-transparent)", Warning());
        float rowPadLeft = Layout::cardInnerPadding();
        float rowPadRight = Layout::cardInnerPadding();
        float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size,
                                    S.drawElement("tabs.balance", "address-icon-size").size * hs);
        float innerW = ImGui::GetContentRegionAvail().x;

        // Theme colors for buttons (resolved once)
        ImU32 favGoldFill   = S.resolveColor("var(--favorite-fill)", IM_COL32(255, 200, 50, 40));
        ImU32 favGoldBorder = S.resolveColor("var(--favorite-border)", IM_COL32(255, 200, 50, 100));
        ImU32 favGoldIcon   = S.resolveColor("var(--favorite-icon)", IM_COL32(255, 200, 50, 255));
        ImU32 favGoldOutline= S.resolveColor("var(--favorite-outline)", IM_COL32(255, 200, 50, 120));
        ImU32 btnFill       = S.resolveColor("var(--row-button-fill)", IM_COL32(255, 255, 255, 12));
        ImU32 btnFillHov    = S.resolveColor("var(--row-button-fill-hover)", IM_COL32(255, 255, 255, 25));
        ImU32 btnBorder     = S.resolveColor("var(--row-button-border)", IM_COL32(255, 255, 255, 25));
        ImU32 btnBorderHov  = S.resolveColor("var(--row-button-border-hover)", IM_COL32(255, 255, 255, 50));
        ImU32 rowHoverCol   = S.resolveColor("var(--row-hover)", IM_COL32(255, 255, 255, 15));
        ImU32 rowSelectCol  = S.resolveColor("var(--row-select)", IM_COL32(255, 255, 255, 20));
        ImU32 dividerCol    = S.resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15));

        // Compute Y positions for all rows (pass 1)
        std::vector<float> rowY(rows.size());
        float cursorStartY = ImGui::GetCursorScreenPos().y;
        for (int i = 0; i < (int)rows.size(); ++i)
            rowY[i] = cursorStartY + i * rowH;

        // Drag logic — detect drag start, compute drag position
        ImVec2 mousePos = ImGui::GetMousePos();
        bool mouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
        bool mouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);

        // Reset drop target each frame — it gets set only if mouse is over a row.
        // Preserve the drop target on the release frame so the drop handler can use it.
        if (s_dragActive && mouseDown) s_dropTargetIdx = -1;

        if (s_dragIdx >= 0 && !mouseDown) {
            // Mouse released — copy if it was a click (no drag activated)
            if (!s_dragActive && s_dragIdx >= 0 && s_dragIdx < (int)rows.size()) {
                const auto& clickRow = rows[s_dragIdx];
                ImGui::SetClipboardText(clickRow.info->address.c_str());
                selected_row = s_dragIdx;
                s_copiedRow = s_dragIdx;
                s_copiedTime = (float)ImGui::GetTime();
            }
            // Drop. Released over a row's CENTER (transfer mode) and that row has funds -> transfer;
            // released over an EDGE, in a gap, or on a zero-balance row -> reorder.
            if (s_dragActive) {
                bool didTransfer = false;
                if (s_dropMode == 0 && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx &&
                    s_dragIdx < (int)rows.size()) {
                    const auto& srcRow = rows[s_dragIdx];
                    const auto& dstRow = rows[s_dropTargetIdx];
                    if (srcRow.info->spendableBalance > 1e-9) {   // only offer a transfer of CONFIRMED funds
                        AddressTransferDialog::TransferInfo ti;
                        ti.fromAddr = srcRow.info->address;
                        ti.toAddr = dstRow.info->address;
                        ti.fromBalance = srcRow.info->spendableBalance;   // spend cap — z_sendmany runs at minconf=1
                        ti.toBalance = dstRow.info->balance;              // destination display only
                        ti.fromIsZ = srcRow.isZ;
                        ti.toIsZ = dstRow.isZ;
                        AddressTransferDialog::show(app, ti);
                        didTransfer = true;
                    }
                }
                if (!didTransfer) {
                    // Reorder — compute insert position from mouseY.
                    int insertIdx = 0;
                    for (int i = 0; i < (int)rows.size(); ++i) {
                        if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1;
                    }
                    if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1 &&
                        s_dragIdx < (int)rows.size()) {
                        // Build the new visible order (move the dragged row to insertIdx) and
                        // persist dense sort orders 0..N-1 in one shot. This works even when the
                        // rows were still at the default order (a pairwise swap would no-op).
                        std::vector<std::string> newOrder;
                        newOrder.reserve(rows.size());
                        for (int i = 0; i < (int)rows.size(); ++i)
                            if (i != s_dragIdx) newOrder.push_back(rows[i].info->address);
                        int clamped = std::min(insertIdx, (int)rows.size());
                        int destIdx = (insertIdx > s_dragIdx) ? clamped - 1 : clamped;
                        destIdx = std::max(0, std::min(destIdx, (int)newOrder.size()));
                        newOrder.insert(newOrder.begin() + destIdx, rows[s_dragIdx].info->address);
                        app->reorderAddresses(newOrder);
                    }
                }
            }
            s_dragIdx = -1;
            s_dragActive = false;
            s_dropTargetIdx = -1;
            s_dropMode = 0;
        }

        // ---- PASS 2: Render rows ----
        for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
            const auto& row = rows[row_idx];
            const auto& addr = *row.info;
            bool isDragged = (s_dragIdx == row_idx && s_dragActive);

            // For dragged row, offset Y to follow mouse
            float drawY = rowY[row_idx];
            if (isDragged) {
                drawY = mousePos.y - s_dragOffsetY;
            }

            ImVec2 rowPos(ImGui::GetCursorScreenPos().x, drawY);
            ImVec2 rowEnd(rowPos.x + innerW, drawY + rowH);
            ImU32 typeCol = row.isZ ? greenCol : goldCol;
            if (row.hidden) typeCol = OnSurfaceDisabled();

            // Dragged row: draw with semi-transparent elevation
            if (isDragged) {
                dl->AddRectFilled(ImVec2(rowPos.x - 2*dp, rowPos.y - 2*dp),
                    ImVec2(rowEnd.x + 2*dp, rowEnd.y + 2*dp),
                    IM_COL32(0, 0, 0, 30), 6.0f * dp);
                dl->AddRectFilled(rowPos, rowEnd, IM_COL32(30, 30, 40, 120), 4.0f * dp);
                // Tooltip following cursor — reflect the gesture: centre of a row = transfer to it,
                // edge of a row = reorder (move here).
                if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size() && s_dropMode == 0) {
                    const auto& target = rows[s_dropTargetIdx];
                    material::Tooltip("%s\n%s\n\n%s %s",
                        truncateAddress(addr.address, 32).c_str(),
                        row.isZ ? TR("shielded") : TR("transparent"),
                        TR("transfer_to"),
                        truncateAddress(target.info->address, 32).c_str());
                } else {
                    material::Tooltip("%s\n%s\n\n%s",
                        truncateAddress(addr.address, 32).c_str(),
                        row.isZ ? TR("shielded") : TR("transparent"),
                        TR("address_reorder_hint"));
                }
            }

            // Golden border for favorites — inset so it doesn't touch container
            if (row.favorite) {
                float inset = 2.0f * dp;
                dl->AddRect(ImVec2(rowPos.x + inset, rowPos.y + inset),
                            ImVec2(rowEnd.x - inset, rowEnd.y - inset),
                            favGoldOutline, 4.0f * dp, 0, 1.5f * dp);
            }

            // Selection highlight
            if (selected_row == row_idx && !isDragged) {
                ImDrawFlags accentFlags = 0;
                float accentRound = 2.0f * dp;
                if (row_idx == 0) { accentFlags = ImDrawFlags_RoundCornersTopLeft; accentRound = glassRound; }
                if (row_idx == (int)rows.size() - 1) { accentFlags |= ImDrawFlags_RoundCornersBottomLeft; accentRound = glassRound; }
                dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
                dl->AddRectFilled(rowPos, rowEnd, rowSelectCol, 4.0f * dp);
            }

            // Hover effects
            bool hovered = !isDragged && material::IsRectHovered(rowPos, rowEnd);

            // Drop target feedback when dragging another row over this one. The cursor's vertical
            // position WITHIN the row picks the gesture: top/bottom edge = reorder (show an insertion
            // line), centre = transfer (highlight the whole row).
            if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) {
                s_dropTargetIdx = row_idx;
                const float relY = rowH > 0.0f ? (mousePos.y - rowPos.y) / rowH : 0.5f;
                s_dropMode = (relY < 0.30f || relY > 0.70f) ? 1 : 0;
                if (s_dropMode == 0) {
                    // Transfer: highlight the whole target row.
                    dl->AddRectFilled(rowPos, rowEnd, WithAlpha(Primary(), 40), 4.0f * dp);
                    dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp);
                } else {
                    // Reorder: draw an insertion line at the near (top or bottom) edge.
                    const float lineY = (relY < 0.5f) ? rowPos.y : rowEnd.y;
                    dl->AddLine(ImVec2(rowPos.x, lineY), ImVec2(rowEnd.x, lineY),
                                WithAlpha(Primary(), 230), 2.5f * dp);
                    dl->AddCircleFilled(ImVec2(rowPos.x, lineY), 3.0f * dp, WithAlpha(Primary(), 230));
                }
            }

            if (hovered && selected_row != row_idx && !s_dragActive) {
                dl->AddRectFilled(rowPos, rowEnd, rowHoverCol, 4.0f * dp);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
            }

            auto rowLayout = ComputeAddressRowLayout(
                rowPos.x, rowPos.y, innerW, rowH, rowPadLeft, rowIconSz,
                Layout::spacingMd(), Layout::spacingSm(), Layout::spacingXs());
            float cx = rowLayout.contentStartX;
            float cy = rowLayout.contentStartY;

            // ---- Button zone (right edge): [eye] [star] ----
            float btnRound = 6.0f * dp;
            bool btnClicked = false;

            if (!isDragged) {
                // Star button
                {
                    const auto& starRect = rowLayout.favoriteButton;
                    ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
                    // material::IsRectHovered so the button inherits overlay/popup input blocking
                    bool bHov = material::IsRectHovered(bMin, bMax);
                    dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
                    dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
                    ImFont* iconFont = Type().iconSmall();
                    const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
                    ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
                    ImU32 starCol = row.favorite ? favGoldIcon : (bHov ? OnSurface() : OnSurfaceDisabled());
                    dl->AddText(iconFont, iconFont->LegacySize,
                        ImVec2(starRect.x + (starRect.width - iSz.x) * 0.5f,
                               starRect.y + (starRect.height - iSz.y) * 0.5f), starCol, starIcon);
                    if (bHov && mouseClicked) {
                        if (row.favorite) app->unfavoriteAddress(addr.address);
                        else app->favoriteAddress(addr.address);
                        btnClicked = true;
                    }
                    if (bHov) material::Tooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
                }

                // Eye button (zero balance or hidden)
                bool showEye = true;
                // Always reserve space for both buttons so content doesn't shift
                float contentRight = rowLayout.contentRight;

                if (showEye) {
                    const auto& eyeRect = rowLayout.visibilityButton;
                    ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
                    // material::IsRectHovered so the button inherits overlay/popup input blocking
                    bool bHov = material::IsRectHovered(bMin, bMax);
                    dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
                    dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
                    ImFont* iconFont = Type().iconSmall();
                    const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
                    ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
                    ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
                    dl->AddText(iconFont, iconFont->LegacySize,
                        ImVec2(eyeRect.x + (eyeRect.width - iSz.x) * 0.5f,
                               eyeRect.y + (eyeRect.height - iSz.y) * 0.5f), iconCol, hideIcon);
                    if (bHov && mouseClicked) {
                        if (row.hidden) app->unhideAddress(addr.address);
                        else app->hideAddress(addr.address);
                        btnClicked = true;
                    }
                    if (bHov) material::Tooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
                }

                // ---- Type icon or custom icon ----
                float iconCx = cx + rowIconSz;
                float iconCy = cy + body2->LegacySize * 0.5f;
                {
                    ImFont* iconFont = Type().iconSmall();
                    bool drewCustom = false;
                    if (!row.icon.empty()) {
                        drewCustom = AddressLabelDialog::drawIconByName(
                            dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
                            OnSurfaceMedium(), iconFont, iconFont->LegacySize);
                    }
                    if (!drewCustom) {
                        const char* glyph = row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
                        ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
                        dl->AddText(iconFont, iconFont->LegacySize,
                            ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, glyph);
                    }
                }

                // ---- Type label (first line) ----
                float labelX = rowLayout.labelX;
                {
                    const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
                    const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
                    const char* miningTag = row.mining ? TR("mining_tag") : "";
                    char typeBuf[64];
                    snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
                    dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);

                    // User label next to type — clip to the gap before the right-aligned
                    // balance so a long custom label can't overrun the balance on this line
                    // (mirrors the address-line width guard below).
                    if (!row.label.empty()) {
                        float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
                        float userLabelX = labelX + typeLabelW + Layout::spacingLg();
                        // Balance is drawn right-aligned at contentRight; recompute its left edge here.
                        char balBufPeek[32];
                        snprintf(balBufPeek, sizeof(balBufPeek), "%.8f", addr.balance);
                        float balW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBufPeek).x;
                        float userLabelAvailW = (contentRight - balW - Layout::spacingMd()) - userLabelX;
                        std::string userLabel = material::TruncateToWidth(
                            row.label, capFont, capFont->LegacySize, userLabelAvailW);
                        if (userLabelAvailW > 0.0f) {
                            dl->AddText(capFont, capFont->LegacySize,
                                        ImVec2(userLabelX, cy),
                                        OnSurfaceMedium(), userLabel.c_str());
                        }
                    }
                }

                // ---- Address (second line) ----
                {
                    float addrAvailW = contentRight - labelX;
                    ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
                    std::string display_addr;
                    if (fullAddrSz.x <= addrAvailW) {
                        display_addr = addr.address;
                    } else {
                        int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
                            ? addrTable.columns.at("address").truncate : 32;
                        display_addr = truncateAddress(addr.address, addrTruncLen);
                    }
                    dl->AddText(capFont, capFont->LegacySize,
                                ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
                                OnSurfaceMedium(), display_addr.c_str());
                }

                // ---- Balance (right-aligned, first line) ----
                {
                    char balBuf[32];
                    snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
                    ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
                    float balX = contentRight - balSz.x;
                    ImU32 balCol = addr.balance > 0.0
                        ? (row.isZ ? greenCol : OnSurface())
                        : OnSurfaceDisabled();
                    if (row.hidden) balCol = OnSurfaceDisabled();
                    DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
                }

                // ---- USD value (second line, right-aligned) ----
                {
                    std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd);
                    if (!usdText.empty()) {
                        ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str());
                        dl->AddText(capFont, capFont->LegacySize,
                                    ImVec2(contentRight - usdSz.x,
                                           cy + body2->LegacySize + Layout::spacingXs()),
                                    OnSurfaceDisabled(), usdText.c_str());
                    }
                }

                // ---- Copy feedback flash ----
                if (s_copiedRow == row_idx) {
                    float elapsed = (float)ImGui::GetTime() - s_copiedTime;
                    if (elapsed < 1.0f) {
                        float alpha = 1.0f - elapsed;
                        ImFont* capF = Type().caption();
                        const char* copiedTxt = TR("copied");
                        ImVec2 sz = capF->CalcTextSizeA(capF->LegacySize, 1000.0f, 0.0f, copiedTxt);
                        ImVec4 fc = ImGui::ColorConvertU32ToFloat4(Success());
                        fc.w = alpha;
                        float tx = rowPos.x + (innerW - sz.x) * 0.5f;
                        float ty = rowPos.y + (rowH - sz.y) * 0.5f;
                        dl->AddText(capF, capF->LegacySize, ImVec2(tx, ty),
                            ImGui::ColorConvertFloat4ToU32(fc), copiedTxt);
                    } else {
                        s_copiedRow = -1;
                    }
                }

                // ---- Click: begin drag tracking (copy deferred to mouse-up) ----
                if (hovered && mouseClicked && !btnClicked) {
                    s_dragIdx = row_idx;
                    s_dragOffsetY = mousePos.y - rowPos.y;
                    s_dragStartY = mousePos.y;
                    s_dragActive = false;
                }

                // Activate drag after a small threshold
                if (s_dragIdx == row_idx && !s_dragActive && mouseDown) {
                    if (std::abs(mousePos.y - s_dragStartY) > 6.0f * dp) {
                        s_dragActive = true;
                        s_dropTargetIdx = -1;
                    }
                }
            } // end !isDragged

            // Advance cursor for interaction area
            ImGui::PushID(row_idx);
            ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
            ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
            if (ImGui::IsItemHovered() && !s_dragActive) {
                material::Tooltip("%s", addr.address.c_str());
            }

            // Context menu
            const auto& acrTheme = GetCurrentAcrylicTheme();
            if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
                if (ImGui::MenuItem(TR("copy_address"))) {
                    ImGui::SetClipboardText(addr.address.c_str());
                    s_copiedRow = row_idx;
                    s_copiedTime = (float)ImGui::GetTime();
                }
                if (ImGui::MenuItem(TR("send_from_this_address"))) {
                    SetSendFromAddress(addr.address);
                    app->setCurrentPage(NavPage::Send);
                }
                ImGui::Separator();
                if (ImGui::MenuItem(TR("set_label"))) {
                    AddressLabelDialog::show(app, addr.address, row.isZ);
                }
                if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
                    app->setMiningAddress(addr.address, !row.mining);
                }
                if (ImGui::MenuItem(TR("export_private_key")))
                    KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
                if (row.isZ) {
                    if (ImGui::MenuItem(TR("export_viewing_key")))
                        KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
                }
                if (ImGui::MenuItem(TR("show_qr_code")))
                    QRPopupDialog::show(addr.address, row.isZ ? TR("z_address") : TR("t_address"));
                ImGui::Separator();
                if (row.hidden) {
                    if (ImGui::MenuItem(TR("restore_address")))
                        app->unhideAddress(addr.address);
                } else if (addr.balance < 1e-9) {
                    if (ImGui::MenuItem(TR("hide_address")))
                        app->hideAddress(addr.address);
                }
                if (row.favorite) {
                    if (ImGui::MenuItem(TR("remove_favorite")))
                        app->unfavoriteAddress(addr.address);
                } else {
                    if (ImGui::MenuItem(TR("favorite_address")))
                        app->favoriteAddress(addr.address);
                }
                // Portfolio membership — toggle this address in each Market-tab portfolio entry.
                ImGui::Separator();
                if (ImGui::BeginMenu(TR("portfolio_add_to"))) {
                    const auto& pf = app->settings()->getPortfolioEntries();
                    const std::string activeHash = app->activeWalletIdentityHash();
                    int shown = 0;
                    for (int pi = 0; pi < (int)pf.size(); pi++) {
                        // Only this wallet's groups (or legacy/unscoped), matching the Market summary —
                        // don't offer to add this address to another wallet's group.
                        if (!(pf[pi].scope.empty() || activeHash.empty() || pf[pi].scope == activeHash))
                            continue;
                        // PushID keeps the item ID unique even when two groups share a label (a
                        // label-derived MenuItem ID would collide); a blank label shows a placeholder
                        // instead of rendering as an invisible row.
                        ImGui::PushID(pi);
                        bool inSet = dragonx::data::PortfolioEntryContains(pf[pi].addresses, addr.address);
                        const char* lbl = pf[pi].label.empty() ? TR("portfolio_new_entry") : pf[pi].label.c_str();
                        if (ImGui::MenuItem(lbl, nullptr, inSet)) {
                            auto entries = pf;  // copy, mutate, persist
                            if (inSet) dragonx::data::PortfolioEntryRemove(entries[pi].addresses, addr.address);
                            else       dragonx::data::PortfolioEntryAdd(entries[pi].addresses, addr.address);
                            app->settings()->setPortfolioEntries(entries);
                            app->settings()->save();
                        }
                        ImGui::PopID();
                        shown++;
                    }
                    if (shown == 0)
                        ImGui::MenuItem(TR("portfolio_no_entries"), nullptr, false, false);
                    ImGui::EndMenu();
                }
                effects::ImGuiAcrylic::EndAcrylicPopup();
            }
            ImGui::PopID();

            // Separator between rows
            if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
                float sepY = rowY[row_idx] + rowH;
                dl->AddLine(
                    ImVec2(rowPos.x + rowPadLeft + rowIconSz * 2.0f, sepY),
                    ImVec2(rowPos.x + innerW - Layout::spacingLg(), sepY),
                    dividerCol);
            }
        }

        // Advance cursor past all rows
        ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,
            cursorStartY + (float)rows.size() * rowH));

        // ---- Keyboard navigation ----
        if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl && !s_dragActive) {
            bool nav = false;
            if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
                if (selected_row > 0) { selected_row--; nav = true; }
                else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
            }
            if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
                if (selected_row < (int)rows.size() - 1) { selected_row++; nav = true; }
                else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
            }
            if (ImGui::IsKeyPressed(ImGuiKey_Enter) && selected_row >= 0 && selected_row < (int)rows.size()) {
                ImGui::SetClipboardText(rows[selected_row].info->address.c_str());
                s_copiedRow = selected_row;
                s_copiedTime = (float)ImGui::GetTime();
            }
            if (ImGui::IsKeyPressed(ImGuiKey_F2) && selected_row >= 0 && selected_row < (int)rows.size()) {
                const auto& r = rows[selected_row];
                AddressLabelDialog::show(app, r.info->address, r.isZ);
            }
            (void)nav;
        }
    }

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

    // Scroll fade
    {
        float fadeZone = std::min(
            (Type().body2()->LegacySize + Type().caption()->LegacySize +
             Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
            addrListH * 0.18f);
        ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
                            listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
    }

    // Address count
    {
        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
        char countBuf[128];
        int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
        snprintf(countBuf, sizeof(countBuf), TR("showing_x_of_y"),
                 (int)rows.size(), totalAddrs);
        Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
    }
}

// Render the shared recent transactions section (used by all layouts)
void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const float dp = Layout::dpiScale();
    const auto actionBtn = S.button("tabs.balance", "action-button");
    const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
    const auto& state = app->state();

    float headerStartY = ImGui::GetCursorPosY();
    ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
    Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_transactions"));
    ImGui::SameLine();
    if (TactileSmallButton(TR("view_all"), S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
        app->setCurrentPage(NavPage::History);
    }
    ImGui::Spacing();
    float headerHeight = ImGui::GetCursorPosY() - headerStartY;

    float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
    float availableListH = ImGui::GetContentRegionAvail().y;
    float listH = std::max({scaledRowH, recentH, availableListH});
    ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
        ImGuiWindowFlags_NoBackground);

    const auto& txs = state.transactions;
    float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
    // Only draw as many rows as fully fit within the reserved section height (header + rows);
    // dropping the overflow row is fine since "View All" already links to full History.
    int maxRows = std::max(1, (int)((recentH - headerHeight) / rowH));
    int count = std::min({4, maxRows, (int)txs.size()});  // show only the most recent (state.transactions is newest-first)

    if (count == 0) {
        Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions_yet"));
    } else {
        ImDrawList* dl = ImGui::GetWindowDrawList();
        ImFont* capFont = Type().caption();
        float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
                                 S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);

        for (int i = 0; i < count; i++) {
            const auto& tx = txs[i];
            ImVec2 rowPos = ImGui::GetCursorScreenPos();
            float rowY = rowPos.y + rowH * 0.5f;
            auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
            DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));

            float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());

            // Start the address column past the MEASURED type-label width (plus a fixed gap) so it can
            // never overlap the label — a fixed schema offset shrinks below the label width at narrow
            // widths (hs < 1) and collides. Mirrors how amtX/agoSz measure their own text below.
            ImVec2 typeSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.typeText.c_str());
            float addrX = tx_x + typeSz.x + Layout::spacingMd();
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());

            ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.amountText.c_str());
            float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
            float amtX = rightEdge - amtSz.x - std::max(
                S.drawElement("tabs.balance", "amount-right-min-margin").size,
                S.drawElement("tabs.balance", "amount-right-margin").size * hs);
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(amtX, rowPos.y + 2 * dp),
                        recentTxAmountColor(tx.type), display.amountText.c_str());

            ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
            dl->AddText(capFont, capFont->LegacySize,
                        ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs, rowPos.y + 2 * dp),
                        OnSurfaceDisabled(), display.timeText.c_str());

            float rowW = ImGui::GetContentRegionAvail().x;
            ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
            if (material::IsRectHovered(rowPos, rowEnd)) {
                dl->AddRectFilled(rowPos, rowEnd, SurfaceOverlay(15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                if (ImGui::IsMouseClicked(0))
                    app->setCurrentPage(NavPage::History);
            }
            ImGui::Dummy(ImVec2(0, rowH));
        }
    }
    ImGui::EndChild();
}

// Render sync progress bar (used by multiple layouts)
void RenderSyncBar(App* app, ImDrawList* dl, float vs) {
    using namespace material;
    const auto& S = schema::UISchema::instance();
    const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
    const auto& state = app->state();
    const float dp = Layout::dpiScale();

    if (state.sync.syncing && state.sync.headers > 0) {
        float prog = static_cast<float>(state.sync.verification_progress);
        if (prog > 1.0f) prog = 1.0f;
        float barH = (syncBar.height >= 0) ? syncBar.height : 3.0f;
        float barW = ImGui::GetContentRegionAvail().x;
        ImVec2 barPos = ImGui::GetCursorScreenPos();
        dl->AddRectFilled(barPos,
            ImVec2(barPos.x + barW, barPos.y + barH),
            SurfaceOverlay(15), 1.0f * dp);
        dl->AddRectFilled(barPos,
            ImVec2(barPos.x + barW * prog, barPos.y + barH),
            WithAlpha(Warning(), 200), 1.0f * dp);
        ImGui::Dummy(ImVec2(barW, barH));
    }
}

} // namespace ui
} // namespace dragonx
