// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// console_tab.cpp — Interactive RPC console with command history,
// tab completion, daemon log display, and color-coded output.

#include "console_tab.h"
#include "console_command_reference.h"
#include "console_input_model.h"
#include "console_output_model.h"
#include "console_tab_helpers.h"
#include "../material/colors.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../layout.h"
#include "../effects/imgui_acrylic.h"
#include "../material/color_theme.h"
#include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../../util/i18n.h"

#include <imgui.h>
#include <cstring>
#include <sstream>
#include <algorithm>
#include <cmath>
#include <mutex>
#include <ctime>
#include <unordered_set>

namespace dragonx {
namespace ui {

// Static color definitions — defaults; overridden from ui.toml in constructor
ImU32 ConsoleTab::COLOR_COMMAND = IM_COL32(191, 209, 229, 255);
ImU32 ConsoleTab::COLOR_RESULT  = IM_COL32(200, 200, 200, 255);
ImU32 ConsoleTab::COLOR_ERROR   = IM_COL32(246,  71,  64, 255);
ImU32 ConsoleTab::COLOR_DAEMON  = IM_COL32(160, 160, 160, 180);
ImU32 ConsoleTab::COLOR_INFO    = IM_COL32(191, 209, 229, 255);
ImU32 ConsoleTab::COLOR_RPC     = IM_COL32(120, 180, 255, 210);
bool  ConsoleTab::s_scanline_enabled = true;
float ConsoleTab::s_console_zoom = 1.0f;
bool  ConsoleTab::s_daemon_messages_enabled = true;
bool  ConsoleTab::s_errors_only_enabled = false;
bool  ConsoleTab::s_rpc_trace_enabled = false;

namespace {

std::mutex s_rpc_trace_console_mutex;
ConsoleTab* s_rpc_trace_console = nullptr;

std::string rpcTraceTimestamp()
{
    std::time_t now = std::time(nullptr);
    std::tm localTime{};
    static std::mutex timeMutex;
    {
        std::lock_guard<std::mutex> lock(timeMutex);
        if (const std::tm* current = std::localtime(&now)) {
            localTime = *current;
        }
    }

    char buffer[16];
    std::strftime(buffer, sizeof(buffer), "%H:%M:%S", &localTime);
    return buffer;
}

} // namespace

void ConsoleTab::refreshColors()
{
    auto& S = schema::UI();
    bool dark = material::IsDarkTheme();

    // Try schema overrides first, then use sensible per-theme defaults
    if (S.isLoaded()) {
        auto cmd = S.drawElement("console", "color-command");
        auto res = S.drawElement("console", "color-result");
        auto err = S.drawElement("console", "color-error");
        auto dmn = S.drawElement("console", "color-daemon");
        auto inf = S.drawElement("console", "color-info");
        auto rpc = S.drawElement("console", "color-rpc");

        ImU32 defCmd = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
        ImU32 defRes = dark ? IM_COL32(200, 200, 200, 255) : IM_COL32(50, 50, 50, 255);
        ImU32 defErr = dark ? IM_COL32(246,  71,  64, 255) : IM_COL32(198, 40, 40, 255);
        ImU32 defDmn = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200);
        ImU32 defInf = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
        ImU32 defRpc = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);

        COLOR_COMMAND = !cmd.color.empty() ? S.resolveColor(cmd.color, defCmd) : defCmd;
        COLOR_RESULT  = !res.color.empty() ? S.resolveColor(res.color, defRes) : defRes;
        COLOR_ERROR   = !err.color.empty() ? S.resolveColor(err.color, defErr) : defErr;
        COLOR_DAEMON  = !dmn.color.empty() ? S.resolveColor(dmn.color, defDmn) : defDmn;
        COLOR_INFO    = !inf.color.empty() ? S.resolveColor(inf.color, defInf) : defInf;
        COLOR_RPC     = !rpc.color.empty() ? S.resolveColor(rpc.color, defRpc) : defRpc;
    } else {
        // No schema — use hardcoded defaults per theme
        COLOR_COMMAND = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
        COLOR_RESULT  = dark ? IM_COL32(200, 200, 200, 255) : IM_COL32(50, 50, 50, 255);
        COLOR_ERROR   = dark ? IM_COL32(246,  71,  64, 255) : IM_COL32(198, 40, 40, 255);
        COLOR_DAEMON  = dark ? IM_COL32(160, 160, 160, 180) : IM_COL32(90, 90, 90, 200);
        COLOR_INFO    = dark ? IM_COL32(191, 209, 229, 255) : IM_COL32(21, 101, 192, 255);
        COLOR_RPC     = dark ? IM_COL32(120, 180, 255, 210) : IM_COL32(25, 118, 210, 220);
    }
}

ConsoleTab::ConsoleTab()
{
    {
        std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
        s_rpc_trace_console = this;
    }
    rpc::RPCClient::setTraceCallback([](const std::string& source, const std::string& method) {
        ConsoleTab* console = nullptr;
        {
            std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
            console = s_rpc_trace_console;
        }
        if (console) console->addRpcTraceLine(source, method);
    });
    rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);

    // Load console colors from ui.toml schema (uses current theme)
    refreshColors();

    // Add welcome message
    addLine(TR("console_welcome"), COLOR_INFO);
    addLine(TR("console_type_help"), COLOR_INFO);
    addLine("", COLOR_RESULT);
}

ConsoleTab::~ConsoleTab()
{
    rpc::RPCClient::setTraceEnabled(false);
    rpc::RPCClient::setTraceCallback(nullptr);
    std::lock_guard<std::mutex> lock(s_rpc_trace_console_mutex);
    if (s_rpc_trace_console == this) s_rpc_trace_console = nullptr;
}

