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

#include "mining_stats.h"
#include "mining_tab_helpers.h"  // FormatHashrate, EstimateHoursToBlock

#include "../../app.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/pool_registry.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../layout.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"

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

namespace dragonx {
namespace ui {

using namespace material;

// Chart/log view toggles (false = chart, true = live log). Moved here with the card they belong to.
static bool s_show_pool_log = false;
static bool s_show_solo_log = false;

// ---------------------------------------------------------------------------
// Sparkline chart of a hashrate history, drawn inside [regionMin, regionMax]
// (the plot area within a card, already inset by padding). Extracted so both
// the solo full-width card and the pool split card share one implementation.
// ---------------------------------------------------------------------------
static void DrawHashrateSparkline(ImDrawList* dl, ImVec2 regionMin, ImVec2 regionMax,
                                  const std::vector<double>& chartHistory,
                                  ImFont* capFont, float dp)
{
    if (chartHistory.size() < 2) return;

    const float chartBot = regionMax.y;
    double yMin = *std::min_element(chartHistory.begin(), chartHistory.end());
    double yMax = *std::max_element(chartHistory.begin(), chartHistory.end());
    if (yMax <= yMin) { yMax = yMin + 1.0; }
    double yRange = yMax - yMin;
    double yPad2 = yRange * 0.1;
    yMin -= yPad2;
    yMax += yPad2;

    const float plotLeft   = regionMin.x;
    const float plotRight  = regionMax.x;
    const float plotTop    = regionMin.y + capFont->LegacySize + 4 * dp;
    const float plotBottom = chartBot - capFont->LegacySize * 2 - 16 * dp;
    const float plotW = plotRight - plotLeft;
    const float plotH = std::max(1.0f, plotBottom - plotTop);

    size_t n = chartHistory.size();
    float stepW = (n > 1) ? plotW / (float)(n - 1) : plotW;

    std::vector<ImVec2> rawPts(n);
    for (size_t i = 0; i < n; i++) {
        float x = plotLeft + (float)i * stepW;
        float y = plotBottom - (float)((chartHistory[i] - yMin) / (yMax - yMin)) * plotH;
        rawPts[i] = ImVec2(x, y);
    }

    // Catmull-Rom spline interpolation for a smooth curve (adaptive subdivision).
    std::vector<ImVec2> points;
    if (n <= 2) {
        points = rawPts;
    } else {
        points.reserve(n * 4);
        for (size_t i = 0; i + 1 < n; i++) {
            ImVec2 p0 = rawPts[i > 0 ? i - 1 : 0];
            ImVec2 p1 = rawPts[i];
            ImVec2 p2 = rawPts[i + 1];
            ImVec2 p3 = rawPts[i + 2 < n ? i + 2 : n - 1];
            float dx = p2.x - p1.x, dy = p2.y - p1.y;
            float dist = sqrtf(dx * dx + dy * dy);
            int subdivs = std::clamp((int)(dist / 3.0f), 1, 16);
            for (int s = 0; s < subdivs; s++) {
                float t = (float)s / (float)subdivs;
                float t2 = t * t;
                float t3 = t2 * t;
                float q0 = -t3 + 2.0f * t2 - t;
                float q1 =  3.0f * t3 - 5.0f * t2 + 2.0f;
                float q2 = -3.0f * t3 + 4.0f * t2 + t;
                float q3 =  t3 - t2;
                float sx = 0.5f * (p0.x * q0 + p1.x * q1 + p2.x * q2 + p3.x * q3);
                float sy = 0.5f * (p0.y * q0 + p1.y * q1 + p2.y * q2 + p3.y * q3);
                sy = std::clamp(sy, plotTop, plotBottom);
                points.push_back(ImVec2(sx, sy));
            }
        }
        points.push_back(rawPts[n - 1]);
    }

    if (points.size() >= 2) {
        for (size_t i = 0; i < points.size(); i++)
            dl->PathLineTo(points[i]);
        dl->PathLineTo(ImVec2(points.back().x, plotBottom));
        dl->PathLineTo(ImVec2(points.front().x, plotBottom));
        dl->PathFillConcave(WithAlpha(Success(), 25));
    }

    dl->AddPolyline(points.data(), (int)points.size(), WithAlpha(Success(), 200),
                    ImDrawFlags_None,
                    schema::UI().drawElement("tabs.mining", "chart-line-thickness").size);

    // Y-axis labels
    std::string yMaxStr = FormatHashrate(yMax);
    std::string yMinStr = FormatHashrate(yMin);
    dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotTop - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), yMaxStr.c_str());
    dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotBottom + 4 * dp), OnSurfaceDisabled(), yMinStr.c_str());

    // X-axis labels
    dl->AddText(capFont, capFont->LegacySize,
                ImVec2(plotLeft, chartBot - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(),
                chartHistory.size() >= 300 ? TR("mining_chart_5m_ago") :
                chartHistory.size() >= 60  ? TR("mining_chart_1m_ago") : TR("mining_chart_start"));
    std::string nowLbl = TR("mining_chart_now");
    ImVec2 nowSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, nowLbl.c_str());
    dl->AddText(capFont, capFont->LegacySize,
                ImVec2(plotRight - nowSz.x, chartBot - capFont->LegacySize - 2 * dp),
                OnSurfaceDisabled(), nowLbl.c_str());
}

