// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// xmrig_manager.cpp — Pool mining process management via drg-xmrig.
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.

#include "xmrig_manager.h"
#include "../resources/embedded_resources.h"

#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <filesystem>
#include <random>
#include <sstream>
#include <algorithm>
#include <chrono>

#include <nlohmann/json.hpp>
#include <curl/curl.h>

#include "../util/logger.h"
#include "../util/platform.h"
#include "../util/pool_registry.h"

#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
#include <shlobj.h>
#include <psapi.h>
#else
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pwd.h>
#include <errno.h>
#endif

namespace fs = std::filesystem;
using json = nlohmann::json;

namespace dragonx {
namespace daemon {

// ============================================================================
// Helpers
// ============================================================================

static std::string randomHexToken(int bytes = 16) {
    static const char hex[] = "0123456789abcdef";
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dist(0, 15);
    std::string out;
    out.reserve(bytes * 2);
    for (int i = 0; i < bytes * 2; ++i)
        out.push_back(hex[dist(gen)]);
    return out;
}

static int randomPort() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<int> dist(18000, 18999);
    return dist(gen);
}

static std::string getConfigDir() {
#ifdef _WIN32
    char path[MAX_PATH];
    if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
        return std::string(path) + "\\ObsidianDragon";
    }
    return ".";
#else
    const char* home = getenv("HOME");
    if (!home) {
        struct passwd* pw = getpwuid(getuid());
        home = pw ? pw->pw_dir : "/tmp";
    }
    return std::string(home) + "/.config/ObsidianDragon";
#endif
}

// libcurl write callback
static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
    auto* s = static_cast<std::string*>(userdata);
    const size_t add = sz * n;
    // Stats JSON (local xmrig HTTP API + pool API) is tiny; refuse an unbounded body from a hostile or
    // MITM'd endpoint so it can't grow this string until OOM. Returning < add aborts the transfer. (L-02)
    constexpr size_t kMaxStatsBytes = 1u << 20;  // 1 MiB
    if (s->size() + add > kMaxStatsBytes) return 0;
    s->append(static_cast<char*>(ptr), add);
    return add;
}

// True if `host` (already stripped of scheme+port) is a loopback/private/link-local/single-label target
// that a public mining pool would never be — used to refuse a background stats GET to it (M-09).
static bool hostLooksInternal(const std::string& host) {
    if (host.empty() || host == "localhost") return true;
    if (host.rfind("127.", 0) == 0 || host.rfind("10.", 0) == 0 ||
        host.rfind("192.168.", 0) == 0 || host.rfind("169.254.", 0) == 0) return true;
    if (host.rfind("172.", 0) == 0) {                   // 172.16.0.0 - 172.31.255.255
        const int second = std::atoi(host.c_str() + 4);
        if (second >= 16 && second <= 31) return true;
    }
    if (host.find(':') != std::string::npos) {          // IPv6 literal: loopback / ULA / link-local
        if (host == "::1" || host.rfind("fc", 0) == 0 || host.rfind("fd", 0) == 0 ||
            host.rfind("fe80", 0) == 0) return true;
    }
    if (host.size() >= 6 && host.compare(host.size() - 6, 6, ".local") == 0) return true;
    if (host.find('.') == std::string::npos) return true;  // bare single-label name = LAN/hosts, not a pool
    return false;
}

// ============================================================================
// Lifecycle
// ============================================================================

XmrigManager::XmrigManager() = default;

XmrigManager::~XmrigManager() {
    should_stop_ = true;
    if (isRunning()) {
        stop(3000);
    }
    // Join a monitor thread left joinable by an unexpected xmrig exit (State::Error, so isRunning() is
    // false and stop() above was skipped) — std::thread's destructor would otherwise std::terminate(). (M-04)
    if (monitor_thread_.joinable())
        monitor_thread_.join();
}

// ============================================================================
// Binary discovery
// ============================================================================