void ConsoleTab::render(daemon::EmbeddedDaemon* daemon, rpc::RPCClient* rpc, rpc::RPCWorker* worker, daemon::XmrigManager* xmrig)
{
    using namespace material;

    // Refresh console colors when dark/light theme changes
    {
        static bool s_lastDark = IsDarkTheme();
        bool nowDark = IsDarkTheme();
        if (nowDark != s_lastDark) {
            // Save old colors to remap existing lines
            ImU32 oldCmd = COLOR_COMMAND, oldRes = COLOR_RESULT;
            ImU32 oldErr = COLOR_ERROR,   oldDmn = COLOR_DAEMON;
            ImU32 oldInf = COLOR_INFO,    oldRpc = COLOR_RPC;
            refreshColors();
            // Remap stored line colors from old to new
            {
                std::lock_guard<std::mutex> lock(lines_mutex_);
                for (auto& line : lines_) {
                    if      (line.color == oldCmd) line.color = COLOR_COMMAND;
                    else if (line.color == oldRes) line.color = COLOR_RESULT;
                    else if (line.color == oldErr) line.color = COLOR_ERROR;
                    else if (line.color == oldDmn) line.color = COLOR_DAEMON;
                    else if (line.color == oldInf) line.color = COLOR_INFO;
                    else if (line.color == oldRpc) line.color = COLOR_RPC;
                }
            }
            s_lastDark = nowDark;
        }
    }

    // Check for daemon state changes
    if (daemon) {
        auto current_state = daemon->getState();
        
        // Show message when daemon starts
        if (current_state == daemon::EmbeddedDaemon::State::Starting && 
            last_daemon_state_ == daemon::EmbeddedDaemon::State::Stopped) {
            addLine("", COLOR_RESULT);
            addLine(TR("console_starting_node"), COLOR_INFO);
            addLine(TR("console_capturing_output"), COLOR_INFO);
            addLine("", COLOR_RESULT);
            shown_startup_message_ = true;
        }
        else if (current_state == daemon::EmbeddedDaemon::State::Running && 
                 last_daemon_state_ != daemon::EmbeddedDaemon::State::Running) {
            addLine(TR("console_daemon_started"), COLOR_INFO);
        }
        else if (current_state == daemon::EmbeddedDaemon::State::Stopped && 
                 last_daemon_state_ == daemon::EmbeddedDaemon::State::Running) {
            addLine("", COLOR_RESULT);
            addLine(TR("console_daemon_stopped"), COLOR_INFO);
        }
        else if (current_state == daemon::EmbeddedDaemon::State::Error) {
            addLine(std::string(TR("console_daemon_error")) + daemon->getLastError() + " ===", COLOR_ERROR);
        }
        
        last_daemon_state_ = current_state;
    }

    // Track RPC connection state and show a message when connected
    if (rpc) {
        bool connected_now = rpc->isConnected();
        if (connected_now && !last_rpc_connected_) {
            addLine(TR("console_connected"), COLOR_INFO);
        } else if (!connected_now && last_rpc_connected_) {
            addLine(TR("console_disconnected"), COLOR_ERROR);
        }
        last_rpc_connected_ = connected_now;
    }

    // Check for new daemon output — always capture so toggle works as a live filter
    if (daemon) {
        std::string new_output = daemon->getOutputSince(last_daemon_output_size_);
        if (!new_output.empty()) {
            // Split by newlines and add each line
            std::istringstream stream(new_output);
            std::string line;
            while (std::getline(stream, line)) {
                if (!line.empty()) {
                    // Color based on content: [ERROR] -> red, [WARN] -> warning color
                    ImU32 lineColor = COLOR_DAEMON;
                    if (line.find("[ERROR]") != std::string::npos || 
                        line.find("error:") != std::string::npos ||
                        line.find("Error:") != std::string::npos) {
                        lineColor = COLOR_ERROR;
                    }
                    addLine("[daemon] " + line, lineColor);
                }
            }
        }
    }
    
    // Check for new xmrig output (pool mining)
    if (xmrig && xmrig->isRunning()) {
        std::string new_output = xmrig->getOutputSince(last_xmrig_output_size_);
        if (!new_output.empty()) {
            std::istringstream stream(new_output);
            std::string line;
            while (std::getline(stream, line)) {
                if (!line.empty()) {
                    // Color xmrig output - errors in red, accepted shares in green
                    ImU32 lineColor = COLOR_DAEMON;
                    if (line.find("error") != std::string::npos || 
                        line.find("ERROR") != std::string::npos ||
                        line.find("failed") != std::string::npos) {
                        lineColor = COLOR_ERROR;
                    } else if (line.find("accepted") != std::string::npos) {
                        lineColor = COLOR_INFO;
                    }
                    addLine("[xmrig] " + line, lineColor);
                }
            }
        }
    } else if (!xmrig || !xmrig->isRunning()) {
        // Reset offset when xmrig stops so we get fresh output next time
        if (last_xmrig_output_size_ != 0) {
            last_xmrig_output_size_ = 0;
        }
    }
    
    // Main console layout
    ImGui::BeginChild("ConsoleContainer", ImVec2(0, 0), false,
                      ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
    
    // Toolbar
    renderToolbar(daemon);
    
    ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
    
    // Output area (scrollable) — glass panel background
    float availHeight = ImGui::GetContentRegionAvail().y;
    float input_height = ComputeConsoleInputHeight(
        ImGui::GetFrameHeightWithSpacing(),
        ImGui::GetStyle().ItemSpacing.y,
        Layout::spacingSm(),
        Layout::spacingXs(),
        schema::UI().drawElement("tabs.console", "input-cursor-offset").size);
    float outputH = ComputeConsoleOutputHeight(
        availHeight,
        input_height,
        schema::UI().drawElement("tabs.console", "output-min-height").size,
        schema::UI().drawElement("tabs.console", "output-min-height-ratio").size);
    
    ImDrawList* dlOut = ImGui::GetWindowDrawList();
    ImVec2 outPanelMin = ImGui::GetCursorScreenPos();
    ImVec2 outPanelMax(outPanelMin.x + ImGui::GetContentRegionAvail().x, outPanelMin.y + outputH);
    GlassPanelSpec outGlass;
    outGlass.rounding = Layout::glassRounding();
    outGlass.fillAlpha = 12;
    DrawGlassPanel(dlOut, outPanelMin, outPanelMax, outGlass);
    
    int consoleParentVtx = dlOut->VtxBuffer.Size;

    ImGui::BeginChild("ConsoleOutput", ImVec2(0, outputH), false, 
                      ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
    ApplySmoothScroll();
    ImDrawList* consoleChildDL = ImGui::GetWindowDrawList();
    int consoleChildVtx = consoleChildDL->VtxBuffer.Size;
    float consoleScrollY    = ImGui::GetScrollY();
    float consoleScrollMaxY = ImGui::GetScrollMaxY();
    // Use smaller font for console output
    ImGui::PushFont(Type().caption());
    ImGui::SetWindowFontScale(s_console_zoom);
    renderOutput();
    ImGui::SetWindowFontScale(1.0f);
    ImGui::PopFont();
    ImGui::EndChild();

    // Auto-toggle auto-scroll based on scroll position:
    // At the bottom → re-enable; scrolled up → already disabled by wheel handler.
    // After wheel-up, wait for the cooldown so smooth-scroll can animate
    // away from the bottom before we check position again.
    if (scroll_up_cooldown_ > 0.0f)
        scroll_up_cooldown_ -= ImGui::GetIO().DeltaTime;
    if (!auto_scroll_ && scroll_up_cooldown_ <= 0.0f && consoleScrollMaxY > 0.0f) {
        float tolerance = Type().caption()->LegacySize * 1.5f;
        if (consoleScrollY >= consoleScrollMaxY - tolerance) {
            auto_scroll_ = true;
            new_lines_since_scroll_ = 0;
        }
    }

    // CSS-style clipping mask
    // When auto-scroll is off, force bottom fade to always show by
    // inflating scrollMax so the mask thinks there's content below.
    {
        float fadeZone = std::min(Type().caption()->LegacySize * 3.0f, outputH * 0.18f);
        float effectiveScrollMax = auto_scroll_ ? consoleScrollMaxY
                                                : std::max(consoleScrollMaxY, consoleScrollY + 10.0f);
        ApplyScrollEdgeMask(dlOut, consoleParentVtx, consoleChildDL, consoleChildVtx,
                            outPanelMin.y, outPanelMax.y, fadeZone, consoleScrollY, effectiveScrollMax);
    }

    // CRT scanline effect over output area — aligned to text lines
    if (s_scanline_enabled) {
        float textLineH = output_line_height_;
        if (textLineH <= 1.0f) textLineH = Type().caption()->LegacySize * s_console_zoom + 2.0f;

        int lightAlpha = std::clamp((int)schema::UI().drawElement("tabs.console", "scanline-line-alpha").sizeOr(10.0f), 0, 255);
        int darkAlpha = std::clamp(lightAlpha + 10, 0, 255);
        if (textLineH >= 1.0f && (lightAlpha > 0 || darkAlpha > 0)) {
            ImU32 lightCol = IM_COL32(255, 255, 255, lightAlpha);
            ImU32 darkCol = IM_COL32(0, 0, 0, darkAlpha);
            for (const auto& row : scanline_rows_) {
                float yTop = std::max(row.yTop, outPanelMin.y);
                float yBot = std::min(row.yBot, outPanelMax.y);
                if (yTop < yBot) {
                    dlOut->AddRectFilled(ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, yBot),
                                         (row.rowIndex % 2 == 0) ? lightCol : darkCol);
                }
            }
        }

        float panelH = outPanelMax.y - outPanelMin.y;
        float scanSpeed = schema::UI().drawElement("tabs.console", "scanline-speed").sizeOr(40.0f);
        float rawScanH = schema::UI().drawElement("tabs.console", "scanline-height").sizeOr(textLineH * 2.0f);
        int scanAlpha = std::clamp((int)schema::UI().drawElement("tabs.console", "scanline-alpha").sizeOr(8.0f), 0, 255);
        if (panelH > 1.0f && textLineH >= 1.0f && scanSpeed > 0.0f && scanAlpha > 0) {
            float scanLines = std::max(1.0f, std::round(rawScanH / textLineH));
            float scanH = scanLines * textLineH;
            float t = (float)std::fmod(ImGui::GetTime() * scanSpeed, (double)(panelH + scanH));
            float scanY = outPanelMin.y + t - scanH;
            float yTop = std::max(scanY, outPanelMin.y);
            float yBot = std::min(scanY + scanH, outPanelMax.y);
            if (yTop < yBot) {
                float mid = (yTop + yBot) * 0.5f;
                ImU32 clear = IM_COL32(255, 255, 255, 0);
                ImU32 peak = IM_COL32(255, 255, 255, scanAlpha);
                dlOut->AddRectFilledMultiColor(
                    ImVec2(outPanelMin.x, yTop), ImVec2(outPanelMax.x, mid),
                    clear, clear, peak, peak);
                dlOut->AddRectFilledMultiColor(
                    ImVec2(outPanelMin.x, mid), ImVec2(outPanelMax.x, yBot),
                    peak, peak, clear, clear);
            }
        }
    }
    
    ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
    
    // Input area
    renderInput(rpc, worker);
    
    ImGui::EndChild();
}

void ConsoleTab::renderCommandsPopupModal()
{
    if (!show_commands_popup_) {
        return;
    }
    renderCommandsPopup();
}

void ConsoleTab::renderToolbar(daemon::EmbeddedDaemon* daemon)
{
    using namespace material;
    ImDrawList* dl = ImGui::GetWindowDrawList();

    // Glass panel for toolbar
    float toolbarH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingMd();
    ImVec2 tbMin = ImGui::GetCursorScreenPos();
    ImVec2 tbMax(tbMin.x + ImGui::GetContentRegionAvail().x, tbMin.y + toolbarH);
    GlassPanelSpec tbGlass;
    tbGlass.rounding = Layout::glassRounding();
    tbGlass.fillAlpha = 12;
    DrawGlassPanel(dl, tbMin, tbMax, tbGlass);

    ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (toolbarH - ImGui::GetFrameHeight()) * 0.5f);
    ImGui::SetCursorPosX(ImGui::GetCursorPosX() + Layout::spacingMd());

    // Daemon status with colored dot
    if (daemon) {
        auto state = daemon->getState();
        const char* status_text = TR("console_status_unknown");
        ImU32 dotCol = IM_COL32(150, 150, 150, 255);
        bool pulse = false;

        switch (state) {
            case daemon::EmbeddedDaemon::State::Stopped:
                status_text = TR("console_status_stopped");
                dotCol = IM_COL32(150, 150, 150, 255);
                break;
            case daemon::EmbeddedDaemon::State::Starting:
                status_text = TR("console_status_starting");
                dotCol = Warning();
                pulse = true;
                break;
            case daemon::EmbeddedDaemon::State::Running:
                status_text = TR("console_status_running");
                dotCol = Success();
                break;
            case daemon::EmbeddedDaemon::State::Stopping:
                status_text = TR("console_status_stopping");
                dotCol = Warning();
                pulse = true;
                break;
            case daemon::EmbeddedDaemon::State::Error:
                status_text = TR("console_status_error");
                dotCol = Error();
                break;
        }

        ImVec2 cp = ImGui::GetCursorScreenPos();
        float dotR = schema::UI().drawElement("tabs.console", "status-dot-radius-base").size + schema::UI().drawElement("tabs.console", "status-dot-radius-scale").size * Layout::hScale();
        float dotY = cp.y + ImGui::GetTextLineHeight() * 0.5f;
        float dotX = cp.x + dotR + 2;

        if (pulse) {
            float a = schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-fast").size);
            ImU32 pCol = (dotCol & 0x00FFFFFF) | ((ImU32)(255 * a) << 24);
            dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, pCol);
        } else {
            dl->AddCircleFilled(ImVec2(dotX, dotY), dotR, dotCol);
        }

        ImGui::Dummy(ImVec2(dotR * 2 + 6, 0));
        ImGui::SameLine();
        Type().textColored(TypeStyle::Caption, dotCol, status_text);
    } else {
        Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("console_no_daemon"));
    }
    
    ImGui::SameLine();
    ImGui::Spacing();
    ImGui::SameLine();
    
    // Auto-scroll toggle
    if (ImGui::Checkbox(TR("console_auto_scroll"), &auto_scroll_)) {
        if (auto_scroll_) {
            scroll_to_bottom_ = true;
            new_lines_since_scroll_ = 0;
        }
    }
    
    ImGui::SameLine();
    ImGui::Spacing();
    ImGui::SameLine();

    // Daemon messages toggle
    {
        static bool s_prev_daemon_enabled = true;
        ImGui::Checkbox(TR("console_daemon"), &s_daemon_messages_enabled);
        // When toggling daemon filter while auto-scroll is active, scroll to bottom
        if (s_prev_daemon_enabled != s_daemon_messages_enabled && auto_scroll_) {
            scroll_to_bottom_ = true;
        }
        s_prev_daemon_enabled = s_daemon_messages_enabled;
    }
    if (ImGui::IsItemHovered()) {
        ImGui::SetTooltip("%s", TR("console_show_daemon_output"));
    }
    
    ImGui::SameLine();
    ImGui::Spacing();
    ImGui::SameLine();
    
    // Errors-only toggle
    {
        static bool s_prev_errors_only = false;
        ImGui::Checkbox(TR("console_errors"), &s_errors_only_enabled);
        // When toggling errors filter while auto-scroll is active, scroll to bottom
        if (s_prev_errors_only != s_errors_only_enabled && auto_scroll_) {
            scroll_to_bottom_ = true;
        }
        s_prev_errors_only = s_errors_only_enabled;
    }
    if (ImGui::IsItemHovered()) {
        ImGui::SetTooltip("%s", TR("console_show_errors_only"));
    }

    ImGui::SameLine();
    ImGui::Spacing();
    ImGui::SameLine();

    // App RPC trace toggle — captures method/source only, never results or params
    {
        static bool s_prev_rpc_trace_enabled = false;
        if (ImGui::Checkbox(TR("console_rpc_trace"), &s_rpc_trace_enabled)) {
            rpc::RPCClient::setTraceEnabled(s_rpc_trace_enabled);
        }
        if (s_prev_rpc_trace_enabled != s_rpc_trace_enabled && auto_scroll_) {
            scroll_to_bottom_ = true;
        }
        s_prev_rpc_trace_enabled = s_rpc_trace_enabled;
    }
    if (ImGui::IsItemHovered()) {
        ImGui::SetTooltip("%s", TR("console_show_rpc_trace"));
    }
    
    ImGui::SameLine();
    ImGui::Spacing();
    ImGui::SameLine();
    
    // Clear button
    if (TactileButton(TR("console_clear"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
        clear();
        clearSelection();
    }
    
    ImGui::SameLine();
    
    // Copy button — material styled
    if (TactileButton(TR("copy"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
        std::lock_guard<std::mutex> lock(lines_mutex_);
        if (has_selection_) {
            std::string selected = getSelectedText();
            if (!selected.empty()) {
                ImGui::SetClipboardText(selected.c_str());
            }
        } else {
            // Copy all output if nothing selected
            std::string all;
            for (const auto& line : lines_) {
                all += line.text + "\n";
            }
            if (!all.empty()) {
                ImGui::SetClipboardText(all.c_str());
            }
        }
    }
    if (ImGui::IsItemHovered()) {
        ImGui::SetTooltip("%s", has_selection_ ? TR("console_copy_selected") : TR("console_copy_all"));
    }
    
    ImGui::SameLine();
    
    // Commands reference button
    if (TactileButton(TR("console_commands"), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
        show_commands_popup_ = true;
    }
    if (ImGui::IsItemHovered()) {
        ImGui::SetTooltip("%s", TR("console_show_rpc_ref"));
    }
    
    ImGui::SameLine();
    
    // Line count
    {
        std::lock_guard<std::mutex> lock(lines_mutex_);
        ImGui::TextDisabled(TR("console_line_count"), lines_.size());
    }

    ImGui::SameLine();
    ImGui::Spacing();
    ImGui::SameLine();

    // Output filter input
    float zoomBtnSpace = ImGui::GetFrameHeight() * 2.0f + Layout::spacingSm() * 3.0f;
    float filterAvail  = ImGui::GetContentRegionAvail().x - zoomBtnSpace;
    float filterW = std::min(schema::UI().drawElement("tabs.console", "filter-max-width").size, filterAvail * schema::UI().drawElement("tabs.console", "filter-width-ratio").size);
    ImGui::SetNextItemWidth(filterW);
    ImGui::InputTextWithHint("##ConsoleFilter", TR("console_filter_hint"), filter_text_, sizeof(filter_text_));

    // Zoom +/- buttons (right side of toolbar)
    ImGui::SameLine();
    ImGui::Spacing();
    ImGui::SameLine();
    {
        auto& S = schema::UI();
        float zoomStep = S.drawElement("tabs.console", "zoom-step").sizeOr(0.1f);
        float zoomMin  = S.drawElement("tabs.console", "zoom-min").sizeOr(0.5f);
        float zoomMax  = S.drawElement("tabs.console", "zoom-max").sizeOr(3.0f);
        float btnSz    = ImGui::GetFrameHeight();

        if (TactileButton(ICON_MD_REMOVE, ImVec2(btnSz, btnSz), Type().iconMed())) {
            s_console_zoom = std::max(zoomMin, s_console_zoom - zoomStep);
        }
        if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip(TR("console_zoom_out"), s_console_zoom * 100.0f);
        }
        ImGui::SameLine();
        if (TactileButton(ICON_MD_ADD, ImVec2(btnSz, btnSz), Type().iconMed())) {
            s_console_zoom = std::min(zoomMax, s_console_zoom + zoomStep);
        }
        if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip(TR("console_zoom_in"), s_console_zoom * 100.0f);
        }
    }
}

void ConsoleTab::renderOutput()
{
    using namespace material;
    auto& S = schema::UI();
    std::lock_guard<std::mutex> lock(lines_mutex_);
    
    // Zero item spacing so Dummy items advance the cursor by exactly their
    // height.  The inter-line gap is added explicitly to wrapped_heights_
    // so that cumulative_y_offsets_ stays perfectly in sync with actual
    // cursor positions (avoiding selection-offset drift).
    float interLineGap = S.drawElement("tabs.console", "output").getFloat("line-spacing", 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
    
    // Inner padding for glass panel
    float padX = Layout::spacingMd();
    float padY = Layout::spacingSm();
    ImGui::SetCursorPosY(ImGui::GetCursorPosY() + padY);
    ImGui::Indent(padX);
    
    float line_height = ImGui::GetTextLineHeight();
    output_line_height_ = line_height;  // store for scanline alignment
    output_origin_ = ImGui::GetCursorScreenPos();
    output_scroll_y_ = ImGui::GetScrollY();
    scanline_rows_.clear();
    
    // Build filtered line index list BEFORE mouse handling (so screenToTextPos works)
    ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
                                     s_errors_only_enabled, s_rpc_trace_enabled,
                                     COLOR_DAEMON, COLOR_ERROR, COLOR_RPC};
    bool has_text_filter = !outputFilter.text.empty();
    bool has_filter = has_text_filter || !outputFilter.daemonMessagesEnabled ||
                      !outputFilter.rpcTraceEnabled || outputFilter.errorsOnly;
    visible_indices_.clear();
    if (has_filter) {
        for (int i = 0; i < static_cast<int>(lines_.size()); i++) {
            if (!consoleLinePassesFilter(lines_[i].text, lines_[i].color, outputFilter)) continue;
            visible_indices_.push_back(i);
        }
    } else {
        // No filter - all lines are visible
        for (int i = 0; i < static_cast<int>(lines_.size()); i++) {
            visible_indices_.push_back(i);
        }
    }
    int visible_count = static_cast<int>(visible_indices_.size());
    
    // Calculate wrapped heights AND build sub-row segments for each visible line.
    // Each segment records which bytes of the source text appear on that visual
    // row, so hit-testing and selection highlight can map screen positions to
    // exact character offsets.
    float wrap_width = ClampConsoleWrapWidth(ImGui::GetContentRegionAvail().x, padX);
    
    ImFont* font = ImGui::GetFont();
    float fontSize = ImGui::GetFontSize();
    
    wrapped_heights_.resize(visible_count);
    cumulative_y_offsets_.resize(visible_count);
    visible_wrap_segments_.resize(visible_count);
    total_wrapped_height_ = 0.0f;
    cached_wrap_width_ = wrap_width;
    
    for (int vi = 0; vi < visible_count; vi++) {
        int i = visible_indices_[vi];
        const std::string& text = lines_[i].text;
        auto& segs = visible_wrap_segments_[vi];
        segs.clear();
        
        if (text.empty()) {
            segs.push_back({0, 0, 0.0f, line_height});
            cumulative_y_offsets_[vi] = total_wrapped_height_;
            wrapped_heights_[vi] = line_height + interLineGap;
            total_wrapped_height_ += wrapped_heights_[vi];
            continue;
        }
        
        // Walk the text using ImFont::CalcWordWrapPositionA to find
        // exactly where ImGui would break each visual row.
        const char* textStart = text.c_str();
        const char* textEnd   = textStart + text.size();
        const char* cur       = textStart;
        float segY = 0.0f;
        
        while (cur < textEnd) {
            const char* wrapPos = font->CalcWordWrapPositionA(
                fontSize / font->LegacySize, cur, textEnd, wrap_width);
            // Ensure forward progress (at least one character)
            if (wrapPos <= cur) wrapPos = cur + 1;
            // Skip a leading newline character that ends the previous segment
            if (*cur == '\n') { cur++; continue; }
            
            int byteStart = static_cast<int>(cur - textStart);
            int byteEnd   = static_cast<int>(wrapPos - textStart);
            // Trim trailing newline from this segment
            if (byteEnd > byteStart && text[byteEnd - 1] == '\n') byteEnd--;
            
            segs.push_back({byteStart, byteEnd, segY, line_height});
            segY += line_height;
            cur = wrapPos;
        }
        
        if (segs.empty()) {
            segs.push_back({0, 0, 0.0f, line_height});
            segY = line_height;
        }
        
        cumulative_y_offsets_[vi] = total_wrapped_height_;
        wrapped_heights_[vi] = segY + interLineGap;
        total_wrapped_height_ += wrapped_heights_[vi];
    }
    
    // Use raw IO for mouse handling to bypass child window event consumption
    ImGuiIO& io = ImGui::GetIO();
    ImVec2 mouse_pos = io.MousePos;
    
    // Manual hit test: is mouse within this child window?
    ImVec2 win_min = ImGui::GetWindowPos();
    ImVec2 win_max = ImVec2(win_min.x + ImGui::GetWindowWidth(), 
                             win_min.y + ImGui::GetWindowHeight());
    bool mouse_in_output = (mouse_pos.x >= win_min.x && mouse_pos.x < win_max.x &&
                            mouse_pos.y >= win_min.y && mouse_pos.y < win_max.y &&
                            !ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopup));
    
    // Disable auto-scroll when user scrolls up (wheel scroll)
    if (mouse_in_output && io.MouseWheel > 0.0f) {
        auto_scroll_ = false;
        scroll_up_cooldown_ = 0.5f; // give smooth-scroll time to animate away
    }
    // Scrolling down to the very bottom re-enables auto-scroll.
    // Actual position check happens after EndChild() using captured
    // scroll values, but is skipped on the frame where wheel-up
    // was detected (scroll position hasn't caught up yet).
    
    // Set cursor to text selection when hovering
    if (mouse_in_output) {
        ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
    }
    
    // Mouse press - start selection (use raw IO mouse state)
    if (mouse_in_output && io.MouseClicked[0]) {
        sel_anchor_ = screenToTextPos(mouse_pos, line_height);
        sel_end_ = sel_anchor_;
        is_selecting_ = true;
        has_selection_ = false;
    }
    
    // Mouse drag - extend selection (continue even if mouse leaves the window)
    if (is_selecting_ && io.MouseDown[0]) {
        TextPos new_end = screenToTextPos(mouse_pos, line_height);
        sel_end_ = new_end;
        // Consider it a real selection once the position changes
        if (sel_end_.line != sel_anchor_.line || sel_end_.col != sel_anchor_.col) {
            has_selection_ = true;
        }
    }
    
    // Mouse release - finalize selection
    if (is_selecting_ && io.MouseReleased[0]) {
        sel_end_ = screenToTextPos(mouse_pos, line_height);
        is_selecting_ = false;
    }
    
    // Ctrl+C / Ctrl+A
    if (mouse_in_output || has_selection_) {
        if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C)) {
            std::string selected = getSelectedText();
            if (!selected.empty()) {
                ImGui::SetClipboardText(selected.c_str());
            }
        }
        if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_A)) {
            // Select all
            if (!lines_.empty()) {
                sel_anchor_ = {0, 0};
                sel_end_ = {static_cast<int>(lines_.size()) - 1, 
                           static_cast<int>(lines_.back().text.size())};
                has_selection_ = true;
            }
        }
    }
    
    // Get selection bounds (ordered)
    TextPos sel_start_pos = selectionStart();
    TextPos sel_end_pos = selectionEnd();
    
    // Render lines with selection highlighting.
    // Each line is split into pre-computed wrap segments rendered individually
    // via AddText so that hit-testing and highlights map 1:1 to visual positions.
    float scroll_y = ImGui::GetScrollY();
    float window_height = ImGui::GetWindowHeight();
    float visible_top = scroll_y;
    float visible_bottom = scroll_y + window_height;
    
    // Find first visible line using binary search
    int first_visible = 0;
    if (!cumulative_y_offsets_.empty()) {
        int lo = 0, hi = static_cast<int>(cumulative_y_offsets_.size()) - 1;
        while (lo < hi) {
            int mid = (lo + hi) / 2;
            float line_bottom = cumulative_y_offsets_[mid] + wrapped_heights_[mid];
            if (line_bottom < visible_top) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        first_visible = lo;
    }
    
    // Add invisible spacer for lines before first visible (for correct scroll)
    if (first_visible > 0 && first_visible < static_cast<int>(cumulative_y_offsets_.size())) {
        ImGui::Dummy(ImVec2(0, cumulative_y_offsets_[first_visible]));
    }
    
    ImDrawList* dl = ImGui::GetWindowDrawList();
    ImU32 selColor = WithAlpha(Secondary(), 80);
    
    // Render visible lines
    int last_rendered_vi = first_visible - 1;
    for (int vi = first_visible; vi < visible_count; vi++) {
        if (vi < static_cast<int>(cumulative_y_offsets_.size()) &&
            cumulative_y_offsets_[vi] > visible_bottom) {
            break;
        }
        last_rendered_vi = vi;
        
        int i = visible_indices_[vi];
        const auto& line = lines_[i];
        const auto& segs = visible_wrap_segments_[vi];
        ImVec2 lineOrigin = ImGui::GetCursorScreenPos();
        float totalH = wrapped_heights_[vi];
        
        // Determine byte-level selection range for this line
        int selByteStart = 0, selByteEnd = 0;
        bool lineSelected = false;
        if (has_selection_ && i >= sel_start_pos.line && i <= sel_end_pos.line) {
            lineSelected = true;
            selByteStart = (i == sel_start_pos.line) ? sel_start_pos.col : 0;
            selByteEnd   = (i == sel_end_pos.line)   ? sel_end_pos.col
                                                     : static_cast<int>(line.text.size());
        }
        
        for (const auto& seg : segs) {
            float rowY = lineOrigin.y + seg.yOffset;
            const char* segStart = line.text.c_str() + seg.byteStart;
            const char* segEnd   = line.text.c_str() + seg.byteEnd;

            if (s_scanline_enabled && line_height > 0.0f) {
                int rowIndex = static_cast<int>(std::floor((cumulative_y_offsets_[vi] + seg.yOffset) / line_height + 0.5f));
                scanline_rows_.push_back({rowY, rowY + seg.height, rowIndex});
            }
            
            // Selection highlight for this sub-row
            if (lineSelected && selByteStart < seg.byteEnd && selByteEnd > seg.byteStart) {
                int hlStart = std::max(selByteStart, seg.byteStart) - seg.byteStart;
                int hlEnd   = std::min(selByteEnd,   seg.byteEnd)   - seg.byteStart;
                int segLen  = seg.byteEnd - seg.byteStart;
                
                float xStart = 0.0f;
                if (hlStart > 0) {
                    xStart = font->CalcTextSizeA(fontSize, FLT_MAX, 0,
                                                  segStart, segStart + hlStart).x;
                }
                float xEnd = font->CalcTextSizeA(fontSize, FLT_MAX, 0,
                                                  segStart, segStart + hlEnd).x;
                // Extend to window edge when selection reaches end of segment
                if (hlEnd >= segLen && selByteEnd >= static_cast<int>(line.text.size())) {
                    xEnd = std::max(xEnd + 8.0f, ImGui::GetWindowWidth());
                }
                
                dl->AddRectFilled(
                    ImVec2(lineOrigin.x + xStart, rowY),
                    ImVec2(lineOrigin.x + xEnd,   rowY + seg.height),
                    selColor);
            }
            
            // Render text segment
            if (seg.byteStart < seg.byteEnd) {
                dl->AddText(font, fontSize,
                            ImVec2(lineOrigin.x, rowY),
                            line.color, segStart, segEnd);
            }
        }
        
        // Advance ImGui cursor by the total wrapped height of this line
        ImGui::Dummy(ImVec2(0, totalH));
    }
    
    // Add spacer for lines after last visible (to maintain correct content height)
    if (last_rendered_vi >= 0 && last_rendered_vi < visible_count - 1) {
        float rendered_height = (last_rendered_vi < static_cast<int>(cumulative_y_offsets_.size()))
            ? cumulative_y_offsets_[last_rendered_vi] + wrapped_heights_[last_rendered_vi]
            : 0.0f;
        float remaining_height = total_wrapped_height_ - rendered_height;
        if (remaining_height > 0) {
            ImGui::Dummy(ImVec2(0, remaining_height));
        }
    }
    
    ImGui::Unindent(padX);
    ImGui::PopStyleVar();

    // Bottom padding keeps the last line above the fade-out zone.
    // Always present so that scrollMaxY stays stable when auto-scroll
    // toggles — otherwise the geometry shift clamps the user back to
    // bottom and a single scroll-up tick can't escape.
    {
        float fadeZone = std::min(Type().caption()->LegacySize * 3.0f,
                                  ImGui::GetWindowHeight() * 0.18f);
        ImGui::Dummy(ImVec2(0, fadeZone));
    }

    // Auto-scroll - when enabled, always scroll to bottom of content
    // This ensures daemon output stays visible and scrolled to bottom
    if (auto_scroll_) {
        ImGui::SetScrollHereY(1.0f);
        scroll_to_bottom_ = false;
        new_lines_since_scroll_ = 0;
    }

    // Filter indicator (text filter only — daemon toggle is already visible in toolbar)
    if (has_text_filter) {
        char filterBuf[128];
        snprintf(filterBuf, sizeof(filterBuf), TR("console_showing_lines"),
                 visible_count, lines_.size());
        ImVec2 indicatorPos = ImGui::GetCursorScreenPos();
        ImGui::GetWindowDrawList()->AddText(indicatorPos,
            WithAlpha(Warning(), 180), filterBuf);
        ImGui::Dummy(ImVec2(0, ImGui::GetTextLineHeight()));
    }
    
    // Right-click context menu
    if (ImGui::BeginPopupContextWindow("ConsoleContextMenu")) {
        if (ImGui::MenuItem(TR("copy"), "Ctrl+C", false, has_selection_)) {
            std::string selected = getSelectedText();
            if (!selected.empty()) {
                ImGui::SetClipboardText(selected.c_str());
            }
        }
        if (ImGui::MenuItem(TR("console_select_all"), "Ctrl+A")) {
            if (!lines_.empty()) {
                sel_anchor_ = {0, 0};
                sel_end_ = {static_cast<int>(lines_.size()) - 1, 
                           static_cast<int>(lines_.back().text.size())};
                has_selection_ = true;
            }
        }
        ImGui::Separator();
        if (ImGui::MenuItem(TR("console_clear_console"))) {
            // Can't call clear() here (already holding lock), just mark for clearing
            lines_.clear();
            has_selection_ = false;
        }
        ImGui::EndPopup();
    }

    // "New output" indicator when user is scrolled up and new lines arrived
    if (!auto_scroll_ && new_lines_since_scroll_ > 0) {
        float indicW = 140.0f;
        float indicH = 24.0f;
        ImDrawList* dlInd = ImGui::GetWindowDrawList();
        ImVec2 wMin = ImGui::GetWindowPos();
        ImVec2 wSize = ImGui::GetWindowSize();
        float ix = wMin.x + (wSize.x - indicW) * 0.5f;
        float iy = wMin.y + wSize.y - indicH - 8.0f;
        ImVec2 iMin(ix, iy);
        ImVec2 iMax(ix + indicW, iy + indicH);

        dlInd->AddRectFilled(iMin, iMax, IM_COL32(40, 40, 40, 220), 12.0f);
        dlInd->AddRect(iMin, iMax, IM_COL32(255, 218, 0, 120), 12.0f);

        char buf[48];
        snprintf(buf, sizeof(buf), TR("console_new_lines"),
                 new_lines_since_scroll_, new_lines_since_scroll_ != 1 ? "s" : "");
        ImFont* capFont = Type().caption();
        if (!capFont) capFont = ImGui::GetFont();
        ImFont* icoFont = Type().iconSmall();
        if (!icoFont) icoFont = capFont;

        // Measure icon + text to center them together
        ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, ICON_MD_ARROW_DOWNWARD);
        ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
        float totalW = icoSz.x + txtSz.x;
        float startX = ix + (indicW - totalW) * 0.5f;
        float icoY = iy + (indicH - icoSz.y) * 0.5f;
        float txtY = iy + (indicH - txtSz.y) * 0.5f;
        ImU32 col = IM_COL32(255, 218, 0, 255);
        dlInd->AddText(icoFont, icoFont->LegacySize, ImVec2(startX, icoY), col, ICON_MD_ARROW_DOWNWARD);
        dlInd->AddText(capFont, capFont->LegacySize, ImVec2(startX + icoSz.x, txtY), col, buf);

        // Click to jump to bottom
        ImGui::SetCursorScreenPos(iMin);
        if (ImGui::InvisibleButton("##scrollToBottom", ImVec2(indicW, indicH))) {
            auto_scroll_ = true;
            scroll_to_bottom_ = true;
            new_lines_since_scroll_ = 0;
        }
    }
}

