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

#include "transactions_tab.h"
#include "transaction_details_dialog.h"
#include "export_transactions_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../util/text_format.h"
#include "../../config/settings.h"
#include "../../config/version.h"
#include "../theme.h"
#include "../effects/imgui_acrylic.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <ctime>
#include <cmath>
#include <unordered_map>
#include <vector>

namespace dragonx {
namespace ui {

using namespace material;

static std::string TrId(const char* key, const char* id) {
    return std::string(TR(key)) + "##" + id;
}

// Helper to truncate strings
static std::string truncateString(const std::string& str, int maxLen = 16) {
    if (str.length() <= static_cast<size_t>(maxLen)) return str;
    int half = (maxLen - 3) / 2;
    return str.substr(0, half) + "..." + str.substr(str.length() - half);
}

// Case-insensitive string search
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
    return dragonx::util::containsIgnoreCase(str, search);
}

// A display-ready transaction that may be a merged autoshield pair.
// For non-merged entries, send_idx or recv_idx is -1.
struct DisplayTx {
    std::string txid;
    std::string display_type;    // "send", "receive", "mined", "immature", "shield"
    double amount = 0.0;
    int64_t timestamp = 0;
    int confirmations = 0;
    std::string address;         // primary display address
    std::string from_address;
    std::string memo;
    int orig_idx = -1;           // index into state.transactions (first/primary)
    int send_idx = -1;           // index of send leg (shield only)
    int recv_idx = -1;           // index of recv leg (shield only)
    bool is_shield = false;

    bool isConfirmed() const { return confirmations >= 1; }
    std::string getTimeString() const;
};

std::string DisplayTx::getTimeString() const {
    if (timestamp <= 0) return TR("pending");
    std::time_t t = static_cast<std::time_t>(timestamp);
    char buf[64];
    std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", std::localtime(&t));
    return buf;
}

// Relative time string (localized long form, e.g. "5 minutes ago")
static std::string timeAgo(int64_t timestamp) {
    return dragonx::util::formatTimeAgo(timestamp);
}

// Draw a small transaction-type icon
static void DrawTxIcon(ImDrawList* dl, const std::string& type,
                       float cx, float cy, float /*s*/, ImU32 col)
{
    using namespace material;
    ImFont* iconFont = Type().iconSmall();
    const char* icon;
    if (type == "send") {
        icon = ICON_MD_CALL_MADE;
    } else if (type == "receive") {
        icon = ICON_MD_CALL_RECEIVED;
    } else if (type == "shield") {
        icon = ICON_MD_CALL_MADE;
    } else {
        icon = ICON_MD_CONSTRUCTION;
    }
    ImVec2 sz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
    dl->AddText(iconFont, iconFont->LegacySize,
                ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
}

void RenderTransactionsTab(App* app)
{
    auto& S = schema::UISchema::instance();
    const auto searchInput  = S.input("tabs.transactions", "search-input");
    const auto filterCombo  = S.combo("tabs.transactions", "filter-combo");
    const auto filterGapEl  = S.drawElement("tabs.transactions", "filter-gap");
    const auto txTable      = S.table("tabs.transactions", "transaction-table");
    const auto addrLabel    = S.label("tabs.transactions", "address-label");
    const auto& state = app->state();

    // Responsive scale factors (recomputed every frame)
    ImVec2 contentAvail = ImGui::GetContentRegionAvail();
    const float hs = Layout::hScale(contentAvail.x);
    const float vs = Layout::vScale(contentAvail.y);
    const float glassRound = Layout::glassRounding();
    const float innerPad   = Layout::cardInnerPadding();
    const float cGap       = Layout::cardGap();

    // Non-scrolling container — content resizes to fit available height
    ImVec2 txAvail = ImGui::GetContentRegionAvail();
    ImGui::BeginChild("##TxScroll", txAvail, false,
        ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);

    ImDrawList* dl = ImGui::GetWindowDrawList();
    GlassPanelSpec glassSpec;
    glassSpec.rounding = glassRound;
    ImFont* ovFont  = Type().overline();
    ImFont* capFont = Type().caption();
    ImFont* body2   = Type().body2();
    char buf[128];

    ImU32 greenCol = Success();
    ImU32 redCol   = Error();
    ImU32 goldCol  = Warning();
    std::string txLoadingText = app->transactionRefreshProgressText();
    bool txLoading = !txLoadingText.empty();

    // Expanded row index for inline detail
    static int s_expanded_row = -1;

    // Pagination state
    static int s_current_page = 0;
    static int s_prev_filter_hash = 0;  // detect filter changes to reset page

    // ================================================================
    // Summary Cards — Received | Sent | Mined
    // ================================================================
    {
        int recvCount = 0, sendCount = 0, minedCount = 0;
        double recvTotal = 0.0, sendTotal = 0.0, minedTotal = 0.0;

        // Identify autoshield legs (same txid with a "send" leg and a "receive"-to-z leg):
        // that pair is a single internal shielding move — shown as one "Shield" row in the
        // list below — not income or spending. Counting both legs double-counts the amount
        // into BOTH the Sent and Received totals, so the cards disagree with the list.
        // Mirror the list's pairing logic and exclude both legs from the totals.
        std::unordered_map<std::string, std::vector<size_t>> summaryTxidMap;
        for (size_t i = 0; i < state.transactions.size(); i++)
            summaryTxidMap[state.transactions[i].txid].push_back(i);
        std::vector<bool> isShieldLeg(state.transactions.size(), false);
        for (const auto& kv : summaryTxidMap) {
            if (kv.second.size() < 2) continue;
            int send_i = -1, recv_i = -1;
            for (size_t si : kv.second) {
                const auto& stx = state.transactions[si];
                if (stx.type == "send" && send_i < 0) send_i = (int)si;
                else if (stx.type == "receive" && recv_i < 0 &&
                         !stx.address.empty() && stx.address[0] == 'z') recv_i = (int)si;
            }
            if (send_i >= 0 && recv_i >= 0) {
                isShieldLeg[send_i] = true;
                isShieldLeg[recv_i] = true;
                // The list shows the merged shield under the "Sent" filter, so count it there too —
                // otherwise the Sent card reads 0 while the Sent list is non-empty. Use the shielded
                // (receive-leg) amount, which is what the merged row displays.
                sendCount++;
                sendTotal += std::abs(state.transactions[recv_i].amount);
            }
        }

        for (size_t i = 0; i < state.transactions.size(); i++) {
            if (isShieldLeg[i]) continue;  // internal shielding move — not received or sent
            const auto& tx = state.transactions[i];
            if (tx.type == "receive") {
                recvCount++;
                recvTotal += std::abs(tx.amount);
            } else if (tx.type == "send") {
                sendCount++;
                sendTotal += std::abs(tx.amount);
            } else if (tx.type == "generate" || tx.type == "immature" || tx.type == "mined") {
                minedCount++;
                minedTotal += std::abs(tx.amount);
            }
        }

        float availWidth = ImGui::GetContentRegionAvail().x;
        float cardGap = cGap;
        float cardW = (availWidth - 2 * cardGap) / 3.0f;
        float cardH = Layout::cardHeight(70.0f, vs);
        float iconSz = std::max(4.0f, schema::UI().drawElement("tabs.transactions", "summary-icon-size").size * hs);
        ImVec2 origin = ImGui::GetCursorScreenPos();

        // Clickable type filter (clicking a card sets the type filter)
        static int type_filter = 0;

        // --- Received card ---
        {
            ImVec2 cMin = origin;
            ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
            DrawGlassPanel(dl, cMin, cMax, glassSpec);

            float cx = cMin.x + innerPad;
            float cy = cMin.y + Layout::spacingMd();

            // Icon
            DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);

            float labelX = cx + iconSz * 3.0f;
            dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("received_upper"));
            cy += ovFont->LegacySize + Layout::spacingSm();

            snprintf(buf, sizeof(buf), TR("txs_count"), recvCount);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
            cy += capFont->LegacySize + Layout::spacingXs();

            snprintf(buf, sizeof(buf), "+%.4f %s", recvTotal, DRAGONX_TICKER);
            dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), greenCol, buf);