std::string XmrigManager::findXmrigBinary() {
    // Use the embedded_resources system (same pattern as daemon)
    std::string path = resources::getXmrigPath();
    if (!path.empty() && fs::exists(path)) {
        return path;
    }

    // Fallback: system PATH — windowless so it never flashes a console.
#ifdef _WIN32
    std::string out = util::Platform::runHiddenCapture("where xmrig.exe");
#else
    std::string out = util::Platform::runHiddenCapture("which xmrig");
#endif
    {
        std::string s = out;
        const auto nl = s.find_first_of("\r\n");   // first line only
        if (nl != std::string::npos) s.erase(nl);
        while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back();
        if (!s.empty() && fs::exists(s)) return s;
    }

    return {};
}

// ============================================================================
// Config generation
// ============================================================================

bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath) {
    api_port_  = randomPort();
    api_token_ = randomHexToken(16);

    int hw = (int)std::thread::hardware_concurrency();
    if (hw < 1) hw = 1;
    // Use explicit thread count (not just a hint)
    threads_ = (cfg.threads > 0) ? cfg.threads : std::max(1, hw / 2);
    if (threads_ > hw) threads_ = hw;

    json j;
    j["autosave"]   = false;
    j["background"] = false;
    j["colors"]     = false;

    j["http"] = {
        {"enabled", true},
        {"host", "127.0.0.1"},
        {"port", api_port_},
        {"access-token", api_token_},
        {"restricted", true}
    };

    j["randomx"] = {
        {"init", -1},
        {"mode", "auto"},
        {"1gb-pages", false},
        {"numa", true},
        {"scratchpad_prefetch_mode", 1}
    };

    j["cpu"] = {
        {"enabled", true},
        {"huge-pages", cfg.hugepages},
        {"max-threads-hint", 100},  // Use 100% of allotted threads
        {"priority", 0},            // Idle priority (lowest) - prevents UI lag
        {"yield", true}             // Yield to other processes
    };

    j["pools"] = json::array({
        {
            {"algo", cfg.algo},
            {"url", cfg.pool_url},
            {"user", cfg.wallet_address},
            {"pass", cfg.worker_name},
            {"keepalive", true},
            {"tls", cfg.tls}
        }
    });

    j["donate-level"] = 0;
    j["print-time"]   = 10;
    j["retries"]      = 5;
    j["retry-pause"]  = 5;

    try {
        fs::create_directories(fs::path(outPath).parent_path());
        const std::string dumped = j.dump(4);
#ifndef _WIN32
        // Create the config 0600 AT CREATION (open with mode) so the API token + wallet address are never
        // in a world/group-readable file — even for a local attacker who opened it in the old
        // create-then-chmod window and held the fd open across the chmod. (L-01)
        int fd = ::open(outPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
        if (fd < 0) {
            setLastError("Cannot write xmrig config: " + outPath);
            DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
            return false;
        }
        size_t off = 0;
        bool wrote = true;
        while (off < dumped.size()) {
            ssize_t nw = ::write(fd, dumped.data() + off, dumped.size() - off);
            if (nw <= 0) { wrote = false; break; }
            off += static_cast<size_t>(nw);
        }
        ::close(fd);
        if (!wrote) {
            setLastError("Cannot write xmrig config: " + outPath);
            return false;
        }
        return true;
#else
        std::ofstream ofs(outPath, std::ios::trunc);
        if (!ofs.is_open()) {
            setLastError("Cannot write xmrig config: " + outPath);
            DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
            return false;
        }
        ofs << dumped;
        ofs.close();
        return true;
#endif
    } catch (const std::exception& e) {
        setLastError(std::string("Config write error: ") + e.what());
        DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
        return false;
    }
}

// ============================================================================
// start / stop
// ============================================================================

bool XmrigManager::start(const Config& cfg) {
    if (state_ == State::Running || state_ == State::Starting) {
        setLastError("Already running");
        DEBUG_LOGF("[WARN] XmrigManager: %s\n", last_error_.c_str());
        return false;
    }

    state_ = State::Starting;
    should_stop_ = false;
    setLastError(std::string());
    {
        std::lock_guard<std::mutex> lk(output_mutex_);
        process_output_.clear();
    }
    {
        std::lock_guard<std::mutex> lk(stats_mutex_);
        stats_ = PoolStats{};
    }

    // Extract pool hostname for stats API queries
    {
        std::string url = cfg.pool_url;
        // Strip protocol prefix if present
        auto pos = url.find("://");
        if (pos != std::string::npos) url = url.substr(pos + 3);
        // Strip port suffix
        pos = url.find(':');
        if (pos != std::string::npos) url = url.substr(0, pos);
        pool_host_ = url;
    }

    // Find binary
    std::string binary = findXmrigBinary();
    if (binary.empty()) {
        setLastError("xmrig binary not found");
        state_ = State::Error;
        DEBUG_LOGF("[ERROR] XmrigManager: xmrig binary not found\n");
        return false;
    }
    DEBUG_LOGF("[INFO] XmrigManager: found binary at %s\n", binary.c_str());

    // Generate config
    std::string cfgDir = getConfigDir();
#ifdef _WIN32
    std::string cfgPath = cfgDir + "\\xmrig-pool.json";
#else
    std::string cfgPath = cfgDir + "/xmrig-pool.json";
#endif
    if (!generateConfig(cfg, cfgPath)) {
        state_ = State::Error;
        DEBUG_LOGF("[ERROR] XmrigManager: failed to generate config\n");
        return false;
    }
    DEBUG_LOGF("[INFO] XmrigManager: config written to %s (API port %d, threads %d)\n",
           cfgPath.c_str(), api_port_, threads_);

    // Spawn process
    if (!startProcess(binary, cfgPath, threads_)) {
        state_ = State::Error;
        return false;
    }

    // Join a prior monitor thread before move-assigning: if xmrig exited unexpectedly, monitorProcess set
    // State::Error and returned, leaving monitor_thread_ joinable — move-assigning over a joinable
    // std::thread calls std::terminate() and aborts the whole wallet. (M-04)
    if (monitor_thread_.joinable())
        monitor_thread_.join();
    monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this);
    state_ = State::Running;
    DEBUG_LOGF("[INFO] XmrigManager: started\n");
    return true;
}