ConsoleTab::TextPos ConsoleTab::screenToTextPos(ImVec2 screen_pos, float line_height) const
{
    TextPos pos;
    
    if (visible_indices_.empty()) {
        return {0, 0};
    }
    
    float relative_y = screen_pos.y - output_origin_.y;
    
    // Binary search for the visible line that contains this Y position
    int visible_line = 0;
    if (!cumulative_y_offsets_.empty()) {
        int lo = 0, hi = static_cast<int>(cumulative_y_offsets_.size()) - 1;
        while (lo < hi) {
            int mid = (lo + hi + 1) / 2;
            if (cumulative_y_offsets_[mid] <= relative_y) {
                lo = mid;
            } else {
                hi = mid - 1;
            }
        }
        visible_line = lo;
    }
    
    // Clamp visible line
    if (visible_line < 0) visible_line = 0;
    if (visible_line >= static_cast<int>(visible_indices_.size())) {
        visible_line = static_cast<int>(visible_indices_.size()) - 1;
        pos.line = visible_indices_[visible_line];
        pos.col = static_cast<int>(lines_[pos.line].text.size());
        return pos;
    }
    
    pos.line = visible_indices_[visible_line];
    const std::string& text = lines_[pos.line].text;
    
    if (text.empty()) {
        pos.col = 0;
        return pos;
    }
    
    // Find which sub-row (wrap segment) the mouse Y falls into
    const auto& segs = visible_wrap_segments_[visible_line];
    float lineRelY = relative_y - cumulative_y_offsets_[visible_line];
    int segIdx = 0;
    for (int s = 0; s < static_cast<int>(segs.size()); s++) {
        if (lineRelY >= segs[s].yOffset)
            segIdx = s;
    }
    const auto& seg = segs[segIdx];
    
    // Calculate column within this segment from X position
    float relative_x = screen_pos.x - output_origin_.x;
    if (relative_x <= 0.0f) {
        pos.col = seg.byteStart;
        return pos;
    }
    
    ImFont* font = ImGui::GetFont();
    float fontSize = ImGui::GetFontSize();
    const char* segStart = text.c_str() + seg.byteStart;
    const char* segEnd   = text.c_str() + seg.byteEnd;
    int segLen = seg.byteEnd - seg.byteStart;
    
    // Walk characters within this segment for accurate positioning
    pos.col = seg.byteEnd; // default: past end of segment
    for (int c = 0; c < segLen; c++) {
        float wCur = font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + c + 1).x;
        float wPrev = (c > 0) ? font->CalcTextSizeA(fontSize, FLT_MAX, 0, segStart, segStart + c).x : 0.0f;
        float charMid = (wPrev + wCur) * 0.5f;
        if (relative_x < charMid) {
            pos.col = seg.byteStart + c;
            return pos;
        }
    }
    
    return pos;
}