            if (material::IsRectHovered(cMin, cMax)) {
                dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 2) ? 0 : 2;
            }
        }

        // --- Sent card ---
        {
            float xOff = cardW + cardGap;
            ImVec2 cMin(origin.x + xOff, origin.y);
            ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
            DrawGlassPanel(dl, cMin, cMax, glassSpec);

            float cx = cMin.x + innerPad;
            float cy = cMin.y + Layout::spacingMd();

            DrawTxIcon(dl, "send", cx + iconSz, cy + iconSz * 1.33f, iconSz, redCol);

            float labelX = cx + iconSz * 3.0f;
            dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("sent_upper"));
            cy += ovFont->LegacySize + Layout::spacingSm();

            snprintf(buf, sizeof(buf), TR("txs_count"), sendCount);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
            cy += capFont->LegacySize + Layout::spacingXs();

            snprintf(buf, sizeof(buf), "-%.4f %s", sendTotal, DRAGONX_TICKER);
            dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), redCol, buf);

            if (material::IsRectHovered(cMin, cMax)) {
                dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 1) ? 0 : 1;
            }
        }

        // --- Mined card ---
        {
            float xOff = 2 * (cardW + cardGap);
            ImVec2 cMin(origin.x + xOff, origin.y);
            ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
            DrawGlassPanel(dl, cMin, cMax, glassSpec);

            float cx = cMin.x + innerPad;
            float cy = cMin.y + Layout::spacingMd();

            DrawTxIcon(dl, "mined", cx + iconSz, cy + iconSz * 1.33f, iconSz, goldCol);

            float labelX = cx + iconSz * 3.0f;
            dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("mined_upper"));
            cy += ovFont->LegacySize + Layout::spacingSm();

            snprintf(buf, sizeof(buf), TR("txs_count"), minedCount);
            dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
            cy += capFont->LegacySize + Layout::spacingXs();

            snprintf(buf, sizeof(buf), "+%.4f %s", minedTotal, DRAGONX_TICKER);
            dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), goldCol, buf);

            if (material::IsRectHovered(cMin, cMax)) {
                dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 3) ? 0 : 3;
            }
        }

        // Selected card accent
        if (type_filter > 0) {
            int idx_map[] = {-1, 1, 0, 2};
            int idx = idx_map[type_filter];
            float xOff = idx * (cardW + cardGap);
            ImVec2 acMin(origin.x + xOff, origin.y + cardH - 3);
            ImVec2 acMax(origin.x + xOff + cardW, origin.y + cardH);
            ImU32 acCol = (type_filter == 1) ? redCol : (type_filter == 2) ? greenCol : goldCol;
            dl->AddRectFilled(acMin, acMax, acCol, 2.0f);
        }

        ImGui::Dummy(ImVec2(availWidth, cardH));
        ImGui::Dummy(ImVec2(0, Layout::spacingMd()));

        // ================================================================
        // Search & Filter bar
        // ================================================================
        static char search_filter[128] = "";
        float searchMaxW = (searchInput.maxWidth >= 0) ? searchInput.maxWidth : 300.0f;
        float searchRatio = (searchInput.widthRatio >= 0) ? searchInput.widthRatio : 0.30f;
        float searchWidth = std::min(searchMaxW * hs, availWidth * searchRatio);
        
        ImGui::SetNextItemWidth(searchWidth);
        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
        ImGui::InputTextWithHint("##TxSearch", TR("search_placeholder"), search_filter, sizeof(search_filter));
        ImGui::PopStyleVar();
        
        float filterGap = std::max(8.0f, ((filterGapEl.size > 0) ? filterGapEl.size : 20.0f) * hs);
        ImGui::SameLine(0, filterGap);
        float comboW = std::max(80.0f, ((filterCombo.width > 0) ? filterCombo.width : 120.0f) * hs);
        ImGui::SetNextItemWidth(comboW);
        const char* types[] = { TR("all_filter"), TR("sent_filter"), TR("received_filter"), TR("mined_filter") };
        ImGui::Combo("##TxType", &type_filter, types, IM_ARRAYSIZE(types));

        ImGui::SameLine(0, filterGap);
        if (TactileButton(TrId("refresh", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
            app->refreshNow();
        }

        ImGui::SameLine(0, filterGap);
        if (TactileButton(TrId("export_csv", "tx").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
            ExportTransactionsDialog::show();
        }

        if (txLoading) {
            ImGui::SameLine(0, filterGap);
            ImGui::PushFont(Type().iconSmall());
            float pulse = 0.55f + 0.45f * std::sin((float)ImGui::GetTime() * 3.0f);
            ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, pulse), ICON_MD_HOURGLASS_EMPTY);
            ImGui::PopFont();
            ImGui::SameLine(0, Layout::spacingXs());
            int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
            const char* dotStr[] = {"", ".", "..", "..."};
            ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "%s%s", txLoadingText.c_str(), dotStr[dots]);
        }

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

        // ================================================================
        // Transaction list — DrawList-based rows in scrollable child
        // ================================================================

        int filtered_count = 0;
        std::string search_str(search_filter);

        // Build display list, merging autoshield send+receive pairs.
        // Two entries sharing the same txid where one is "send" and
        // one is "receive" to a z-address are combined into a single
        // "shield" entry.  We first index by txid, then build the list.
        //
        // This merge + sort is O(N log N) with several heap allocations, so it is MEMOIZED: it only
        // rebuilds when the underlying transactions actually change, not every frame. The cache key
        // is a cheap, allocation-free FNV-1a fingerprint over the fields that affect the displayed
        // rows (count, last update time, and each tx's confirmations / timestamp / type+address
        // first char). A new block bumps every confirmation, so the key changes and we rebuild;
        // between changes (the common case while the user reads/scrolls) we reuse the cache. The
        // result is already sorted newest-first by the refresh service, but we re-sort here to apply
        // the "pending first" ordering — also folded into the memoized build.
        static std::vector<DisplayTx> s_display_cache;
        static std::uint64_t s_display_cache_key = 0;
        static bool s_display_cache_valid = false;

        std::uint64_t displayKey;
        {
            std::uint64_t h = 1469598103934665603ULL;  // FNV-1a offset basis
            auto mix = [&h](std::uint64_t v) { h = (h ^ v) * 1099511628211ULL; };
            mix(state.transactions.size());
            mix(static_cast<std::uint64_t>(state.last_tx_update));
            for (const auto& t : state.transactions) {
                mix(static_cast<std::uint64_t>(t.confirmations));
                mix(static_cast<std::uint64_t>(t.timestamp));
                mix(t.type.empty()    ? 0u : static_cast<unsigned char>(t.type[0]));
                mix(t.address.empty() ? 0u : static_cast<unsigned char>(t.address[0]));
            }
            displayKey = h;
        }

        if (!s_display_cache_valid || displayKey != s_display_cache_key) {
            s_display_cache.clear();
            std::vector<DisplayTx>& display_txns = s_display_cache;
            // Map txid -> indices in state.transactions
            std::unordered_map<std::string, std::vector<size_t>> txid_map;
            for (size_t i = 0; i < state.transactions.size(); i++) {
                txid_map[state.transactions[i].txid].push_back(i);
            }

            std::vector<bool> consumed(state.transactions.size(), false);

            for (size_t i = 0; i < state.transactions.size(); i++) {
                if (consumed[i]) continue;
                const auto& tx = state.transactions[i];

                // Try to find an autoshield pair (same txid, send+receive to z-addr)
                bool merged = false;
                const auto& siblings = txid_map[tx.txid];
                if (siblings.size() >= 2) {
                    int send_i = -1, recv_i = -1;
                    for (size_t si : siblings) {
                        if (consumed[si]) continue;
                        const auto& stx = state.transactions[si];
                        if (stx.type == "send" && send_i < 0) send_i = (int)si;
                        else if (stx.type == "receive" && recv_i < 0) recv_i = (int)si;
                    }
                    if (send_i >= 0 && recv_i >= 0) {
                        const auto& stx = state.transactions[send_i];
                        const auto& rtx = state.transactions[recv_i];
                        // Confirm receive goes to a z-address (shielded)
                        bool recv_is_shielded = !rtx.address.empty() && rtx.address[0] == 'z';
                        if (recv_is_shielded) {
                            DisplayTx dtx;
                            dtx.txid = tx.txid;
                            dtx.display_type = "shield";
                            dtx.is_shield = true;
                            dtx.amount = rtx.amount;  // positive receive amount
                            dtx.timestamp = std::max(stx.timestamp, rtx.timestamp);
                            // Both legs are the SAME transaction, so they share one real confirmation
                            // count — but the send leg (from z_viewtransaction) often comes through
                            // with confirmations=0. min() would then make a long-confirmed shield look
                            // pending (conf==0), which the sort floats to the very top, out of date
                            // order. Take the populated value.
                            dtx.confirmations = std::max(stx.confirmations, rtx.confirmations);
                            dtx.address = rtx.address; // shielded destination
                            dtx.from_address = stx.address.empty() ? stx.from_address : stx.address;
                            dtx.memo = rtx.memo.empty() ? stx.memo : rtx.memo;
                            dtx.orig_idx = send_i;
                            dtx.send_idx = send_i;
                            dtx.recv_idx = recv_i;
                            consumed[send_i] = true;
                            consumed[recv_i] = true;
                            display_txns.push_back(std::move(dtx));
                            merged = true;
                        }
                    }
                }

                if (!merged) {
                    consumed[i] = true;
                    DisplayTx dtx;
                    dtx.txid = tx.txid;
                    dtx.display_type = tx.type;
                    dtx.amount = tx.amount;
                    dtx.timestamp = tx.timestamp;
                    dtx.confirmations = tx.confirmations;
                    dtx.address = tx.address;
                    dtx.from_address = tx.from_address;
                    dtx.memo = tx.memo;
                    dtx.orig_idx = (int)i;
                    display_txns.push_back(std::move(dtx));
                }
            }

            // Sort: pending (0-conf) transactions first, then newest-first by timestamp, with txid as
            // a final deterministic tiebreak so same-block transactions keep a stable order across
            // rebuilds (otherwise equal timestamps reorder every time a new block bumps confirmations).
            std::sort(display_txns.begin(), display_txns.end(),
                [](const DisplayTx& a, const DisplayTx& b) {
                    bool aPending = (a.confirmations == 0);
                    bool bPending = (b.confirmations == 0);
                    if (aPending != bPending) return aPending;
                    if (a.timestamp != b.timestamp) return a.timestamp > b.timestamp;
                    return a.txid > b.txid;
                });

            s_display_cache_key = displayKey;
            s_display_cache_valid = true;
        }

        // The merged + sorted list (rebuilt above only on change). Filtering/search/pagination below
        // run per-frame over this cache — they're cheap (a linear scan) and depend on interactive
        // filter/search state, so they are intentionally not memoized.
        const std::vector<DisplayTx>& display_txns = s_display_cache;

        // Apply type + search filters
        std::vector<size_t> filtered_indices;
        for (size_t i = 0; i < display_txns.size(); i++) {
            const auto& dtx = display_txns[i];
            if (type_filter != 0) {
                if (type_filter == 1 && dtx.display_type != "send" && dtx.display_type != "shield") continue;
                if (type_filter == 2 && dtx.display_type != "receive") continue;
                if (type_filter == 3 && dtx.display_type != "generate" && dtx.display_type != "immature" && dtx.display_type != "mined") continue;
            }
            if (!search_str.empty()) {
                if (!containsIgnoreCase(dtx.address, search_str) &&
                    !containsIgnoreCase(dtx.txid, search_str) &&
                    !containsIgnoreCase(dtx.memo, search_str) &&
                    !(dtx.is_shield && containsIgnoreCase(std::string("shielded"), search_str))) {
                    continue;
                }
            }
            filtered_indices.push_back(i);
        }
        filtered_count = static_cast<int>(filtered_indices.size());

        // Pagination — slice filtered results into pages
        int perPage = std::max(10, (int)schema::UI().drawElement("tabs.transactions", "rows-per-page").sizeOr(50.0f));
        int totalPages = std::max(1, (filtered_count + perPage - 1) / perPage);

        // Reset page when filters change
        int filterHash = type_filter * 1000003 + filtered_count * 31 + static_cast<int>(search_str.size());
        if (filterHash != s_prev_filter_hash) {
            s_current_page = 0;
            s_prev_filter_hash = filterHash;
        }
        if (s_current_page >= totalPages) s_current_page = totalPages - 1;
        if (s_current_page < 0) s_current_page = 0;

        int pageStart = s_current_page * perPage;
        int pageEnd   = std::min(pageStart + perPage, filtered_count);

        // ---- Heading line: "TRANSACTIONS" left, pagination right ----
        {
            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transactions_upper"));

            if (totalPages > 1) {
                float paginationH = ImGui::GetFrameHeight();
                float btnW = paginationH;
                float gap = Layout::spacingSm();
                float pageNumW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, " 999 / 999 ").x + Layout::spacingLg();
                float totalPagW = btnW * 4 + pageNumW + gap * 4;

                // Right-align: position cursor so the group ends at the right edge
                ImGui::SameLine();
                float startX = ImGui::GetContentRegionMax().x - totalPagW;
                ImGui::SetCursorPosX(startX);

                ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm());
                ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.5f, 0.5f));
                ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 30)));
                ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 45)));

                // First page
                ImGui::BeginDisabled(s_current_page == 0);
                ImGui::PushFont(Type().iconSmall());
                if (ImGui::Button(ICON_MD_FIRST_PAGE "##txFirst", ImVec2(btnW, btnW))) {
                    s_current_page = 0;
                    s_expanded_row = -1;
                }
                ImGui::PopFont();
                ImGui::EndDisabled();

                ImGui::SameLine(0, gap);

                // Previous page
                ImGui::BeginDisabled(s_current_page == 0);
                ImGui::PushFont(Type().iconSmall());
                if (ImGui::Button(ICON_MD_CHEVRON_LEFT "##txPrev", ImVec2(btnW, btnW))) {
                    s_current_page--;
                    s_expanded_row = -1;
                }
                ImGui::PopFont();
                ImGui::EndDisabled();

                ImGui::SameLine(0, gap);

                // Page indicator — render centered text over a fixed-width dummy
                {
                    snprintf(buf, sizeof(buf), "%d / %d", s_current_page + 1, totalPages);
                    ImVec2 pageSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
                    ImVec2 regionPos = ImGui::GetCursorScreenPos();
                    // Reserve the fixed width without advancing to a new line
                    ImGui::InvisibleButton("##pageNum", ImVec2(pageNumW, paginationH));
                    // Draw the text centered within that reserved area
                    ImVec2 textPos(
                        regionPos.x + (pageNumW - pageSz.x) * 0.5f,
                        regionPos.y + (paginationH - pageSz.y) * 0.5f
                    );
                    ImGui::GetWindowDrawList()->AddText(capFont, capFont->LegacySize, textPos, OnSurfaceMedium(), buf);
                }

                ImGui::SameLine(0, gap);

                // Next page
                ImGui::BeginDisabled(s_current_page >= totalPages - 1);
                ImGui::PushFont(Type().iconSmall());
                if (ImGui::Button(ICON_MD_CHEVRON_RIGHT "##txNext", ImVec2(btnW, btnW))) {
                    s_current_page++;
                    s_expanded_row = -1;
                }
                ImGui::PopFont();
                ImGui::EndDisabled();

                ImGui::SameLine(0, gap);

                // Last page
                ImGui::BeginDisabled(s_current_page >= totalPages - 1);
                ImGui::PushFont(Type().iconSmall());
                if (ImGui::Button(ICON_MD_LAST_PAGE "##txLast", ImVec2(btnW, btnW))) {
                    s_current_page = totalPages - 1;
                    s_expanded_row = -1;
                }
                ImGui::PopFont();
                ImGui::EndDisabled();

                ImGui::PopStyleColor(3);
                ImGui::PopStyleVar(2);
            }
        }
        ImGui::Dummy(ImVec2(0, Layout::spacingXs()));

        // Glass panel wrapping the list area — scale reserve with vScale
        float scaledReserve = (txTable.bottomReserve > 0) ? txTable.bottomReserve : std::max(schema::UI().drawElement("tabs.transactions", "bottom-reserve-min").size, schema::UI().drawElement("tabs.transactions", "bottom-reserve-base").size * vs);
        float listH = ImGui::GetContentRegionAvail().y - scaledReserve;
        float minListH = std::max(schema::UI().drawElement("tabs.transactions", "list-min-height").size, schema::UI().drawElement("tabs.transactions", "list-base-height").size * vs);
        if (listH < minListH) listH = minListH;

        ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
        ImVec2 listPanelMax(listPanelMin.x + availWidth, listPanelMin.y + listH);
        DrawGlassPanel(dl, listPanelMin, listPanelMax, glassSpec);

        // Scroll state for clipping mask (captured inside child, used after EndChild)
        float scrollY = 0.0f;
        float scrollMaxY = 0.0f;

        // Vertex start indices for CSS-style clipping mask (alpha fade at edges)
        int vtxMaskStart = dl->VtxBuffer.Size;

        ImGui::BeginChild("##TxList", ImVec2(availWidth, listH), false,
                          ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
        ApplySmoothScroll();
        ImDrawList* childDL = ImGui::GetWindowDrawList();
        int childVtxStart = childDL->VtxBuffer.Size;
        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
        {
            if (!app->isConnected()) {
                ImGui::Dummy(ImVec2(0, 20));
                Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
                                   TR(app->isLiteBuild() ? "lite_no_wallet" : "not_connected"));
            } else if (state.transactions.empty()) {
                ImGui::Dummy(ImVec2(0, 20));
                if (txLoading) {
                    int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
                    const char* dotStr[] = {"", ".", "..", "..."};
                    snprintf(buf, sizeof(buf), "%s%s", txLoadingText.c_str(), dotStr[dots]);
                    Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
                } else {
                    Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_transactions"));
                }
            } else if (filtered_indices.empty()) {
                ImGui::Dummy(ImVec2(0, 20));
                Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_matching"));
            } else {
                float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
                float innerW = ImGui::GetContentRegionAvail().x;
                float rowIconSz = std::max(3.5f, schema::UI().drawElement("tabs.transactions", "row-icon-size").size * hs);
                float rowPadLeft = Layout::spacingLg();

                // Scroll state for gradient overlay fade (drawn after EndChild)
                scrollY    = ImGui::GetScrollY();
                scrollMaxY = ImGui::GetScrollMaxY();

                // Render only the current page slice (pagination already bounds per-frame work)
                for (int fi = pageStart; fi < pageEnd; fi++) {
                    size_t i = filtered_indices[fi];
                    const auto& tx = display_txns[i];
                    bool is_expanded = (s_expanded_row == static_cast<int>(i));

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

                    // Determine type info
                    bool shieldedDisplay = tx.display_type == "shield";
                    ImU32 iconCol;
                    const char* typeStr;
                    if (shieldedDisplay) {
                        iconCol = redCol; typeStr = TR("sent_type");
                    } else if (tx.display_type == "receive") {
                        iconCol = greenCol; typeStr = TR("recv_type");
                    } else if (tx.display_type == "send") {
                        iconCol = redCol; typeStr = TR("sent_type");
                    } else if (tx.display_type == "immature") {
                        iconCol = Warning(); typeStr = TR("immature_type");
                    } else {
                        iconCol = goldCol; typeStr = TR("mined_type");
                    }

                    // Expanded selection accent
                    if (is_expanded) {
                        dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), schema::UI().drawElement("tabs.transactions", "row-hover-rounding").size);
                        dl->AddRectFilled(rowPos, ImVec2(rowPos.x + schema::UI().drawElement("tabs.transactions", "row-accent-width").size, rowEnd.y), Primary(), schema::UI().drawElement("tabs.transactions", "accent-bar-rounding").size);
                    }

                    // Hover glow
                    bool hovered = material::IsRectHovered(rowPos, rowEnd);
                    if (hovered && !is_expanded) {
                        dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), schema::UI().drawElement("tabs.transactions", "row-hover-rounding").size);
                        ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                    }

                    float cx = rowPos.x + rowPadLeft;
                    float cy = rowPos.y + Layout::spacingMd();

                    // Icon
                    DrawTxIcon(dl, shieldedDisplay ? "send" : tx.display_type,
                               cx + rowIconSz, cy + body2->LegacySize * 0.5f, rowIconSz, iconCol);

                    // Type label
                    float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
                    dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), iconCol, typeStr);

                    // Time (next to type)
                    std::string ago = timeAgo(tx.timestamp);
                    float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeStr).x;
                    dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
                                OnSurfaceDisabled(), ago.c_str());

                    // Address (second line, left side)
                    std::string addr_display = truncateString(tx.address, (addrLabel.truncate > 0) ? addrLabel.truncate : 20);
                    dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
                                OnSurfaceMedium(), addr_display.c_str());

                    // Amount (right-aligned, first line)
                    ImU32 amtCol = (tx.amount >= 0) ? greenCol : redCol;
                    if (tx.amount >= 0)
                        snprintf(buf, sizeof(buf), "+%.8f", tx.amount);
                    else
                        snprintf(buf, sizeof(buf), "%.8f", tx.amount);
                    ImVec2 amtSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf);
                    float amtX = rowPos.x + innerW - amtSz.x - Layout::spacingLg();
                    DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(amtX, cy), amtCol, buf,
                                   1.0f, 1.0f, IM_COL32(0, 0, 0, 120));

                    // USD equivalent (right-aligned, second line)
                    double priceUsd = state.market.price_usd;
                    if (priceUsd > 0.0) {
                        double usdVal = std::abs(tx.amount) * priceUsd;
                        if (usdVal >= 1.0)
                            snprintf(buf, sizeof(buf), "$%.2f", usdVal);
                        else if (usdVal >= 0.01)
                            snprintf(buf, sizeof(buf), "$%.4f", usdVal);
                        else
                            snprintf(buf, sizeof(buf), "$%.6f", usdVal);
                        ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
                        dl->AddText(capFont, capFont->LegacySize,
                                    ImVec2(rowPos.x + innerW - usdSz.x - Layout::spacingLg(), cy + body2->LegacySize + Layout::spacingXs()),
                                    OnSurfaceDisabled(), buf);
                    }

                    // Status badge (centered area, second line)
                    {
                        const char* statusStr;
                        ImU32 statusCol;
                        if (tx.confirmations == 0) {
                            statusStr = TR("pending"); statusCol = Warning();
                        } else if (tx.confirmations < 10) {
                            snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
                            statusStr = buf; statusCol = Warning();
                        } else if (tx.confirmations >= 100 && (tx.display_type == "generate" || tx.display_type == "mined")) {
                            statusStr = TR("mature"); statusCol = greenCol;
                        } else {
                            statusStr = TR("confirmed"); statusCol = WithAlpha(Success(), 140);
                        }
                        // Position status badge in the middle-right area
                        ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
                        const char* shieldedStr = TR("shielded_type");
                        ImVec2 shieldSz = shieldedDisplay
                            ? capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, shieldedStr)
                            : ImVec2(0, 0);
                        float shieldPillW = shieldSz.x + Layout::spacingSm() * 2.0f;
                        float stackW = shieldedDisplay ? std::max(sSz.x, shieldPillW) : sSz.x;
                        float statusX = amtX - stackW - Layout::spacingXxl();
                        float minStatusX = cx + innerW * 0.25f; // don't overlap address
                        if (statusX < minStatusX) statusX = minStatusX;
                        float statusTextX = statusX + (stackW - sSz.x) * 0.5f;
                        if (shieldedDisplay) {
                            float shieldX = statusX + (stackW - shieldSz.x) * 0.5f;
                            ImU32 shieldCol = Primary();
                            ImU32 shieldBg = (shieldCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
                            ImVec2 shieldPillMin(shieldX - Layout::spacingSm(), cy - 1.0f);
                            ImVec2 shieldPillMax(shieldX + shieldSz.x + Layout::spacingSm(),
                                                 shieldPillMin.y + capFont->LegacySize + Layout::spacingXs());
                            dl->AddRectFilled(shieldPillMin, shieldPillMax, shieldBg,
                                              schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
                            dl->AddText(capFont, capFont->LegacySize,
                                        ImVec2(shieldX, cy), shieldCol, shieldedStr);
                        }
                        // Background pill
                        ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast<ImU32>(30) << 24);
                        ImVec2 pillMin(statusTextX - Layout::spacingSm(), cy + body2->LegacySize + 1);
                        ImVec2 pillMax(statusTextX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs());
                        dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.transactions", "status-pill-rounding").size);
                        dl->AddText(capFont, capFont->LegacySize,
                                    ImVec2(statusTextX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr);
                    }

                    // Click to expand/collapse + invisible button for interaction
                    ImGui::InvisibleButton("##txRow", ImVec2(innerW, rowH));
                    if (ImGui::IsItemClicked(0)) {
                        s_expanded_row = is_expanded ? -1 : static_cast<int>(i);
                    }

                    // Tooltip
                    if (ImGui::IsItemHovered()) {
                        ImGui::SetTooltip("%s\n%s\n%s", tx.address.c_str(),
                            tx.txid.c_str(), tx.getTimeString().c_str());
                    }

                    // Context menu
                    const auto& acrylicTheme = GetCurrentAcrylicTheme();
                    if (effects::ImGuiAcrylic::BeginAcrylicContextItem("TxContext", 0, acrylicTheme.menu)) {
                        if (ImGui::MenuItem(TR("copy_address")) && !tx.address.empty()) {
                            ImGui::SetClipboardText(tx.address.c_str());
                        }
                        if (ImGui::MenuItem(TR("copy_txid"))) {
                            ImGui::SetClipboardText(tx.txid.c_str());
                        }
                        ImGui::Separator();
                        if (ImGui::MenuItem(TR("view_on_explorer"))) {
                            std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
                            util::Platform::openUrl(url);
                        }
                        if (ImGui::MenuItem(TR("view_details"))) {
                            if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
                                TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
                        }
                        effects::ImGuiAcrylic::EndAcrylicPopup();
                    }

                    // ---- Inline detail expansion ----
                    if (is_expanded) {
                        ImVec2 detailPos = ImGui::GetCursorScreenPos();
                        // We'll draw the glass panel after measuring the content
                        float detailPad = Layout::spacingLg();
                        float detailW = innerW - detailPad * 2;

                        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
                        ImGui::SetCursorPosX(ImGui::GetCursorPosX() + detailPad);
                        ImGui::BeginGroup();
                        ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + detailW);

                        // From address
                        if (!tx.from_address.empty()) {
                            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("from_upper"));
                            ImGui::TextWrapped("%s", tx.from_address.c_str());
                            ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
                        }

                        // To address
                        Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
                            tx.display_type == "send" ? TR("to_upper") : (tx.display_type == "shield" ? TR("shielded_to") : TR("address_upper")));
                        ImGui::TextWrapped("%s", tx.address.c_str());
                        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

                        // TxID (full, copyable)
                        Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("transaction_id"));
                        ImGui::TextWrapped("%s", tx.txid.c_str());
                        if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                        if (ImGui::IsItemClicked()) ImGui::SetClipboardText(tx.txid.c_str());
                        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

                        // Memo
                        if (!tx.memo.empty()) {
                            Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_upper"));
                            ImGui::TextWrapped("%s", tx.memo.c_str());
                            ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
                        }

                        // Confirmations + time
                        snprintf(buf, sizeof(buf), TR("confirmations_display"),
                            tx.confirmations, tx.getTimeString().c_str());
                        Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
                        ImGui::Dummy(ImVec2(0, Layout::spacingSm()));

                        // Action buttons
                        ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15)));
                        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 30)));
                        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.transactions", "detail-btn-rounding").size);
                        if (TactileSmallButton(TrId("copy_txid", "detail").c_str(), S.resolveFont("button"))) {
                            ImGui::SetClipboardText(tx.txid.c_str());
                        }
                        ImGui::SameLine();
                        if (!tx.address.empty() && TactileSmallButton(TrId("copy_address", "detail").c_str(), S.resolveFont("button"))) {
                            ImGui::SetClipboardText(tx.address.c_str());
                        }
                        ImGui::SameLine();
                        if (TactileSmallButton(TrId("explorer", "detail").c_str(), S.resolveFont("button"))) {
                            std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
                            util::Platform::openUrl(url);
                        }
                        ImGui::SameLine();
                        if (TactileSmallButton(TrId("full_details", "detail").c_str(), S.resolveFont("button"))) {
                            if (tx.orig_idx >= 0 && tx.orig_idx < (int)state.transactions.size())
                                TransactionDetailsDialog::show(state.transactions[tx.orig_idx]);
                        }
                        ImGui::PopStyleVar();
                        ImGui::PopStyleColor(2);

                        ImGui::PopTextWrapPos();
                        ImGui::EndGroup();

                        // Draw glass panel behind detail area
                        ImVec2 detailEnd = ImGui::GetCursorScreenPos();
                        float detailH = detailEnd.y - detailPos.y + Layout::spacingMd();
                        GlassPanelSpec detailGlass;
                        detailGlass.rounding = glassRound * 0.75f;
                        detailGlass.fillAlpha = 25;
                        DrawGlassPanel(dl, ImVec2(detailPos.x + Layout::spacingSm() + Layout::spacingXs(), detailPos.y),
                                       ImVec2(detailPos.x + innerW - Layout::spacingSm() - Layout::spacingXs(), detailPos.y + detailH), detailGlass);
                        ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
                    }

                    // Subtle divider between rows
                    if (fi < pageEnd - 1 && !is_expanded) {
                        ImVec2 divStart = ImGui::GetCursorScreenPos();
                        dl->AddLine(ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y),
                                    ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
                                    IM_COL32(255, 255, 255, 15));
                    }

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

        // CSS-style clipping mask
        {
            float fadeZone = std::min(
                (body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
                listH * 0.18f);
            ApplyScrollEdgeMask(dl, vtxMaskStart, childDL, childVtxStart,
                                listPanelMin.y, listPanelMax.y, fadeZone, scrollY, scrollMaxY);
        }

        // Status line with page info
        snprintf(buf, sizeof(buf), TR("showing_transactions"),
                 filtered_count > 0 ? pageStart + 1 : 0, pageEnd, filtered_count, state.transactions.size());
        Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
    }

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

} // namespace ui
} // namespace dragonx