void XmrigManager::stop(int wait_ms) {
    if (state_ == State::Stopped || state_ == State::Stopping)
        return;

    state_ = State::Stopping;
    should_stop_ = true;

#ifdef _WIN32
    if (process_handle_) {
        // Try graceful termination first
        TerminateProcess(process_handle_, 0);
        WaitForSingleObject(process_handle_, wait_ms);
        CloseHandle(process_handle_);
        process_handle_ = nullptr;
    }
    if (stdout_read_) {
        CloseHandle(stdout_read_);
        stdout_read_ = nullptr;
    }
#else
    if (process_pid_ > 0) {
        // Send SIGTERM to process group
        kill(-process_pid_, SIGTERM);

        // Poll-wait with drain
        auto deadline = std::chrono::steady_clock::now()
                      + std::chrono::milliseconds(wait_ms);
        while (std::chrono::steady_clock::now() < deadline) {
            drainOutput();
            int status = 0;
            pid_t ret = waitpid(process_pid_, &status, WNOHANG);
            if (ret == process_pid_ || ret < 0) break;
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
        }

        // If still alive, SIGKILL
        if (kill(process_pid_, 0) == 0) {
            kill(-process_pid_, SIGKILL);
            waitpid(process_pid_, nullptr, 0);
        }
        process_pid_ = 0;
    }
    if (stdout_fd_ >= 0) {
        close(stdout_fd_);
        stdout_fd_ = -1;
    }
#endif

    if (monitor_thread_.joinable())
        monitor_thread_.join();

    state_ = State::Stopped;
    DEBUG_LOGF("[INFO] XmrigManager: stopped\n");
}

// ============================================================================
// Process spawning — platform-specific
// ============================================================================

#ifdef _WIN32

bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) {
    SECURITY_ATTRIBUTES sa{};
    sa.nLength = sizeof(sa);
    sa.bInheritHandle = TRUE;

    HANDLE hRead = nullptr, hWrite = nullptr;
    if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
        setLastError("CreatePipe failed");
        DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
        return false;
    }
    SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);

    // Use explicit --threads to enforce exact thread count (not just a hint)
    std::string cmdLine = "\"" + xmrigPath + "\" --config=\"" + cfgPath + "\" --threads=" + std::to_string(threads);

    STARTUPINFOA si{};
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
    si.hStdOutput = hWrite;
    si.hStdError  = hWrite;
    si.wShowWindow = SW_HIDE;

    PROCESS_INFORMATION pi{};
    BOOL ok = CreateProcessA(
        nullptr, const_cast<char*>(cmdLine.c_str()),
        nullptr, nullptr, TRUE,
        CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | IDLE_PRIORITY_CLASS,
        nullptr, nullptr, &si, &pi
    );

    CloseHandle(hWrite);

    if (!ok) {
        CloseHandle(hRead);
        DWORD err = GetLastError();
        char errBuf[256];
        FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                       NULL, err, 0, errBuf, sizeof(errBuf), NULL);
        setLastError("CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf);
        DEBUG_LOGF("[ERROR] XmrigManager: %s\nCommand: %s\n", last_error_.c_str(), cmdLine.c_str());
        return false;
    }

    process_handle_ = pi.hProcess;
    stdout_read_    = hRead;
    CloseHandle(pi.hThread);
    
    return true;
}

bool XmrigManager::isRunning() const {
    if (!process_handle_) return false;
    DWORD exit_code;
    GetExitCodeProcess(process_handle_, &exit_code);
    return exit_code == STILL_ACTIVE;
}

double XmrigManager::getMemoryUsageMB() const {
    if (!process_handle_) return 0.0;
    PROCESS_MEMORY_COUNTERS pmc;
    ZeroMemory(&pmc, sizeof(pmc));
    pmc.cb = sizeof(pmc);
    if (GetProcessMemoryInfo(process_handle_, &pmc, sizeof(pmc))) {
        return static_cast<double>(pmc.WorkingSetSize) / (1024.0 * 1024.0);
    }
    return 0.0;
}

void XmrigManager::drainOutput() {
    if (!stdout_read_) return;
    char buf[4096];
    DWORD avail = 0;
    while (PeekNamedPipe(stdout_read_, nullptr, 0, nullptr, &avail, nullptr) && avail > 0) {
        DWORD nread = 0;
        DWORD toRead = std::min(avail, (DWORD)sizeof(buf));
        if (ReadFile(stdout_read_, buf, toRead, &nread, nullptr) && nread > 0) {
            std::lock_guard<std::mutex> lk(output_mutex_);
            appendOutput(buf, nread);
        } else {
            break;
        }
    }
}

#else // ---- POSIX ----

bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) {
    int pipefd[2];
    if (pipe(pipefd) != 0) {
        setLastError("pipe() failed");
        DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
        return false;
    }

    pid_t pid = fork();
    if (pid < 0) {
        setLastError("fork() failed");
        DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
        close(pipefd[0]);
        close(pipefd[1]);
        return false;
    }

    if (pid == 0) {
        // Child
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        dup2(pipefd[1], STDERR_FILENO);
        close(pipefd[1]);

        // Detach from controlling terminal's stdin to prevent SIGTTIN/SIGTTOU
        // when running in a new process group (setpgid below).
        int devnull = open("/dev/null", O_RDONLY);
        if (devnull >= 0) {
            dup2(devnull, STDIN_FILENO);
            close(devnull);
        }

        // Ignore job-control signals that a background process group may receive
        signal(SIGTTIN, SIG_IGN);
        signal(SIGTTOU, SIG_IGN);

        // New process group so we can kill the whole group
        setpgid(0, 0);
        
        // Lowest priority to reduce UI lag (nice value 19 = minimum priority)
        if (nice(19) == -1 && errno != 0) { /* ignore failure */ }

        std::string cfgArg = "--config=" + cfgPath;
        std::string threadsArg = "--threads=" + std::to_string(threads);
        const char* argv[] = { xmrigPath.c_str(), cfgArg.c_str(), threadsArg.c_str(), nullptr };
        execv(xmrigPath.c_str(), const_cast<char* const*>(argv));
        _exit(127);
    }

    // Parent
    close(pipefd[1]);
    process_pid_ = pid;
    stdout_fd_   = pipefd[0];

    // Non-blocking reads
    int flags = fcntl(stdout_fd_, F_GETFL, 0);
    fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK);

    return true;
}

bool XmrigManager::isRunning() const {
    // Use state_ instead of waitpid() to avoid races with moitorProcess
    // which also calls waitpid.  state_ is atomic and always correct.
    State s = state_.load(std::memory_order_relaxed);
    return (s == State::Running || s == State::Starting);
}