bool ConsoleTab::isPosBeforeOrEqual(const TextPos& a, const TextPos& b) const
{
    if (a.line < b.line) return true;
    if (a.line > b.line) return false;
    return a.col <= b.col;
}

ConsoleTab::TextPos ConsoleTab::selectionStart() const
{
    return isPosBeforeOrEqual(sel_anchor_, sel_end_) ? sel_anchor_ : sel_end_;
}

ConsoleTab::TextPos ConsoleTab::selectionEnd() const
{
    return isPosBeforeOrEqual(sel_anchor_, sel_end_) ? sel_end_ : sel_anchor_;
}

std::string ConsoleTab::getSelectedText() const
{
    if (!has_selection_) return "";
    
    TextPos start = selectionStart();
    TextPos end = selectionEnd();
    
    if (start.line < 0 || start.line >= static_cast<int>(lines_.size())) return "";
    
    // Build a set of visible line indices for quick lookup
    // (Only copy visible lines when filtering is active)
    std::unordered_set<int> visible_set(visible_indices_.begin(), visible_indices_.end());
    bool has_filter = !visible_indices_.empty() && visible_indices_.size() < lines_.size();
    
    std::string result;
    bool first_line = true;
    
    for (int i = start.line; i <= end.line && i < static_cast<int>(lines_.size()); i++) {
        // Skip lines that aren't visible when filtering
        if (has_filter && visible_set.find(i) == visible_set.end()) {
            continue;
        }
        
        const std::string& text = lines_[i].text;
        
        int col_start = 0;
        int col_end = static_cast<int>(text.size());
        
        if (i == start.line) col_start = std::min(start.col, static_cast<int>(text.size()));
        if (i == end.line) col_end = std::min(end.col, static_cast<int>(text.size()));
        
        // Add newline between lines (but not before first)
        if (!first_line) {
            result += "\n";
        }
        first_line = false;
        
        if (col_start < col_end) {
            result += text.substr(col_start, col_end - col_start);
        }
    }
    
    return result;
}