// ---------------------------------------------------------------------------
// Read-only, selectable/copyable log view filling [regionMin, size].
// ---------------------------------------------------------------------------
static void DrawMiningLogView(ImVec2 regionMin, float regionW, float regionH,
                              const std::vector<std::string>& logLines, const char* inputId)
{
    static std::string s_log_buf;
    s_log_buf.clear();
    for (const auto& line : logLines)
        if (!line.empty()) { s_log_buf += line; s_log_buf += '\n'; }

    ImGui::SetCursorScreenPos(regionMin);
    ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
    ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
    ImGui::PushFont(Type().body2());
    ImGui::InputTextMultiline(inputId, const_cast<char*>(s_log_buf.c_str()), s_log_buf.size() + 1,
                              ImVec2(regionW, regionH), ImGuiInputTextFlags_ReadOnly);
    ImGui::PopFont();
    ImGui::PopStyleColor(2);
}

// ---------------------------------------------------------------------------
// The left pool card (pool mode only): Manual/Auto toggle, a metric stack, and
// a fixed-height scrolling pool list. Pool rows are clickable to select in
// Manual mode (routed through the parent's dirty flag → save + restart) and
// show a "mining here" dot in Auto mode. The list scrolls, so any number of
// pools keeps the card — and everything below it — at a constant height.
// ---------------------------------------------------------------------------
static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* dl,
                               ImFont* capFont, ImFont* sub1, ImFont* ovFont,
                               float dp, float gap, float pad,
                               ImVec2 cardMin, ImVec2 cardMax,
                               char (&s_pool_url)[256], bool& s_pool_settings_dirty)
{
    const bool autoMode = app->settings()->getPoolSelectMode()
                          == config::Settings::PoolSelectMode::AutoBalance;

    const float x = cardMin.x + pad;
    const float w = (cardMax.x - cardMin.x) - pad * 2;
    float y = cardMin.y + pad * 0.75f;

    const float toggleH = sub1->LegacySize + 10 * dp;
    const float rowH    = sub1->LegacySize + 7 * dp;

    // --- Manual | Auto-balance toggle ---
    {
        ImFont* segFont = Type().caption();
        const char* segA = TR("mining_select_manual");
        const char* segB = TR("mining_select_auto");
        const float rnd = 4 * dp;
        const float half = w * 0.5f;
        const ImVec2 aMin(x, y), aMax(x + half, y + toggleH);
        const ImVec2 bMin(x + half, y), bMax(x + w, y + toggleH);
        dl->AddRectFilled(aMin, bMax, WithAlpha(OnSurface(), 15), rnd);
        dl->AddRect(aMin, bMax, WithAlpha(OnSurface(), 40), rnd);
        auto seg = [&](const ImVec2& mn, const ImVec2& mx, const char* label, bool selected) {
            if (selected) dl->AddRectFilled(mn, mx, WithAlpha(Primary(), 180), rnd);
            else if (material::IsRectHovered(mn, mx)) dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 20), rnd);
            ImVec2 sz = segFont->CalcTextSizeA(segFont->LegacySize, FLT_MAX, 0, label);
            dl->AddText(segFont, segFont->LegacySize,
                        ImVec2(mn.x + (mx.x - mn.x - sz.x) * 0.5f, mn.y + (mx.y - mn.y - sz.y) * 0.5f),
                        selected ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium(), label);
        };
        seg(aMin, aMax, segA, !autoMode);
        seg(bMin, bMax, segB, autoMode);
        ImGui::SetCursorScreenPos(aMin);
        ImGui::InvisibleButton("##CardSelManual", ImVec2(half, toggleH));
        if (ImGui::IsItemClicked() && autoMode) {
            app->settings()->setPoolSelectMode(config::Settings::PoolSelectMode::Manual);
            app->settings()->save();
        }
        if (material::IsRectHovered(aMin, aMax)) {
            ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
            material::Tooltip("%s", TR("mining_manual_desc"));
        }
        ImGui::SetCursorScreenPos(bMin);
        ImGui::InvisibleButton("##CardSelAuto", ImVec2(w - half, toggleH));
        if (ImGui::IsItemClicked() && !autoMode) {
            app->settings()->setPoolSelectMode(config::Settings::PoolSelectMode::AutoBalance);
            app->settings()->save();
            app->requestPoolBalanceRefresh();
        }
        if (material::IsRectHovered(bMin, bMax)) {
            ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
            material::Tooltip("%s", TR("mining_auto_balance_desc"));
        }
    }
    y += toggleH + gap;

    // --- Metric stack (label left, value right) ---
    auto metricRow = [&](const char* label, const std::string& val, ImU32 valCol) {
        dl->AddText(ovFont, ovFont->LegacySize,
                    ImVec2(x, y + (rowH - ovFont->LegacySize) * 0.5f), OnSurfaceMedium(), label);
        ImVec2 vSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, val.c_str());
        dl->AddText(sub1, sub1->LegacySize,
                    ImVec2(x + w - vSz.x, y + (rowH - sub1->LegacySize) * 0.5f), valCol, val.c_str());
        y += rowH;
    };

    const auto& pm = state.pool_mining;
    metricRow(TR("mining_local_hashrate"), FormatHashrate(pm.hashrate_10s),
              pm.xmrig_running ? Success() : OnSurfaceDisabled());
    metricRow(TR("mining_pool_hashrate"), FormatHashrate(pm.pool_hashrate),
              pm.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled());
    {
        char sharesBuf[64];
        snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld",
                 (long long)pm.accepted, (long long)pm.rejected);
        metricRow(TR("mining_shares"), sharesBuf, OnSurface());
    }
    {
        char uptBuf[64];
        int64_t up = pm.uptime_sec;
        if (up <= 0)          snprintf(uptBuf, sizeof(uptBuf), "N/A");
        else if (up < 3600)   snprintf(uptBuf, sizeof(uptBuf), "%lldm %llds", (long long)(up / 60), (long long)(up % 60));
        else                  snprintf(uptBuf, sizeof(uptBuf), "%lldh %lldm", (long long)(up / 3600), (long long)((up % 3600) / 60));
        metricRow(TR("mining_uptime"), uptBuf, OnSurface());
    }
    y += gap * 0.5f;

    // The pool list = official pools ∪ user-saved favorites ∪ the current custom pool.
    const auto effective = util::effectivePools(app->settings()->getPoolUrl(),
                                                app->settings()->getSavedPoolUrls());

    // --- POOLS (N) header + Refresh ---
    {
        char hdr[48];
        snprintf(hdr, sizeof(hdr), "%s (%d)", TR("mining_pools_header"),
                 (int)effective.size());
        dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr);

        float btnS = ovFont->LegacySize + 6 * dp;
        ImVec2 rbMin(x + w - btnS, y - 2 * dp);
        material::IconButtonStyle st;
        st.color = OnSurfaceMedium();
        st.hoverBg = StateHover();
        st.bgRounding = 3 * dp;
        st.tooltip = TR("mining_refresh");
        ImGui::SetCursorScreenPos(rbMin);
        if (material::IconButton("##PoolRefresh", ICON_MD_REFRESH, Type().iconSmall(), ImVec2(btnS, btnS), st))
            app->requestPoolBalanceRefresh();
    }
    y += ovFont->LegacySize + 4 * dp;

    // --- Scrollable pool list (fixed height → constant card height for any N) ---
    const float listH = std::max(rowH, cardMax.y - y - pad * 0.75f);
    ImGui::SetCursorScreenPos(ImVec2(x, y));
    ImGui::BeginChild("##PoolList", ImVec2(w, listH), false, ImGuiWindowFlags_NoBackground);
    {
        ImDrawList* cdl = ImGui::GetWindowDrawList();
        const auto snap = app->poolStatsSnapshot();
        const util::KnownPool* current = util::findPoolByUrl(effective, app->settings()->getPoolUrl());
        const float childW = ImGui::GetContentRegionAvail().x;
        const float listRowH = capFont->LegacySize + 10 * dp;

        for (const auto& kp : effective) {
            ImGui::PushID(kp.id.c_str());
            const bool isCurrent = current && current->id == kp.id;
            const auto it = snap.byId.find(kp.id);
            const bool haveHr = (it != snap.byId.end() && it->second.ok);

            ImVec2 rMin = ImGui::GetCursorScreenPos();
            ImVec2 rMax(rMin.x + childW, rMin.y + listRowH);

            bool hov = false, clk = false;
            if (!autoMode) {
                ImGui::InvisibleButton("##prow", ImVec2(childW, listRowH));
                hov = ImGui::IsItemHovered();
                clk = ImGui::IsItemClicked();
            } else {
                ImGui::Dummy(ImVec2(childW, listRowH));
            }

            if (isCurrent)   cdl->AddRectFilled(rMin, rMax, WithAlpha(Primary(), 30), 3 * dp);
            else if (hov)    cdl->AddRectFilled(rMin, rMax, StateHover(), 3 * dp);

            const float cy = rMin.y + listRowH * 0.5f;
            cdl->AddCircleFilled(ImVec2(rMin.x + 6 * dp, cy), 3.0f * dp,
                                 isCurrent ? Success() : OnSurfaceDisabled());

            const float textY = rMin.y + (listRowH - capFont->LegacySize) * 0.5f;

            // Build the right-side "<hashrate>  N% fee" run first so we know its width
            // and can bound (and ellipsis-truncate) the left host label to avoid a
            // collision — both text runs grow ~1.5x at font_scale 1.5 while the card
            // width barely does.
            char right[64];
            std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("—");
            // Prefer the live fee the pool reports; fall back to the compile-time
            // KnownPool.feePercent. A synthetic user pool has an unknown (<0) fee, so
            // we show just its hashrate placeholder for it.
            double feePct = (it != snap.byId.end() && it->second.feePercent >= 0.0)
                                ? it->second.feePercent
                                : kp.feePercent;
            if (feePct >= 0.0)
                snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
                         FormatFeePercent(feePct).c_str());
            else
                snprintf(right, sizeof(right), "%s", hrStr.c_str());
            ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right);

            // Show the short host label (the narrow card can't fit host:port); the
            // full stratum URL is in the hover tooltip. Truncate it to the gap left
            // of the right-side run so a long hostname can't overlap the hashrate.
            const float labelX = rMin.x + 16 * dp;
            const float labelMaxW = (rMax.x - rSz.x - 6 * dp - gap) - labelX;
            std::string label = TruncateToWidth(kp.label, capFont, capFont->LegacySize, labelMaxW);
            cdl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, textY),
                         isCurrent ? Primary() : OnSurface(), label.c_str());

            cdl->AddText(capFont, capFont->LegacySize,
                         ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right);

            if (!autoMode && hov) {
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                material::Tooltip("%s", kp.stratum.c_str());
            }
            if (clk) {
                strncpy(s_pool_url, kp.stratum.c_str(), sizeof(s_pool_url) - 1);
                s_pool_url[sizeof(s_pool_url) - 1] = '\0';
                s_pool_settings_dirty = true;
            }
            ImGui::PopID();
        }
    }
    ImGui::EndChild();
}