double XmrigManager::getMemoryUsageMB() const {
    if (process_pid_ <= 0) return 0.0;
#ifdef __APPLE__
    // macOS: use ps to read RSS for xmrig process
    char cmd[128];
    snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d 2>/dev/null", process_pid_);
    FILE* fp = popen(cmd, "r");
    if (!fp) return 0.0;
    char line[64];
    double mb = 0.0;
    if (fgets(line, sizeof(line), fp)) {
        long rss_kb = atol(line);
        if (rss_kb > 0) mb = static_cast<double>(rss_kb) / 1024.0;
    }
    pclose(fp);
    return mb;
#else
    char path[64];
    snprintf(path, sizeof(path), "/proc/%d/statm", process_pid_);
    FILE* fp = fopen(path, "r");
    if (!fp) return 0.0;
    long dummy = 0, pages = 0;
    // statm: size resident shared text lib data dt
    // We want resident (2nd field)
    if (fscanf(fp, "%ld %ld", &dummy, &pages) != 2) pages = 0;
    fclose(fp);
    long pageSize = sysconf(_SC_PAGESIZE);
    return static_cast<double>(pages * pageSize) / (1024.0 * 1024.0);
#endif
}

void XmrigManager::drainOutput() {
    if (stdout_fd_ < 0) return;
    char buf[4096];
    while (true) {
        ssize_t n = read(stdout_fd_, buf, sizeof(buf));
        if (n > 0) {
            std::lock_guard<std::mutex> lk(output_mutex_);
            appendOutput(buf, (size_t)n);
        } else {
            break;
        }
    }
}

#endif // platform

// ============================================================================
// Output management
// ============================================================================

void XmrigManager::appendOutput(const char* data, size_t len) {
    // Caller must hold output_mutex_
    static constexpr size_t MAX_OUTPUT = 1024 * 1024; // 1 MB cap
    process_output_.append(data, len);
    if (process_output_.size() > MAX_OUTPUT) {
        // Trim from the front at a newline boundary
        size_t cut = process_output_.size() - MAX_OUTPUT;
        auto pos = process_output_.find('\n', cut);
        if (pos != std::string::npos)
            process_output_.erase(0, pos + 1);
        else
            process_output_.erase(0, cut);
    }
}

std::vector<std::string> XmrigManager::getRecentLines(int maxLines) const {
    std::lock_guard<std::mutex> lk(output_mutex_);
    std::vector<std::string> lines;
    if (process_output_.empty()) return lines;

    // Walk backwards collecting lines
    size_t end = process_output_.size();
    while ((int)lines.size() < maxLines && end > 0) {
        size_t nl = process_output_.rfind('\n', end - 1);
        if (nl == std::string::npos) {
            lines.push_back(process_output_.substr(0, end));
            break;
        }
        if (nl + 1 < end)
            lines.push_back(process_output_.substr(nl + 1, end - nl - 1));
        end = nl;
    }
    std::reverse(lines.begin(), lines.end());
    // Remove empty trailing line
    while (!lines.empty() && lines.back().empty())
        lines.pop_back();
    return lines;
}

// ============================================================================
// Monitor thread
// ============================================================================