void ConsoleTab::clearSelection()
{
    has_selection_ = false;
    is_selecting_ = false;
    sel_anchor_ = {-1, 0};
    sel_end_ = {-1, 0};
}

void ConsoleTab::renderInput(rpc::RPCClient* rpc, rpc::RPCWorker* worker)
{
    using namespace material;
    
    // Glass panel for input area
    ImDrawList* dlIn = ImGui::GetWindowDrawList();
    float inputPanelH = ImGui::GetFrameHeightWithSpacing() + Layout::spacingSm() + Layout::spacingXs();
    ImVec2 inMin = ImGui::GetCursorScreenPos();
    ImVec2 inMax(inMin.x + ImGui::GetContentRegionAvail().x, inMin.y + inputPanelH);
    GlassPanelSpec inGlass;
    inGlass.rounding = Layout::glassRounding();
    inGlass.fillAlpha = 12;
    material::DrawGlassPanel(dlIn, inMin, inMax, inGlass);
    
    // Center content vertically within glass panel
    float inputFrameH = ImGui::GetFrameHeight();
    ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (inputPanelH - inputFrameH) * 0.5f);
    ImGui::SetCursorPosX(ImGui::GetCursorPosX() + Layout::spacingMd());



    // Input field
    ImGui::PushItemWidth(-Layout::spacingMd());
    
    ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue |
                                 ImGuiInputTextFlags_CallbackHistory |
                                 ImGuiInputTextFlags_CallbackCompletion;
    
    bool reclaim_focus = false;
    
    auto callback = [](ImGuiInputTextCallbackData* data) -> int {
        ConsoleTab* console = static_cast<ConsoleTab*>(data->UserData);
        
        if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) {
            // Handle history navigation
            if (console->command_history_.empty()) return 0;
            
            int prev_index = console->history_index_;
            console->history_index_ = NavigateConsoleHistoryIndex(
                console->history_index_,
                console->command_history_.size(),
                data->EventKey == ImGuiKey_UpArrow);
            
            if (prev_index != console->history_index_) {
                std::string history = ConsoleHistoryEntry(console->command_history_, console->history_index_);
                data->DeleteChars(0, data->BufTextLen);
                data->InsertChars(0, history.c_str());
            }
        }
        else if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) {
            std::string input(data->Buf);
            if (!input.empty()) {
                auto completion = CompleteConsoleCommand(input);
                
                if (completion.matches.size() == 1) {
                    // Single match — complete it
                    data->DeleteChars(0, data->BufTextLen);
                    data->InsertChars(0, completion.matches.front().c_str());
                } else if (completion.matches.size() > 1) {
                    // Multiple matches — show list in console and complete common prefix
                    console->addLine(TR("console_completions"), ConsoleTab::COLOR_INFO);
                    for (const auto& line : FormatConsoleCompletionLines(completion.matches)) {
                        console->addLine(line, ConsoleTab::COLOR_RESULT);
                    }
                    
                    // Complete to longest common prefix
                    if (completion.commonPrefix.length() > input.length()) {
                        data->DeleteChars(0, data->BufTextLen);
                        data->InsertChars(0, completion.commonPrefix.c_str());
                    }
                }
            }
        }
        
        return 0;
    };
    
    if (ImGui::InputText("##ConsoleInput", input_buffer_, sizeof(input_buffer_), flags, callback, this)) {
        std::string cmd(input_buffer_);
        if (!cmd.empty()) {
            executeCommand(cmd, rpc, worker);
            input_buffer_[0] = '\0';
            reclaim_focus = true;
        }
    }
    
    ImGui::PopItemWidth();

    // Auto-focus on input
    if (reclaim_focus) {
        ImGui::SetKeyboardFocusHere(-1);
    }
}