void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
                       ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
                       float dp, float vs, float gap, float pad, float availWidth,
                       const GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode,
                       App* app, char (&s_pool_url)[256], bool& s_pool_settings_dirty)
{
        (void)vs;
        const bool s_pool_mode = poolMode;
        ImU32 greenCol = Success();

        bool showLogView = s_pool_mode
            ? (s_show_pool_log && !state.pool_mining.log_lines.empty())
            : (s_show_solo_log && !mining.log_lines.empty());
        bool hasLogContent = s_pool_mode
            ? !state.pool_mining.log_lines.empty()
            : !mining.log_lines.empty();
        const std::vector<double>& chartHistory = s_pool_mode
            ? state.pool_mining.hashrate_history
            : mining.hashrate_history;
        bool hasChartContent = chartHistory.size() >= 2;

        float statRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm();
        bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log;

        // ================================================================
        // POOL MODE — split card: [ left pool card ][ chart / log ]
        // ================================================================
        if (s_pool_mode) {
            // Left card height driven by its content so the pool list always fits;
            // the whole row is at least as tall as the old stats+chart card.
            const float toggleH = sub1->LegacySize + 10 * dp;
            const float mRowH   = sub1->LegacySize + 7 * dp;
            const float listMinH = (capFont->LegacySize + 10 * dp) * 2.6f;  // ~2–3 rows before scroll
            float leftContentH = pad * 0.75f + toggleH + gap
                               + mRowH * 4.0f + gap * 0.5f
                               + (ovFont->LegacySize + 4 * dp) + listMinH + pad * 0.75f;
            float cardH = std::max(leftContentH, statRowH + chartBudgetH + pad);

            ImVec2 cardMin = ImGui::GetCursorScreenPos();
            // A bit wider than the old 25%/180dp so the pool rows fit the "<hashrate>  N% fee" text.
            // Ratio ceiling (0.45) never binds on a wide window, so give leftW an
            // absolute cap too — the pool card doesn't need to be enormous.
            float leftW  = std::clamp(availWidth * 0.30f, 210.0f * dp, 440.0f * dp);
            float colGap = gap;
            float rightW = availWidth - leftW - colGap;

            ImVec2 leftMin  = cardMin;
            ImVec2 leftMax(cardMin.x + leftW, cardMin.y + cardH);
            ImVec2 rightMin(leftMax.x + colGap, cardMin.y);
            ImVec2 rightMax(cardMin.x + availWidth, cardMin.y + cardH);

            DrawGlassPanel(dl, leftMin, leftMax, glassSpec);
            DrawGlassPanel(dl, rightMin, rightMax, glassSpec);

            RenderLeftPoolCard(app, state, dl, capFont, sub1, ovFont, dp, gap, pad,
                               leftMin, leftMax, s_pool_url, s_pool_settings_dirty);

            // rightW is uncapped, so the chart/hint band it draws would sprawl the
            // full panel on a wide window. Constrain just the drawn content to a
            // centered ~1000dp band inside the panel's padded content region; the
            // glass panel itself still fills rightW, leftover -> side margin.
            const float rightContentW = rightW - pad * 2.0f;
            // Fill the panel's content width so the chart extends across the whole panel on a wide window.
            // (Previously capped at ~1000dp and centered, which left large empty margins either side.)
            const float chartBandW    = rightContentW;
            const float chartBandX    = rightMin.x + pad;

            // Right panel: live log (if toggled + available) else the sparkline.
            if (showLogView) {
                float logPad = pad * 0.5f;
                DrawMiningLogView(ImVec2(rightMin.x + logPad, rightMin.y + logPad),
                                  rightW - logPad * 2, cardH - logPad * 2,
                                  state.pool_mining.log_lines, "##PoolLogText");
            } else if (hasChartContent) {
                DrawHashrateSparkline(dl,
                    ImVec2(chartBandX, rightMin.y + statRowH * 0.5f),
                    ImVec2(chartBandX + chartBandW, rightMax.y),
                    chartHistory, capFont, dp);
            } else {
                const char* hint = TR("mining_chart_start");
                ImVec2 hs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, hint);
                dl->AddText(capFont, capFont->LegacySize,
                            ImVec2(chartBandX + (chartBandW - hs.x) * 0.5f,
                                   (rightMin.y + rightMax.y - hs.y) * 0.5f),
                            OnSurfaceDisabled(), hint);
            }

            // Chart/log toggle button, top-right of the RIGHT panel.
            if (hasLogContent || hasChartContent) {
                ImFont* iconFont = Type().iconSmall();
                const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
                const char* toggleTip  = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log");
                ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
                float btnSize = iconSz.y + 8 * dp;
                ImVec2 btnMin(rightMax.x - pad - btnSize, rightMin.y + pad * 0.5f);
                ImVec2 btnMax(btnMin.x + btnSize, btnMin.y + btnSize);
                ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
                bool hov = IsRectHovered(btnMin, btnMax);
                if (hov) {
                    dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
                    ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                    material::Tooltip("%s", toggleTip);
                }
                dl->AddText(iconFont, iconFont->LegacySize,
                            ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
                            OnSurfaceMedium(), toggleIcon);
                if (hov && ImGui::IsMouseClicked(0)) showLogFlag = !showLogFlag;
            }

            ImGui::SetCursorScreenPos(cardMin);
            ImGui::Dummy(ImVec2(availWidth, cardH));
            ImGui::Dummy(ImVec2(0, gap));
            return;
        }

        // ================================================================
        // SOLO MODE — full-width card: stat columns on top, chart/log below
        // ================================================================
        float totalCardH = statRowH + chartBudgetH + pad;
        ImVec2 cardMin = ImGui::GetCursorScreenPos();
        ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH);
        DrawGlassPanel(dl, cardMin, cardMax, glassSpec);

        if (showLogView) {
            float logPad = pad * 0.5f;
            DrawMiningLogView(ImVec2(cardMin.x + logPad, cardMin.y + logPad),
                              availWidth - logPad * 2, totalCardH - logPad * 2,
                              mining.log_lines, "##SoloLogText");
            ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
            ImGui::Dummy(ImVec2(availWidth, 0));
        } else {
            double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty);

            const char* col1Label = TR("mining_local_hashrate");
            std::string col1Str   = FormatHashrate(mining.localHashrate);
            ImU32 col1Col         = mining.generate ? greenCol : OnSurfaceDisabled();
            const char* col2Label = TR("mining_network");
            std::string col2Str   = FormatHashrate(mining.networkHashrate);
            const char* col3Label = TR("mining_est_block");
            std::string col3Str   = FormatEstTime(est_hours);
            int numStats = 3;

            float statColW = (availWidth - pad * 2) / (float)numStats;
            float sy = cardMin.y + pad * 0.5f;
            struct StatEntry { const char* label; std::string value; ImU32 col; };
            StatEntry stats[] = {
                { col1Label, col1Str, col1Col },
                { col2Label, col2Str, OnSurface() },
                { col3Label, col3Str, OnSurface() },
            };
            for (int si = 0; si < numStats; si++) {
                float sx = cardMin.x + pad + si * statColW;
                float centerX = sx + statColW * 0.5f;
                ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, stats[si].label);
                dl->AddText(ovFont, ovFont->LegacySize,
                            ImVec2(centerX - lblSz.x * 0.5f, sy), OnSurfaceMedium(), stats[si].label);
                ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, stats[si].value.c_str());
                float valY = sy + ovFont->LegacySize + Layout::spacingXs();
                dl->AddText(sub1, sub1->LegacySize,
                            ImVec2(centerX - valSz.x * 0.5f, valY), stats[si].col, stats[si].value.c_str());

                if (si == 0 && chartHistory.size() >= 6) {
                    size_t hn = chartHistory.size();
                    double recent = (chartHistory[hn-1] + chartHistory[hn-2] + chartHistory[hn-3]) / 3.0;
                    double older  = (chartHistory[hn-4] + chartHistory[hn-5] + chartHistory[hn-6]) / 3.0;
                    ImFont* iconFont = Type().iconSmall();
                    float arrowX = centerX + valSz.x * 0.5f + Layout::spacingSm();
                    if (recent > older * 1.02) {
                        const char* icon = ICON_MD_TRENDING_UP;
                        ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
                        dl->AddText(iconFont, iconFont->LegacySize,
                                    ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f),
                                    WithAlpha(Success(), 220), icon);
                    } else if (recent < older * 0.98) {
                        const char* icon = ICON_MD_TRENDING_DOWN;
                        ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
                        dl->AddText(iconFont, iconFont->LegacySize,
                                    ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f),
                                    WithAlpha(Error(), 220), icon);
                    }
                }
            }

            if (hasChartContent) {
                DrawHashrateSparkline(dl,
                    ImVec2(cardMin.x + pad, cardMin.y + statRowH),
                    ImVec2(cardMax.x - pad, cardMax.y),
                    chartHistory, capFont, dp);
            }
            ImGui::Dummy(ImVec2(availWidth, totalCardH));
        }

        if (hasLogContent || hasChartContent) {
            ImFont* iconFont = Type().iconSmall();
            const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
            const char* toggleTip = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log");
            ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
            float btnSize = iconSz.y + 8 * dp;
            float btnX = cardMax.x - pad - btnSize;
            float btnY = cardMin.y + pad * 0.5f;
            ImVec2 btnMin(btnX, btnY);
            ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
            ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
            bool hov = IsRectHovered(btnMin, btnMax);
            if (hov) {
                dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                material::Tooltip("%s", toggleTip);
            }
            dl->AddText(iconFont, iconFont->LegacySize,
                        ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
                        OnSurfaceMedium(), toggleIcon);
            if (hov && ImGui::IsMouseClicked(0)) {
                showLogFlag = !showLogFlag;
            }
        }

        ImGui::Dummy(ImVec2(0, gap));
}

} // namespace ui
} // namespace dragonx