void XmrigManager::monitorProcess() {
    // Wait a few seconds for xmrig HTTP API to start up before first poll
    for (int i = 0; i < 30 && !should_stop_; i++) {
        drainOutput();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    int poll_counter = 0;
    int pool_api_counter = 0;
    while (!should_stop_) {
        drainOutput();

        // Check if the child process is still alive (monitor thread only)
#ifdef _WIN32
        if (process_handle_) {
            DWORD exitCode = 0;
            if (GetExitCodeProcess(process_handle_, &exitCode) && exitCode != STILL_ACTIVE) {
                DEBUG_LOGF("[ERROR] XmrigManager: process exited (code %lu)\n", exitCode);
                state_ = State::Error;
                setLastError("xmrig process exited unexpectedly");
                break;
            }
        }
#else
        if (process_pid_ > 0) {
            int status = 0;
            pid_t ret = waitpid(process_pid_, &status, WNOHANG);
            if (ret == process_pid_ || ret < 0) {
                DEBUG_LOGF("[ERROR] XmrigManager: process exited (waitpid=%d)\n", ret);
                state_ = State::Error;
                setLastError("xmrig process exited unexpectedly");
                break;
            }
        }
#endif

        // Poll HTTP stats every ~2 seconds (20 * 100ms)
        if (++poll_counter >= 20) {
            poll_counter = 0;
            fetchStatsHttp();
        }

        // Poll pool-side stats every ~30 seconds (300 * 100ms)
        if (++pool_api_counter >= 300) {
            pool_api_counter = 0;
            fetchPoolApiStats();
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    drainOutput(); // Final drain
}

// ============================================================================
// Stats polling via HTTP API
// ============================================================================

void XmrigManager::pollStats() {
    // No-op on UI thread — stats are fetched by the monitor thread.
    // Just drain stdout so log lines stay fresh.
    drainOutput();
}

void XmrigManager::fetchStatsHttp() {
    if (state_ != State::Running) return;

    // Drain stdout while we're at it
    drainOutput();

    // Build URL
    char url[256];
    snprintf(url, sizeof(url), "http://127.0.0.1:%d/2/summary", api_port_);

    std::string responseData;
    CURL* curl = curl_easy_init();
    if (!curl) {
        DEBUG_LOGF("[WARN] XmrigManager::pollStats: curl_easy_init failed\n");
        return;
    }

    // Set up auth header
    std::string authHeader = "Authorization: Bearer " + api_token_;
    struct curl_slist* headers = nullptr;
    headers = curl_slist_append(headers, authHeader.c_str());

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 2000L);       // 2s timeout — generous for loaded system
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 1000L); // 1s connect timeout
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);            // Avoid signal issues in threads

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        static int s_fail_count = 0;
        if (++s_fail_count <= 5 || s_fail_count % 30 == 0) {
            DEBUG_LOGF("[WARN] XmrigManager::pollStats: curl failed (%d): %s  url=%s\n",
                    s_fail_count, curl_easy_strerror(res), url);
        }
        return;
    }

    try {
        json resp = json::parse(responseData);

        std::lock_guard<std::mutex> lk(stats_mutex_);

        // Miner version (top-level in /2/summary) — lets the UI show the actually
        // running miner's version even when no release tag was persisted (bundled miner).
        if (resp.contains("version") && resp["version"].is_string())
            stats_.version = resp["version"].get<std::string>();

        if (resp.contains("hashrate") && resp["hashrate"].contains("total")) {
            auto& total = resp["hashrate"]["total"];
            if (total.is_array() && total.size() >= 3) {
                stats_.hashrate_10s = total[0].is_null() ? 0.0 : total[0].get<double>();
                stats_.hashrate_60s = total[1].is_null() ? 0.0 : total[1].get<double>();
                stats_.hashrate_15m = total[2].is_null() ? 0.0 : total[2].get<double>();
            }
        }

        if (resp.contains("connection")) {
            auto& conn = resp["connection"];
            stats_.accepted   = conn.value("accepted", (int64_t)0);
            stats_.rejected   = conn.value("rejected", (int64_t)0);
            stats_.uptime_sec = conn.value("uptime", (int64_t)0);
            stats_.pool_diff  = conn.value("diff", 0.0);
            stats_.pool_url   = conn.value("pool", std::string{});
            stats_.algo       = conn.value("algo", std::string{});
            stats_.connected  = (stats_.uptime_sec > 0);
        }
        
        // Parse memory usage from "resources" section
        if (resp.contains("resources") && resp["resources"].contains("memory")) {
            auto& mem = resp["resources"]["memory"];
            stats_.memory_free  = mem.value("free", (int64_t)0);
            stats_.memory_total = mem.value("total", (int64_t)0);
            stats_.memory_used  = mem.value("resident_set_memory", (int64_t)0);
        }
        
        // Parse active thread count from hashrate.threads array
        if (resp.contains("hashrate") && resp["hashrate"].contains("threads")) {
            auto& threads = resp["hashrate"]["threads"];
            if (threads.is_array()) {
                stats_.threads_active = static_cast<int>(threads.size());
            }
        } else if (resp.contains("cpu") && resp["cpu"].contains("threads")) {
            // Fallback: get from cpu section
            stats_.threads_active = resp["cpu"].value("threads", 0);
        }
    } catch (...) {
        // Malformed JSON — ignore, retry next poll
    }
}