void ConsoleTab::renderCommandsPopup()
{
    using namespace material;
    
    float popW = std::min(schema::UI().drawElement("tabs.console", "popup-max-width").size, ImGui::GetMainViewport()->Size.x * schema::UI().drawElement("tabs.console", "popup-width-ratio").size);
    if (!material::BeginOverlayDialog(TR("console_rpc_reference"), &show_commands_popup_, popW, 0.94f)) {
        return;
    }

    Type().text(TypeStyle::H6, TR("console_rpc_reference"));
    ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
    
    // Search filter
    static char cmdFilter[128] = {0};
    ImGui::SetNextItemWidth(-1);
    ImGui::InputTextWithHint("##CmdSearch", TR("console_search_commands"), cmdFilter, sizeof(cmdFilter));
    ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
    
    const auto& categories = consoleCommandCategories();
    
    std::string filter(cmdFilter);
    std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
    
    ImGui::BeginChild("CmdListScroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing() - Layout::spacingXs()),
                      false);
    
    ImGui::PushFont(Type().caption());
    
    float cmdMinWidth = schema::UI().drawElement("tabs.console", "cmd-min-width").sizeOr(500.0f);
    float popupInnerW = ImGui::GetContentRegionAvail().x;
    bool showParams = popupInnerW >= cmdMinWidth;
    int catIdx = 0;

    for (const auto& cat : categories) {
        // Count matching commands in this category
        int matchCount = 0;
        if (filter.empty()) {
            matchCount = cat.count;
        } else {
            for (int i = 0; i < cat.count; i++) {
                std::string name(cat.commands[i].name);
                std::string desc(cat.commands[i].desc);
                std::string params(cat.commands[i].params);
                std::transform(name.begin(), name.end(), name.begin(), ::tolower);
                std::transform(desc.begin(), desc.end(), desc.begin(), ::tolower);
                std::transform(params.begin(), params.end(), params.begin(), ::tolower);
                if (name.find(filter) != std::string::npos ||
                    desc.find(filter) != std::string::npos ||
                    params.find(filter) != std::string::npos) {
                    matchCount++;
                }
            }
        }
        if (matchCount == 0) { catIdx++; continue; }

        // Default-open only the first category (Control); collapse the rest
        ImGuiTreeNodeFlags headerFlags = (catIdx == 0) ? ImGuiTreeNodeFlags_DefaultOpen : 0;
        // When filtering, open all matching categories
        if (!filter.empty()) headerFlags = ImGuiTreeNodeFlags_DefaultOpen;

        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
        // Show match count badge when filtering
        char headerLabel[128];
        if (!filter.empty()) {
            snprintf(headerLabel, sizeof(headerLabel), "%s  (%d)", cat.name, matchCount);
        } else {
            snprintf(headerLabel, sizeof(headerLabel), "%s", cat.name);
        }
        bool open = ImGui::CollapsingHeader(headerLabel, headerFlags);
        ImGui::PopStyleColor();
        catIdx++;
        
        if (open) {
            float nameColW = schema::UI().drawElement("tabs.console", "cmd-name-col-width").size * Layout::hScale();
            float paramsColW = schema::UI().drawElement("tabs.console", "cmd-params-col-width").size * Layout::hScale();
            int numCols = showParams ? 3 : 2;
            if (ImGui::BeginTable("##cmds", numCols, ImGuiTableFlags_None)) {
                ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, nameColW);
                if (showParams)
                    ImGui::TableSetupColumn("Parameters", ImGuiTableColumnFlags_WidthFixed, paramsColW);
                ImGui::TableSetupColumn("Desc", ImGuiTableColumnFlags_WidthStretch);
                
                for (int i = 0; i < cat.count; i++) {
                    const auto& cmd = cat.commands[i];
                    std::string nameLower(cmd.name);
                    std::string descLower(cmd.desc);
                    std::string paramsLower(cmd.params);
                    std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
                    std::transform(descLower.begin(), descLower.end(), descLower.begin(), ::tolower);
                    std::transform(paramsLower.begin(), paramsLower.end(), paramsLower.begin(), ::tolower);
                    
                    if (!filter.empty() &&
                        nameLower.find(filter) == std::string::npos &&
                        descLower.find(filter) == std::string::npos &&
                        paramsLower.find(filter) == std::string::npos) {
                        continue;
                    }
                    
                    ImGui::TableNextRow();
                    ImGui::TableNextColumn();
                    ImGui::PushStyleColor(ImGuiCol_Text,
                        ImGui::ColorConvertU32ToFloat4(IM_COL32(100, 180, 255, 255)));
                    ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0));
                    ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.2f, 0.4f, 0.6f, 0.3f));
                    char selId[128];
                    snprintf(selId, sizeof(selId), "%s##cmdRef", cmd.name);
                    if (ImGui::Selectable(selId, false)) {
                        if (cmd.params[0] != '\0') {
                            snprintf(input_buffer_, sizeof(input_buffer_), "%s %s", cmd.name, cmd.params);
                        } else {
                            strncpy(input_buffer_, cmd.name, sizeof(input_buffer_) - 1);
                            input_buffer_[sizeof(input_buffer_) - 1] = '\0';
                        }
                        ImGui::CloseCurrentPopup();
                    }
                    if (ImGui::IsItemHovered()) {
                        ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                        if (cmd.params[0] != '\0')
                            ImGui::SetTooltip(TR("console_click_insert_params"), cmd.name, cmd.params);
                        else
                            ImGui::SetTooltip(TR("console_click_insert"), cmd.name);
                    }
                    ImGui::PopStyleColor(3);
                    if (showParams) {
                        ImGui::TableNextColumn();
                        // Style optional params [param] in dimmed text
                        const char* p = cmd.params;
                        bool first = true;
                        while (*p) {
                            const char* bracketStart = strchr(p, '[');
                            if (bracketStart) {
                                // Draw required part before bracket
                                if (bracketStart > p) {
                                    if (!first) ImGui::SameLine(0, 0);
                                    std::string req(p, bracketStart);
                                    ImGui::TextColored(
                                        ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()),
                                        "%s", req.c_str());
                                    first = false;
                                }
                                // Find matching ]
                                const char* bracketEnd = strchr(bracketStart, ']');
                                if (bracketEnd) {
                                    if (!first) ImGui::SameLine(0, 0);
                                    std::string opt(bracketStart, bracketEnd + 1);
                                    ImGui::TextColored(
                                        ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()),
                                        "%s", opt.c_str());
                                    first = false;
                                    p = bracketEnd + 1;
                                } else {
                                    if (!first) ImGui::SameLine(0, 0);
                                    ImGui::TextColored(
                                        ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()),
                                        "%s", bracketStart);
                                    first = false;
                                    break;
                                }
                            } else {
                                if (!first) ImGui::SameLine(0, 0);
                                ImGui::TextColored(
                                    ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled()),
                                    "%s", p);
                                first = false;
                                break;
                            }
                        }
                    }
                    ImGui::TableNextColumn();
                    ImGui::TextColored(
                        ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()),
                        "%s", cmd.desc);
                }
                
                ImGui::EndTable();
            }
            ImGui::Spacing();
        }
    }
    
    ImGui::PopFont();
    ImGui::EndChild();
    
    ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
    
    // Close button
    if (ImGui::Button(TR("console_close"), ImVec2(-1, 0))) {
        cmdFilter[0] = '\0';
        show_commands_popup_ = false;
    }
    
    material::EndOverlayDialog();
}