// ============================================================================
// Pool-side stats (hashrate reported by the pool)
// ============================================================================

void XmrigManager::fetchPoolApiStats() {
    if (state_ != State::Running || pool_host_.empty()) return;

    // Resolve the stats endpoint + JSON schema for this pool. Known pools carry their
    // own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
    // /api/pools); unknown/custom hosts fall back to the .is convention.
    const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
    // SSRF guard: for an UNKNOWN (user-typed) pool host, don't let the wallet issue a background GET to a
    // loopback/private/link-local/single-label target — those aren't public mining pools, and a
    // paste-a-pool-config lure could otherwise point us at an internal host. Known pools use their trusted
    // registry statsUrl and are exempt. (M-09)
    if (!known && hostLooksInternal(pool_host_)) return;
    const std::string url = known ? known->statsUrl
                                  : ("https://" + pool_host_ + "/api/stats");

    std::string responseData;
    CURL* curl = curl_easy_init();
    if (!curl) return;

    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 5000L);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    // pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents.
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; ObsidianDragon)");
    curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");

    CURLcode res = curl_easy_perform(curl);
    curl_easy_cleanup(curl);
    if (res != CURLE_OK) return;

    bool ok = false;
    const double poolHR = util::parsePoolHashrate(
        known ? known->schema : util::PoolStatsSchema::DragonXIs,
        responseData, known ? known->miningcorePoolId : std::string{}, ok);
    if (!ok) return;

    std::lock_guard<std::mutex> lk(stats_mutex_);
    stats_.pool_hashrate = poolHR;
}

// ============================================================================
// Installed-miner version detection (`<binary> --version`, cached)
// ============================================================================

namespace {
std::mutex        g_installed_ver_mutex;
std::string       g_installed_ver;
std::atomic<bool> g_ver_detect_started{false};

// Extract the first "D.D[.D...]" version token from `--version` output (skips the
// build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none.
std::string parseMinerVersion(const std::string& out)
{
    for (size_t i = 0; i < out.size(); ++i) {
        if (std::isdigit(static_cast<unsigned char>(out[i]))) {
            size_t j = i;
            int dots = 0;
            while (j < out.size() &&
                   (std::isdigit(static_cast<unsigned char>(out[j])) || out[j] == '.')) {
                if (out[j] == '.') ++dots;
                ++j;
            }
            if (dots >= 1 && (j - i) >= 3) {
                // Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"),
                // matching what the running miner's API reports.
                size_t end = j;
                if (end < out.size() && out[end] == '-') {
                    size_t k = end + 1;
                    while (k < out.size() && std::isalnum(static_cast<unsigned char>(out[k]))) ++k;
                    if (k > end + 1) end = k;
                }
                return out.substr(i, end - i);
            }
            i = j;
        }
    }
    return {};
}
} // namespace

void XmrigManager::startVersionDetection()
{
    if (g_ver_detect_started.exchange(true)) return;  // one-shot
    std::thread([]() {
        const std::string bin = findXmrigBinary();
        std::string ver;
        // Don't hand a path containing shell/cmd metacharacters to popen()'s shell — bin is normally an
        // app-controlled path, but this closes command injection if it ever isn't. (M-10)
        // Reject only chars that stay shell-special INSIDE the double-quotes we wrap bin in ("\"" + bin + "\"")
        // on cmd.exe or /bin/sh. Parens are inert when quoted, so they're excluded — otherwise common Windows
        // paths like "C:\Program Files (x86)\..." would be rejected and version detection would silently fail. (M-10)
        const bool binShellSafe =
            !bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
        if (binShellSafe) {
            // Windowless capture (mergeStderr: xmrig may print --version to stderr) — never flashes.
            const std::string cmd = "\"" + bin + "\" --version";
            const std::string out = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/true);
            if (!out.empty()) ver = parseMinerVersion(out);
        }
        std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
        g_installed_ver = ver;
    }).detach();
}

std::string XmrigManager::installedVersion()
{
    std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
    return g_installed_ver;
}

} // namespace daemon
} // namespace dragonx