void ConsoleTab::executeCommand(const std::string& cmd, rpc::RPCClient* rpc, rpc::RPCWorker* worker)
{
    using namespace material;
    // Add to history (avoid duplicates)
    AppendConsoleHistory(command_history_, cmd, 100);
    history_index_ = -1;
    
    // Echo command
    addLine("> " + cmd, COLOR_COMMAND);
    
    // Handle built-in commands
    if (cmd == "clear") {
        clear();
        return;
    }
    
    if (cmd == "help") {
        addLine(TR("console_available_commands"), COLOR_INFO);
        addLine(TR("console_help_clear"), COLOR_RESULT);
        addLine(TR("console_help_help"), COLOR_RESULT);
        addLine("", COLOR_RESULT);
        addLine(TR("console_common_rpc"), COLOR_INFO);
        addLine(TR("console_help_getinfo"), COLOR_RESULT);
        addLine(TR("console_help_getbalance"), COLOR_RESULT);
        addLine(TR("console_help_gettotalbalance"), COLOR_RESULT);
        addLine(TR("console_help_getblockcount"), COLOR_RESULT);
        addLine(TR("console_help_getpeerinfo"), COLOR_RESULT);
        addLine(TR("console_help_setgenerate"), COLOR_RESULT);
        addLine(TR("console_help_getmininginfo"), COLOR_RESULT);
        addLine(TR("console_help_stop"), COLOR_RESULT);
        addLine("", COLOR_RESULT);
        addLine(TR("console_click_commands"), COLOR_INFO);
        addLine(TR("console_tab_completion"), COLOR_INFO);
        return;
    }
    
    // Execute RPC command
    if (!rpc || !rpc->isConnected()) {
        addLine(TR("console_not_connected"), COLOR_ERROR);
        return;
    }
    
    auto call = BuildConsoleRpcCall(cmd);
    if (!call.valid) return;

    std::string method = call.method;
    nlohmann::json params = call.params;
    
    // Execute RPC call on worker thread to avoid blocking UI
    if (worker) {
        // Capture 'this' for addLine calls in MainCb (runs on main thread, ConsoleTab outlives callbacks)
        auto self = this;
        worker->post([rpc, method, params, self]() -> rpc::RPCWorker::MainCb {
            std::string result_str;
            bool is_error = false;
            try {
                rpc::RPCClient::TraceScope trace("Console tab / User command");
                result_str = rpc->callRaw(method, params);
            } catch (const std::exception& e) {
                result_str = e.what();
                is_error = true;
            }
            return [result_str, is_error, self]() {
                // Process results on main thread where ImGui colors are available
                using namespace material;
                ImU32 json_key_col   = WithAlpha(Secondary(), 255);
                ImU32 json_str_col   = WithAlpha(Success(), 255);
                ImU32 json_num_col   = WithAlpha(Warning(), 255);
                ImU32 json_brace_col = IM_COL32(200, 200, 200, 150);

                for (const auto& resultLine : FormatConsoleRpcResultLines(result_str, is_error)) {
                    ImU32 lineCol = COLOR_RESULT;
                    switch (resultLine.role) {
                        case ConsoleResultLineRole::Error: lineCol = COLOR_ERROR; break;
                        case ConsoleResultLineRole::JsonKey: lineCol = json_key_col; break;
                        case ConsoleResultLineRole::JsonString: lineCol = json_str_col; break;
                        case ConsoleResultLineRole::JsonNumber: lineCol = json_num_col; break;
                        case ConsoleResultLineRole::JsonBrace: lineCol = json_brace_col; break;
                        case ConsoleResultLineRole::Result: break;
                    }
                    self->addLine(resultLine.text, lineCol);
                }
            };
        });
    } else {
        // Fallback: synchronous execution if no worker available
        try {
            rpc::RPCClient::TraceScope trace("Console tab / User command");
            std::string result_str = rpc->callRaw(method, params);
            for (const auto& resultLine : FormatConsoleRpcResultLines(result_str, false)) {
                addLine(resultLine.text, COLOR_RESULT);
            }
        } catch (const std::exception& e) {
            for (const auto& resultLine : FormatConsoleRpcResultLines(e.what(), true)) {
                addLine(resultLine.text, COLOR_ERROR);
            }
        }
    }
}

void ConsoleTab::addLine(const std::string& line, ImU32 color)
{
    std::lock_guard<std::mutex> lock(lines_mutex_);
    
    lines_.push_back({line, color});
    
    // Limit buffer size — adjust selection indices when lines are removed
    // from the front so the highlight stays on the text the user selected.
    int popped = 0;
    while (lines_.size() > 10000) {
        lines_.pop_front();
        popped++;
    }
    if (popped > 0 && has_selection_) {
        sel_anchor_.line -= popped;
        sel_end_.line    -= popped;
        if (sel_anchor_.line < 0 && sel_end_.line < 0) {
            // Entire selection was in the removed range
            has_selection_ = false;
            is_selecting_  = false;
        } else {
            if (sel_anchor_.line < 0) { sel_anchor_.line = 0; sel_anchor_.col = 0; }
            if (sel_end_.line < 0)    { sel_end_.line = 0;    sel_end_.col = 0;    }
        }
    }
    
    // Track new output while user is scrolled up
    if (!auto_scroll_) {
        new_lines_since_scroll_++;
    }
    
    scroll_to_bottom_ = auto_scroll_;
}

void ConsoleTab::addRpcTraceLine(const std::string& source, const std::string& method)
{
    addLine("[rpc] [" + rpcTraceTimestamp() + "] [" + source + "] " + method, COLOR_RPC);
}

void ConsoleTab::addCommandResult(const std::string& cmd, const std::string& result, bool is_error)
{
    addLine("> " + cmd, COLOR_COMMAND);
    
    std::istringstream stream(result);
    std::string line;
    while (std::getline(stream, line)) {
        addLine(line, is_error ? COLOR_ERROR : COLOR_RESULT);
    }
}

void ConsoleTab::clear()
{
    {
        std::lock_guard<std::mutex> lock(lines_mutex_);
        lines_.clear();
        last_daemon_output_size_ = 0;
        last_xmrig_output_size_ = 0;
    }
    // addLine() takes the lock itself, so call it outside the locked scope
    addLine(TR("console_cleared"), COLOR_INFO);
}

} // namespace ui
} // namespace dragonx
