// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.

#include "embedded_daemon.h"
#include "../config/version.h"
#include "../resources/embedded_resources.h"
#include "../util/platform.h"

#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <chrono>
#include <cstring>
#include <algorithm>
#include <vector>

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

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <psapi.h>
#include <tlhelp32.h>
#include <shlobj.h>
#include <iphlpapi.h>
#else
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#ifdef __APPLE__
#include <sys/sysctl.h>
#endif
#endif

namespace fs = std::filesystem;

namespace dragonx {
namespace daemon {

EmbeddedDaemon::EmbeddedDaemon() = default;

EmbeddedDaemon::~EmbeddedDaemon()
{
    stop(3000);  // Wait up to 3 seconds for clean shutdown
}

std::string EmbeddedDaemon::findDaemonBinary()
{
    // Get the directory where the wallet binary is located
    std::string exe_dir;
#ifdef _WIN32
    char exe_path[MAX_PATH];
    GetModuleFileNameA(NULL, exe_path, MAX_PATH);
    exe_dir = fs::path(exe_path).parent_path().string();
#elif defined(__APPLE__)
    exe_dir = util::Platform::getExecutableDirectory();
#else
    char exe_path[4096];
    ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
    if (len != -1) {
        exe_path[len] = '\0';
        exe_dir = fs::path(exe_path).parent_path().string();
    }
#endif

    // ---------------------------------------------------------------
    // 1. Check wallet's own directory first — allows bundling binaries
    //    alongside the wallet executable (highest priority).
    // ---------------------------------------------------------------
    if (!exe_dir.empty()) {
#ifdef _WIN32
        // Check wallet's own directory for manually placed binaries
        std::vector<std::string> localPaths = {
            exe_dir + "\\dragonxd.exe",
        };
#else
        std::vector<std::string> localPaths = {
            exe_dir + "/dragonxd",
        };
#endif
        for (const auto& path : localPaths) {
            if (fs::exists(path)) {
                DEBUG_LOGF("[INFO] Found daemon in wallet directory: %s\n", path.c_str());
                return path;
            }
        }
    }

    // ---------------------------------------------------------------
    // 2. Check if we have an embedded daemon that needs extraction
    // ---------------------------------------------------------------
    std::string embeddedPath = resources::getDaemonPath();
    if (!embeddedPath.empty() && fs::exists(embeddedPath)) {
        DEBUG_LOGF("[INFO] Using extracted daemon at: %s\n", embeddedPath.c_str());
        return embeddedPath;
    }
    
    // ---------------------------------------------------------------
    // 3. Search additional well-known locations
    // ---------------------------------------------------------------
    // IMPORTANT: Always prefer dragonxd.exe directly over .bat wrappers.
    // Using .bat files causes issues because cmd.exe exits immediately
    // while dragonxd.exe continues running, making process monitoring fail.
    std::vector<std::string> search_paths;
    
#ifdef _WIN32
    // Parent directory
    if (!exe_dir.empty()) {
        search_paths.push_back(exe_dir + "\\..\\dragonxd.exe");
    }
    search_paths.push_back("C:\\Program Files\\DragonX\\dragonxd.exe");
#else
    if (!exe_dir.empty()) {
        search_paths.push_back(exe_dir + "/../dragonxd");
    }
    
    // Standard Linux locations
    search_paths.push_back("/usr/local/bin/dragonxd");
    search_paths.push_back("/usr/bin/dragonxd");
    
    // Home directory
    const char* home = getenv("HOME");
    if (home) {
        search_paths.push_back(std::string(home) + "/dragonx/src/dragonxd");
        search_paths.push_back(std::string(home) + "/bin/dragonxd");
    }
    
#ifdef __APPLE__
    // macOS app bundle
    search_paths.push_back("/Applications/DragonX.app/Contents/MacOS/dragonxd");
#endif
#endif
    
    // Check each path
    for (const auto& path : search_paths) {
        if (fs::exists(path)) {
            DEBUG_LOGF("[INFO] Found daemon launcher at: %s\n", path.c_str());
            return path;
        }
    }
    
    DEBUG_LOGF("[ERROR] Daemon launcher not found in any standard location\n");
    return "";
}

std::vector<std::string> EmbeddedDaemon::getChainParams()
{
    // DragonX chain parameters.
    // On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
    // On Linux, -printtoconsole is used for pipe-based output capture.
    // Auto-detect a reasonable -dbcache based on available physical RAM.
    // Default LevelDB cache is small (~450MB); larger caches improve sync
    // performance and reduce disk I/O — especially on macOS with APFS.
    std::string dbcache_arg = "-dbcache=450";
    {
#ifdef __APPLE__
        // sysctl hw.memsize returns total physical RAM in bytes
        int64_t memsize = 0;
        size_t len = sizeof(memsize);
        if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
            int totalMB = static_cast<int>(memsize / (1024 * 1024));
            // Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
            int cache = std::max(450, std::min(4096, totalMB / 8));
            dbcache_arg = "-dbcache=" + std::to_string(cache);
        }
#elif defined(__linux__)
        long pages = sysconf(_SC_PHYS_PAGES);
        long page_size = sysconf(_SC_PAGE_SIZE);
        if (pages > 0 && page_size > 0) {
            int totalMB = static_cast<int>((static_cast<int64_t>(pages) * page_size) / (1024 * 1024));
            int cache = std::max(450, std::min(4096, totalMB / 8));
            dbcache_arg = "-dbcache=" + std::to_string(cache);
        }
#elif defined(_WIN32)
        MEMORYSTATUSEX memInfo;
        memInfo.dwLength = sizeof(memInfo);
        if (GlobalMemoryStatusEx(&memInfo)) {
            int totalMB = static_cast<int>(memInfo.ullTotalPhys / (1024 * 1024));
            int cache = std::max(450, std::min(4096, totalMB / 8));
            dbcache_arg = "-dbcache=" + std::to_string(cache);
        }
#endif
        DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
    }

    return {
        "-tls=only",
#ifndef _WIN32
        "-printtoconsole",
#endif
        "-clientname=ObsidianDragon",
        "-ac_name=DRAGONX",
        "-ac_algo=randomx",
        "-ac_halving=3500000",
        "-ac_reward=300000000",
        "-ac_blocktime=36",
        "-ac_private=1",
        // Seeds: seed.dragonx.is is a round-robin A record over the live seed set (self-updates without
        // a wallet release), with node1/node5 as static fallbacks — mirrors the daemon's own vSeeds.
        // Plain -addnode hostname resolution works on EVERY daemon version, and is load-bearing for
        // pre-1.3.0 daemons whose built-in peer discovery was broken (they rely on these to find peers).
        "-addnode=seed.dragonx.is",
        "-addnode=node1.dragonx.is",
        "-addnode=node5.dragonx.is",
        "-experimentalfeatures",
        "-developerencryptwallet",
        // Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be
        // exported (z_exportmnemonic) and is portable to SDXLite/ObsidianDragonLite.
        // The daemon reads this ONLY inside GenerateNewSeed() when a wallet has no seed
        // yet, so it is inert on existing wallets — safe to pass unconditionally.
        "-usemnemonic=1",
        dbcache_arg
    };
}

void EmbeddedDaemon::setState(State s, const std::string& message)
{
    state_ = s;
    if (!message.empty() && s == State::Error) {
        std::lock_guard<std::mutex> lk(error_mutex_);   // dedicated mutex — never taken with output_mutex_ held
        last_error_ = message;
    }

    if (state_callback_) {
        state_callback_(s, message);
    }
}

// Cap process_output_ to prevent unbounded memory growth.
// Keeps the most recent MAX_OUTPUT_BYTES, trimming at a newline boundary.
static constexpr size_t MAX_OUTPUT_BYTES = 1024 * 1024; // 1 MB

void EmbeddedDaemon::appendOutput(const char* data, size_t len)
{
    // Caller must hold output_mutex_
    process_output_.append(data, len);
    if (process_output_.size() > MAX_OUTPUT_BYTES + MAX_OUTPUT_BYTES / 4) {
        // Trim to MAX_OUTPUT_BYTES, cutting at a newline boundary
        size_t trim_to = process_output_.size() - MAX_OUTPUT_BYTES;
        size_t nl = process_output_.find('\n', trim_to);
        if (nl != std::string::npos && nl < process_output_.size() - 1) {
            process_output_.erase(0, nl + 1);
        } else {
            process_output_.erase(0, trim_to);
        }
    }
}

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

    // Walk backwards collecting up to maxLines newline-delimited lines
    size_t end = out.size();
    // Skip trailing newline
    if (end > 0 && out[end - 1] == '\n') --end;

    for (int i = 0; i < maxLines && end > 0; ++i) {
        size_t nl = out.rfind('\n', end - 1);
        size_t start = (nl == std::string::npos) ? 0 : nl + 1;
        std::string line = out.substr(start, end - start);
        if (!line.empty()) lines.push_back(std::move(line));
        end = (nl == std::string::npos) ? 0 : nl;
    }
    // Reverse so oldest is first
    std::reverse(lines.begin(), lines.end());
    return lines;
}

// Identify what process owns a given TCP port.
// Returns a string like "PID 1234 (dragonxd.exe)" or "unknown" on failure.
static std::string getPortOwnerInfo(int port)
{
#ifdef _WIN32
    DWORD size = 0;
    // First call to get required buffer size
    GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
    if (size == 0) return "unknown (GetExtendedTcpTable size query failed)";
    std::vector<BYTE> buf(size);
    DWORD ret = GetExtendedTcpTable(buf.data(), &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
    if (ret != NO_ERROR) return "unknown (GetExtendedTcpTable failed, error " + std::to_string(ret) + ")";
    auto* table = reinterpret_cast<MIB_TCPTABLE_OWNER_PID*>(buf.data());
    DWORD ownerPid = 0;
    for (DWORD i = 0; i < table->dwNumEntries; i++) {
        auto& row = table->table[i];
        int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
        // Match port in LISTEN state (MIB_TCP_STATE_LISTEN = 2)
        if (rowPort == port && row.dwState == MIB_TCP_STATE_LISTEN) {
            ownerPid = row.dwOwningPid;
            break;
        }
    }
    if (ownerPid == 0) {
        // Maybe it's in ESTABLISHED or another state from our connect probe — try any state
        for (DWORD i = 0; i < table->dwNumEntries; i++) {
            auto& row = table->table[i];
            int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
            if (rowPort == port && row.dwOwningPid != 0) {
                ownerPid = row.dwOwningPid;
                break;
            }
        }
    }
    if (ownerPid == 0) return "unknown (no PID found for port " + std::to_string(port) + ")";
    // Resolve PID to process name
    std::string procName = "<unknown>";
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snap != INVALID_HANDLE_VALUE) {
        PROCESSENTRY32 pe;
        pe.dwSize = sizeof(pe);
        if (Process32First(snap, &pe)) {
            do {
                if (pe.th32ProcessID == ownerPid) {
                    procName = pe.szExeFile;
                    break;
                }
            } while (Process32Next(snap, &pe));
        }
        CloseHandle(snap);
    }
    return "PID " + std::to_string(ownerPid) + " (" + procName + ")";
#else
    // Linux: parse /proc/net/tcp to find the inode, then scan /proc/*/fd
    FILE* fp = fopen("/proc/net/tcp", "r");
    if (!fp) return "unknown (cannot read /proc/net/tcp)";
    char line[512];
    unsigned long inode = 0;
    while (fgets(line, sizeof(line), fp)) {
        unsigned int localPort, state;
        unsigned long lineInode;
        if (sscanf(line, " %*d: %*X:%X %*X:%*X %X %*X:%*X %*X:%*X %*X %*u %*u %lu",
                   &localPort, &state, &lineInode) == 3) {
            if (static_cast<int>(localPort) == port && state == 0x0A) { // 0x0A = LISTEN
                inode = lineInode;
                break;
            }
        }
    }
    fclose(fp);
    if (inode == 0) return "unknown (no listener found for port " + std::to_string(port) + ")";
    // Scan /proc/*/fd/* for the matching inode
    for (const auto& entry : fs::directory_iterator("/proc")) {
        if (!entry.is_directory()) continue;
        std::string pidStr = entry.path().filename().string();
        if (pidStr.empty() || !std::isdigit(pidStr[0])) continue;
        std::string fdDir = "/proc/" + pidStr + "/fd";
        try {
            for (const auto& fdEntry : fs::directory_iterator(fdDir)) {
                char target[512];
                ssize_t len = readlink(fdEntry.path().c_str(), target, sizeof(target) - 1);
                if (len > 0) {
                    target[len] = '\0';
                    std::string t(target);
                    if (t.find("socket:[" + std::to_string(inode) + "]") != std::string::npos) {
                        // Found the PID, now get the process name
                        std::string commPath = "/proc/" + pidStr + "/comm";
                        FILE* cf = fopen(commPath.c_str(), "r");
                        std::string procName = "<unknown>";
                        if (cf) {
                            char name[256];
                            if (fgets(name, sizeof(name), cf)) {
                                procName = name;
                                while (!procName.empty() && procName.back() == '\n') procName.pop_back();
                            }
                            fclose(cf);
                        }
                        return "PID " + pidStr + " (" + procName + ")";
                    }
                }
            }
        } catch (...) { /* permission denied — skip */ }
    }
    return "unknown (inode " + std::to_string(inode) + " found but no matching PID)";
#endif
}

// Check if a TCP port is already in use (something is LISTENING). The daemon binds BOTH 127.0.0.1 (IPv4)
// and ::1 (IPv6); during shutdown one can linger after the other releases (and a "Binding RPC on ::1 …
// failed" is fatal to a fresh start), so we treat the port as in use if EITHER localhost family has it.
static bool isPortInUse(int port)
{
#ifdef _WIN32
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
    bool inUse = false;
    {   // IPv4 127.0.0.1
        SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (sock != INVALID_SOCKET) {
            struct sockaddr_in addr; memset(&addr, 0, sizeof(addr));
            addr.sin_family = AF_INET;
            addr.sin_port = htons(static_cast<u_short>(port));
            addr.sin_addr.s_addr = inet_addr("127.0.0.1");
            if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
            closesocket(sock);
        }
    }
    if (!inUse) {   // IPv6 ::1
        SOCKET sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
        if (sock != INVALID_SOCKET) {
            struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr));
            addr.sin6_family = AF_INET6;
            addr.sin6_port = htons(static_cast<u_short>(port));
            addr.sin6_addr = in6addr_loopback;   // ::1 — avoids inet_pton's _WIN32_WINNT gating on mingw
            if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
            closesocket(sock);
        }
    }
    WSACleanup();
    return inUse;
#else
    // On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp{,6} to avoid creating sockets. The
    // parse is family-agnostic: %*X skips the local IP (8 hex for v4, 32 for v6), %X grabs the port.
    auto scanProc = [port](const char* path) -> bool {
        FILE* fp = fopen(path, "r");
        if (!fp) return false;
        char line[512];
        unsigned int localPort, state;
        bool found = false;
        while (fgets(line, sizeof(line), fp)) {
            if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
                if (localPort == static_cast<unsigned int>(port) && state == 0x0A) { found = true; break; }
            }
        }
        fclose(fp);
        return found;
    };
    if (FILE* probe = fopen("/proc/net/tcp", "r")) {   // /proc available → authoritative LISTEN check
        fclose(probe);
        return scanProc("/proc/net/tcp") || scanProc("/proc/net/tcp6");
    }
    // Fallback (macOS): connect() probe on both loopback families.
    auto connProbe = [port](int family, const char* addr) -> bool {
        int sock = socket(family, SOCK_STREAM, 0);
        if (sock < 0) return false;
        bool ok = false;
        if (family == AF_INET) {
            struct sockaddr_in a; memset(&a, 0, sizeof(a));
            a.sin_family = AF_INET; a.sin_port = htons(static_cast<uint16_t>(port));
            a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
            ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
        } else {
            struct sockaddr_in6 a; memset(&a, 0, sizeof(a));
            a.sin6_family = AF_INET6; a.sin6_port = htons(static_cast<uint16_t>(port));
            inet_pton(AF_INET6, addr, &a.sin6_addr);
            ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
        }
        close(sock);
        return ok;
    };
    return connProbe(AF_INET, "127.0.0.1") || connProbe(AF_INET6, "::1");
#endif
}

bool EmbeddedDaemon::start(const std::string& binary_path)
{
    // Don't start if already running or starting
    if (state_ == State::Starting || state_ == State::Running) {
        DEBUG_LOGF("[INFO] Daemon already %s\n", state_ == State::Starting ? "starting" : "running");
        return true;
    }
    
    if (isRunning()) {
        DEBUG_LOGF("[INFO] Daemon process already running\n");
        return true;
    }
    
    // Check if something is already listening on the RPC port. An isolated instance (migrate-to-
    // seed) runs on its own non-default port alongside the main daemon, so it skips this bail.
    int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
    if (!skip_port_check_ && isPortInUse(rpc_port)) {
        std::string owner = getPortOwnerInfo(rpc_port);
        VERBOSE_LOGF("[INFO] Port %d is already in use by %s — external daemon detected, will connect to it.\\n", rpc_port, owner.c_str());
        external_daemon_detected_ = true;
        // Don't set Error — the wallet will connect to the running daemon.
        setState(State::Stopped, "External daemon detected on port " + std::string(DRAGONX_DEFAULT_RPC_PORT) + " (owned by " + owner + ")");
        return false;
    }
    external_daemon_detected_ = false;

    // A previous dragonxd can release the RPC port well before it releases the datadir
    // .lock — a graceful shutdown can take up to ~90s (see isDaemonProcessRunning). Starting
    // into a still-held lock spawns a process that dies instantly with "Cannot obtain a lock
    // on data directory"; the crash monitor reports that generically and, three times in
    // ~12s, that is enough to trip the 3-strike restart cap before the lock's ~90s life
    // elapses. Gate on the process actually still being alive, with a SHORT bounded wait
    // (not the full ~90s — start() runs on the UI thread). Isolated starts (migrate-to-seed:
    // skip_port_check_ / -datadir override) are exempt; they run their own datadir+port.
    {
        constexpr int kDatadirLockWaitPollMs   = 100;
        constexpr int kDatadirLockWaitMaxPolls = 3;   // ~300ms total, breaks early on exit
        bool stillRunning = false;
        if (!skip_port_check_ && override_datadir_.empty()) {
            stillRunning = isDaemonProcessRunning();
            for (int i = 0; stillRunning && i < kDatadirLockWaitMaxPolls; ++i) {
                std::this_thread::sleep_for(std::chrono::milliseconds(kDatadirLockWaitPollMs));
                stillRunning = isDaemonProcessRunning();
            }
        }
        const StartLockGateDecision gate =
            evaluateDatadirLockGate(skip_port_check_, !override_datadir_.empty(), stillRunning);
        if (!gate.proceed) {
            VERBOSE_LOGF("[INFO] %s\n", gate.errorMessage);
            setState(State::Error, gate.errorMessage);
            return false;
        }
    }
    
    setState(State::Starting, "Looking for dragonxd binary...");
    
    std::string daemon_path = binary_path;
    if (daemon_path.empty()) {
        daemon_path = findDaemonBinary();
    }
    
    if (daemon_path.empty() || !fs::exists(daemon_path)) {
        DEBUG_LOGF("[ERROR] dragonxd binary not found\\n");
        setState(State::Error, "dragonxd binary not found.\n\nTo use embedded daemon, place dragonxd.exe (or dragonxd.bat) in the same directory as the wallet.\n\nAlternatively, start dragonxd manually and the wallet will connect to it.");
        return false;
    }
    
    DEBUG_LOGF("[INFO] Starting dragonxd from: %s\\n", daemon_path.c_str());
    
    auto args = getChainParams();

    // Append debug logging flags from user settings
    for (const auto& cat : debug_categories_) {
        args.push_back("-debug=" + cat);
    }

    // Append max connections if configured (0 = daemon default)
    if (max_connections_ > 0) {
        args.push_back("-maxconnections=" + std::to_string(max_connections_));
    }

    // Host a RandomX stratum pool from this node (-stratum). Only v1.3.0+ daemons implement it; older
    // ones ignore the unknown flag (no fatal arg check), and the Settings toggle is gated on daemon
    // version, so this is only enabled against a daemon that supports it. Without -stratumallowip the
    // daemon serves loopback only (safe default); a subnet opens it to that LAN.
    if (stratum_enabled_) {
        args.push_back("-stratum");
        if (!stratum_allowip_.empty()) args.push_back("-stratumallowip=" + stratum_allowip_);
    }

    // Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
    // non-default name so the common case's command line is unchanged; skip during an isolated
    // start (seed migration manages its own throwaway wallet).
    if (!wallet_file_.empty() && wallet_file_ != "wallet.dat" && override_datadir_.empty()) {
        DEBUG_LOGF("[INFO] Loading wallet file: %s\n", wallet_file_.c_str());
        args.push_back("-wallet=" + wallet_file_);
    }

    // Add wallet-repair flag if requested (one-shot). Precedence: salvage > zap > rescan; each implies a
    // rescan in the daemon, so we don't stack them.
    if (salvage_on_next_start_.exchange(false)) {
        // -salvagewallet recovers readable keypairs from a corrupt wallet.dat; the daemon then implies -rescan.
        DEBUG_LOGF("[INFO] Adding -salvagewallet flag to recover a corrupt wallet\n");
        args.push_back("-salvagewallet");
        zap_on_next_start_.store(false);
        rescan_on_next_start_.store(false);
    } else if (zap_on_next_start_.exchange(false)) {
        // -zapwallettxes=2 wipes all wallet tx/note records and rebuilds them from the chain (implies -rescan).
        DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
        args.push_back("-zapwallettxes=2");
        rescan_on_next_start_.store(false);  // implied by zap; avoid redundant -rescan
    } else if (rescan_on_next_start_.exchange(false)) {
        // Add -rescan flag if requested (one-shot)
        DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
        args.push_back("-rescan");
    }

    // -reindex rebuilds the block index + chainstate from the raw blocks (fixes an unreadable/format-
    // mismatched block DB). It's about the CHAIN, not the wallet, so it's independent of the wallet-repair
    // chain above (and implies its own wallet rescan). One-shot, consumed here.
    if (reindex_on_next_start_.exchange(false)) {
        DEBUG_LOGF("[INFO] Adding -reindex flag to rebuild the block database from raw blocks\n");
        args.push_back("-reindex");
    }

    // One-shot isolated-datadir override (migrate-to-seed flow): run this start against a
    // throwaway datadir, plus any extra args (e.g. -connect=0). Consumed here so later starts
    // revert to the normal datadir. The datadir's basename MUST be the assetchain name (DRAGONX)
    // or the daemon mis-resolves its conf/port; it reads <datadir>/DRAGONX.conf automatically, so
    // no -conf is passed (an explicit -conf confuses the Komodo/Hush path resolution).
    if (!override_datadir_.empty()) {
        DEBUG_LOGF("[INFO] Isolated start override: -datadir=%s\n", override_datadir_.c_str());
        args.push_back("-datadir=" + override_datadir_);
    }
    for (const auto& a : override_extra_args_) args.push_back(a);
    override_datadir_.clear();
    override_extra_args_.clear();

    if (!startProcess(daemon_path, args)) {
        // startProcess() sets a precise last_error_ (e.g. "dragonxd could not be executed:
        // ... not executable or wrong architecture"). Surface THAT via setState — which also
        // stores the Error message into last_error_ — instead of clobbering it with a generic
        // string that would then be all getLastError()/the UI ever sees.
        std::string detail = last_error_.empty() ? std::string("Failed to start dragonxd process")
                                                  : last_error_;
        DEBUG_LOGF("[ERROR] %s\n", detail.c_str());
        setState(State::Error, detail);
        return false;
    }
    
    setState(State::Running, "dragonxd started");
    DEBUG_LOGF("[INFO] dragonxd process started successfully\\n");
    
    // Start monitor thread (if not already running)
    should_stop_ = false;
    if (!monitor_thread_.joinable()) {
        monitor_thread_ = std::thread(&EmbeddedDaemon::monitorProcess, this);
    }
    
    return true;
}

#ifdef _WIN32

// Forward declaration — defined after startProcess
static DWORD findProcessByName(const char* name);

// Quote a single argument per the CommandLineToArgvW rules (MSDN) so a value containing a space or a
// quote is delivered as ONE argv token to the daemon instead of splitting/corrupting argv (L-02).
static std::string quoteWinArg(const std::string& arg) {
    if (!arg.empty() && arg.find_first_of(" \t\n\v\"") == std::string::npos) return arg;
    std::string out = "\"";
    for (size_t i = 0; ; ++i) {
        size_t nbs = 0;
        while (i < arg.size() && arg[i] == '\\') { ++nbs; ++i; }
        if (i == arg.size()) { out.append(nbs * 2, '\\'); break; }
        if (arg[i] == '"')   { out.append(nbs * 2 + 1, '\\'); out.push_back('"'); }
        else                 { out.append(nbs, '\\'); out.push_back(arg[i]); }
    }
    out.push_back('"');
    return out;
}

bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector<std::string>& args)
{
    // Build command line (binary path always quoted; each arg quoted/escaped per Windows rules — L-02)
    std::string cmd = "\"" + binary_path + "\"";
    for (const auto& arg : args) {
        cmd += " " + quoteWinArg(arg);
    }
    DEBUG_LOGF("[INFO] Starting daemon: %s\n", cmd.c_str());
    
    // Set working directory to the daemon binary's directory
    std::string work_dir = fs::path(binary_path).parent_path().string();
    if (work_dir.empty()) {
        work_dir = ".";
    }
    DEBUG_LOGF("[INFO] Working directory: %s\n", work_dir.c_str());
    
    // Store launch info for diagnostics (shown in error messages if crash occurs)
    launch_cmd_ = cmd;
    launch_binary_ = binary_path;
    launch_workdir_ = work_dir;
    
    // Log binary file size for corruption detection
    {
        std::error_code ec;
        auto fsize = fs::file_size(binary_path, ec);
        if (!ec) {
            DEBUG_LOGF("[INFO] Daemon binary size: %llu bytes\n", (unsigned long long)fsize);
        }
    }
    
    // Determine debug.log path for output tailing.
    // The daemon writes to %APPDATA%\Hush\DRAGONX\debug.log when -printtoconsole is NOT used.
    {
        char appdata[MAX_PATH];
        if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata))) {
            debug_log_path_ = std::string(appdata) + "\\Hush\\DRAGONX\\debug.log";
        } else {
            const char* home = getenv("APPDATA");
            if (home) {
                debug_log_path_ = std::string(home) + "\\Hush\\DRAGONX\\debug.log";
            }
        }
        
        // Record current file size so we only read NEW output from this launch
        if (!debug_log_path_.empty() && fs::exists(debug_log_path_)) {
            debug_log_offset_ = static_cast<size_t>(fs::file_size(debug_log_path_));
        } else {
            debug_log_offset_ = 0;
        }
        DEBUG_LOGF("[INFO] Will tail daemon output from: %s (offset %zu)\n", 
                debug_log_path_.c_str(), debug_log_offset_);
    }
    
    // Launch daemon windowless. Use CREATE_NO_WINDOW (NOT CREATE_NEW_CONSOLE): CREATE_NEW_CONSOLE
    // allocates a console window that briefly flashes on screen before SW_HIDE can hide it, which is
    // visible as a console-window flash on wallet launch. CREATE_NO_WINDOW gives the console child no
    // window at all (same approach as the xmrig launcher). The daemon logs to debug.log, not a console.
    // The daemon binary must NOT be in the data directory (%APPDATA%\Hush\DRAGONX)
    // — it must be in <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
    STARTUPINFOA si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;
    ZeroMemory(&pi, sizeof(pi));

    char* cmd_line = _strdup(cmd.c_str());
    BOOL success = CreateProcessA(
        NULL,
        cmd_line,
        NULL,
        NULL,
        FALSE,
        CREATE_NO_WINDOW,
        NULL,
        work_dir.c_str(),
        &si,
        &pi
    );
    free(cmd_line);
    
    if (!success) {
        DWORD err = GetLastError();
        char errBuf[256] = {0};
        FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                       NULL, err, 0, errBuf, sizeof(errBuf), NULL);
        last_error_ = "CreateProcess failed with error " + std::to_string(err) + ": " + errBuf;
        DEBUG_LOGF("[ERROR] CreateProcess failed: error %lu - %s\nCommand: %s\n", err, errBuf, cmd.c_str());
        return false;
    }
    
    process_handle_ = pi.hProcess;
    CloseHandle(pi.hThread);
    
    return true;
}

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

// Find a running process by executable name, return its PID (0 if not found)
static DWORD findProcessByName(const char* name)
{
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snap == INVALID_HANDLE_VALUE) return 0;

    // Use the explicit WIDE Toolhelp API + a wide compare so this is correct regardless of the UNICODE
    // macro. (The non-suffixed PROCESSENTRY32/Process32First map to the wide variants when UNICODE is
    // defined, in which case szExeFile is WCHAR[] and an ANSI _stricmp would compare garbage and NEVER
    // match — silently making findProcessByName a no-op that returns 0 for a running process.)
    wchar_t wname[MAX_PATH];
    if (MultiByteToWideChar(CP_ACP, 0, name, -1, wname, MAX_PATH) == 0) { CloseHandle(snap); return 0; }

    PROCESSENTRY32W entry;
    entry.dwSize = sizeof(entry);

    DWORD pid = 0;
    if (Process32FirstW(snap, &entry)) {
        do {
            if (lstrcmpiW(entry.szExeFile, wname) == 0) {   // Win32 case-insensitive wide compare
                pid = entry.th32ProcessID;
                break;
            }
        } while (Process32NextW(snap, &entry));
    }
    CloseHandle(snap);
    return pid;
}

double EmbeddedDaemon::getMemoryUsageMB() const
{
    if (process_handle_ == nullptr) 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 EmbeddedDaemon::drainOutput()
{
    // Read new content from debug.log (tail-follow approach)
    if (debug_log_path_.empty()) return;
    
    HANDLE hFile = CreateFileA(debug_log_path_.c_str(), GENERIC_READ,
                                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                                NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return;
    
    // Get current file size
    LARGE_INTEGER fileSize;
    if (!GetFileSizeEx(hFile, &fileSize)) {
        CloseHandle(hFile);
        return;
    }
    
    size_t currentSize = static_cast<size_t>(fileSize.QuadPart);
    // Truncation / rotation detection. dragonxd shrinks debug.log on startup (ShrinkDebugFile keeps
    // only the tail once it has grown large), so the file can become SMALLER than where we left off.
    // Our offset was set to the PRE-shrink size at spawn, so without this reset it stays stranded ahead
    // of the freshly-truncated file and we read NOTHING for the whole session — no block height (status
    // bar shows "Block: 0") and, worse, no witness-rebuild progress, since the "Setting Initial Sapling
    // Witness …" lines the warmup progress bar parses only exist in debug.log on Windows. On a shrink,
    // restart from the new beginning; the parser is monotonic and converges as it reaches current output.
    if (currentSize < debug_log_offset_) {
        debug_log_offset_ = 0;
    }
    if (currentSize <= debug_log_offset_) {
        CloseHandle(hFile);
        return;  // No new data
    }

    // Seek to where we left off
    LARGE_INTEGER seekPos;
    seekPos.QuadPart = static_cast<LONGLONG>(debug_log_offset_);
    SetFilePointerEx(hFile, seekPos, NULL, FILE_BEGIN);
    
    // Read new content in chunks
    char buffer[4096];
    DWORD bytes_read;
    while (debug_log_offset_ < currentSize) {
        DWORD toRead = static_cast<DWORD>(std::min<size_t>(sizeof(buffer) - 1, currentSize - debug_log_offset_));
        if (!ReadFile(hFile, buffer, toRead, &bytes_read, NULL) || bytes_read == 0) break;
        
        buffer[bytes_read] = '\0';
        {
            std::lock_guard<std::mutex> lk(output_mutex_);
            appendOutput(buffer, bytes_read);
        }
        VERBOSE_LOGF("[dragonxd] %s", buffer);
        debug_log_offset_ += bytes_read;
    }
    
    CloseHandle(hFile);
}

void EmbeddedDaemon::stop(int wait_ms)
{
    should_stop_ = true;
    setState(State::Stopping, "Stopping dragonxd...");
    
    // Check if our tracked process handle is still alive
    bool tracked_alive = false;
    if (process_handle_ != nullptr) {
        DWORD exit_code;
        if (GetExitCodeProcess(process_handle_, &exit_code) && exit_code == STILL_ACTIVE) {
            tracked_alive = true;
        }
    }

    // Helper: poll-wait for a process handle, draining stdout each iteration
    auto pollWait = [&](HANDLE hProc, int ms) -> bool {
        auto start = std::chrono::steady_clock::now();
        while (true) {
            drainOutput();
            DWORD res = WaitForSingleObject(hProc, 200);
            if (res == WAIT_OBJECT_0) return true;  // exited
            auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
                std::chrono::steady_clock::now() - start).count();
            if (elapsed >= ms) return false;  // timeout
        }
    };
    
    if (tracked_alive) {
        // Our tracked process (dragonxd.exe launched directly) is still alive.
        // The RPC "stop" was already sent by the caller — wait for it to exit.
        DEBUG_LOGF("Waiting up to %d ms for tracked daemon process to exit...\n", wait_ms);
        if (!pollWait(process_handle_, wait_ms)) {
            DEBUG_LOGF("Timeout — forcing daemon termination...\n");
            TerminateProcess(process_handle_, 1);
            WaitForSingleObject(process_handle_, 2000);
        }
        drainOutput();
        CloseHandle(process_handle_);
        process_handle_ = nullptr;
    } else {
        // Tracked handle is dead (batch file case: cmd.exe already exited).
        // The real dragonxd.exe may still be running as an orphan.
        if (process_handle_ != nullptr) {
            CloseHandle(process_handle_);
            process_handle_ = nullptr;
        }
        
        // Find the real dragonxd.exe process by name
        DWORD dragonxd_pid = findProcessByName("dragonxd.exe");
        if (dragonxd_pid != 0) {
            DEBUG_LOGF("Found orphaned dragonxd.exe (PID %lu) — waiting for RPC stop to take effect...\n", dragonxd_pid);
            HANDLE hProc = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, dragonxd_pid);
            if (hProc) {
                // RPC stop was already sent — wait for graceful exit
                if (!pollWait(hProc, wait_ms)) {
                    DEBUG_LOGF("Timeout — forcing dragonxd.exe (PID %lu) termination...\n", dragonxd_pid);
                    TerminateProcess(hProc, 1);
                    WaitForSingleObject(hProc, 2000);
                }
                drainOutput();
                CloseHandle(hProc);
                DEBUG_LOGF("dragonxd.exe stopped\n");
            } else {
                DEBUG_LOGF("Could not open dragonxd.exe process (PID %lu), error %lu\n", 
                       dragonxd_pid, GetLastError());
            }
        } else {
            DEBUG_LOGF("No dragonxd.exe process found — daemon may have already exited\n");
        }
    }
    
    if (stdout_read_ != nullptr) {
        CloseHandle(stdout_read_);
        stdout_read_ = nullptr;
    }
    // Final drain of debug.log
    drainOutput();
    
    if (monitor_thread_.joinable()) {
        monitor_thread_.join();
    }
    
    setState(State::Stopped, "dragonxd stopped");
}

// Translate Windows NTSTATUS / exit codes to human-readable descriptions
static std::string translateWindowsExitCode(DWORD code) {
    switch (code) {
        case 0x80000003: return "STATUS_BREAKPOINT (0x80000003) — possible missing DLL or binary crash";
        case 0xC0000005: return "STATUS_ACCESS_VIOLATION (0xC0000005) — memory access violation";
        case 0xC0000135: return "STATUS_DLL_NOT_FOUND (0xC0000135) — required DLL not found";
        case 0xC000007B: return "STATUS_INVALID_IMAGE_FORMAT (0xC000007B) — wrong architecture or corrupt binary";
        case 0xC0000142: return "STATUS_DLL_INIT_FAILED (0xC0000142) — DLL initialization failed";
        case 0xC0000409: return "STATUS_STACK_BUFFER_OVERRUN (0xC0000409) — stack buffer overflow";
        case 0xC0000374: return "STATUS_HEAP_CORRUPTION (0xC0000374) — heap corruption detected";
        default:
            if (code > 0x80000000) {
                char buf[64];
                snprintf(buf, sizeof(buf), "exit code: 0x%08lX", code);
                return std::string(buf);
            }
            return "exit code: " + std::to_string(code);
    }
}

void EmbeddedDaemon::monitorProcess()
{
    while (!should_stop_ && isRunning()) {
        // Tail debug.log for new output
        drainOutput();
        std::this_thread::sleep_for(std::chrono::milliseconds(250));
    }
    
    // Read any remaining output after process exits
    drainOutput();
    
    // Check if process exited unexpectedly
    if (!should_stop_ && !isRunning()) {
        // Get exit code for diagnostics
        DWORD exit_code = 0;
        if (process_handle_) {
            GetExitCodeProcess(process_handle_, &exit_code);
        }
        
        crash_count_++;

        // Build informative error message with full diagnostic details
        std::string error_msg = "dragonxd exited unexpectedly (" + translateWindowsExitCode(exit_code) + ")";
        
        // Add launch diagnostics
        error_msg += "\n\nLaunch details:";
        error_msg += "\n  Binary: " + launch_binary_;
        error_msg += "\n  Working dir: " + launch_workdir_;
        error_msg += "\n  Command: " + launch_cmd_;
        
        // Check binary file size
        {
            std::error_code ec;
            auto fsize = fs::file_size(launch_binary_, ec);
            if (!ec) {
                char sizeBuf[32];
                snprintf(sizeBuf, sizeof(sizeBuf), "%llu", (unsigned long long)fsize);
                error_msg += "\n  Binary size: " + std::string(sizeBuf) + " bytes";
            } else {
                error_msg += "\n  Binary size: could not read (" + ec.message() + ")";
            }
        }
        
        // Check if debug.log has any content from this launch
        if (!debug_log_path_.empty()) {
            std::error_code ec;
            auto logSize = fs::file_size(debug_log_path_, ec);
            if (!ec) {
                size_t newBytes = (static_cast<size_t>(logSize) > debug_log_offset_) 
                    ? static_cast<size_t>(logSize) - debug_log_offset_ : 0;
                char logBuf[64];
                snprintf(logBuf, sizeof(logBuf), "%zu", newBytes);
                error_msg += "\n  debug.log new bytes: " + std::string(logBuf);
            }
        }
        
        // Include daemon output from debug.log
        {
            std::lock_guard<std::mutex> lk(output_mutex_);
            if (!process_output_.empty()) {
                // Get last ~500 chars of output for context
                std::string last_output = process_output_;
                if (last_output.size() > 500) {
                    last_output = "..." + last_output.substr(last_output.size() - 500);
                }
                error_msg += "\n\nDaemon output:\n" + last_output;
            }
        }
        
        setState(State::Error, error_msg);
    }
}

#else  // Linux/macOS

bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vector<std::string>& args)
{
    // Create pipe for stdout/stderr
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        last_error_ = "Failed to create pipe: " + std::string(strerror(errno));
        return false;
    }

    // Self-pipe used purely as an exec-success/failure handshake, separate from
    // the stdout pipe above. Both ends are close-on-exec, so a successful execv()
    // closes the write end for free (parent reads EOF); on execv() failure the
    // child writes errno here, so the parent learns synchronously instead of
    // reporting State::Running for a child that never became dragonxd. We use
    // pipe()+FD_CLOEXEC (not pipe2) because this POSIX branch is shared with
    // macOS, which has no pipe2().
    int execpipe[2];
    if (pipe(execpipe) == -1) {
        last_error_ = "Failed to create exec-status pipe: " + std::string(strerror(errno));
        close(pipefd[0]);
        close(pipefd[1]);
        return false;
    }
    fcntl(execpipe[0], F_SETFD, FD_CLOEXEC);
    fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
    
    pid_t pid = fork();
    if (pid == -1) {
        last_error_ = "Fork failed: " + std::string(strerror(errno));
        close(pipefd[0]);
        close(pipefd[1]);
        close(execpipe[0]);
        close(execpipe[1]);
        return false;
    }
    
    if (pid == 0) {
        // Child process
        close(pipefd[0]);   // Close read end of the stdout pipe
        close(execpipe[0]); // Child only writes the exec-status pipe
        
        // Put child in its own process group so we can kill the entire
        // group later (including dragonxd spawned by a wrapper script).
        // Without this, SIGTERM only kills the shell, leaving dragonxd orphaned.
        setpgid(0, 0);
        
        // Change to the daemon binary's directory so dragonxd can find
        // sapling params via its PWD search path (same as CreateProcessA
        // lpCurrentDirectory on Windows).
        {
            std::string daemon_dir = fs::path(binary_path).parent_path().string();
            if (!daemon_dir.empty()) {
                if (chdir(daemon_dir.c_str()) != 0) {
                    fprintf(stderr, "chdir(%s) failed: %s\n", daemon_dir.c_str(), strerror(errno));
                }
            }
        }
        
        // Redirect stdout and stderr to pipe
        dup2(pipefd[1], STDOUT_FILENO);
        dup2(pipefd[1], STDERR_FILENO);
        close(pipefd[1]);
        
        // Disable output buffering
        setenv("PYTHONUNBUFFERED", "1", 1);
        
        // Build argv
        std::vector<char*> argv;
        
        // Check if this is a shell script
        bool is_script = false;
        if (binary_path.size() >= 3) {
            std::string ext = binary_path.substr(binary_path.size() - 3);
            if (ext == ".sh" || binary_path.find("dragonxd") != std::string::npos) {
                // Check if it's a script by looking at first bytes
                FILE* f = fopen(binary_path.c_str(), "r");
                if (f) {
                    char buf[2] = {0};
                    if (fread(buf, 1, 2, f) == 2 && buf[0] == '#' && buf[1] == '!') {
                        is_script = true;
                    }
                    fclose(f);
                }
            }
        }
        
        if (is_script) {
            // Run shell scripts through bash
            argv.push_back(const_cast<char*>("/bin/bash"));
            argv.push_back(const_cast<char*>(binary_path.c_str()));
        } else {
            argv.push_back(const_cast<char*>(binary_path.c_str()));
        }
        
        for (const auto& arg : args) {
            argv.push_back(const_cast<char*>(arg.c_str()));
        }
        argv.push_back(nullptr);
        
        // Execute using execvp for PATH resolution
        if (is_script) {
            execv("/bin/bash", argv.data());
        } else {
            execv(binary_path.c_str(), argv.data());
        }
        
        // If we get here, execv() failed — the child never became dragonxd.
        // Capture errno before fprintf/strerror can clobber it, report it to
        // the parent over the exec-status pipe (EINTR-safe), then exit.
        int exec_errno = errno;
        fprintf(stderr, "execv failed: %s\n", strerror(exec_errno));
        ssize_t w;
        do {
            w = write(execpipe[1], &exec_errno, sizeof(exec_errno));
        } while (w < 0 && errno == EINTR);
        _exit(127);
    }
    
    // Parent process
    close(pipefd[1]);   // Close our copy of the stdout write end
    close(execpipe[1]); // Must close our copy, or the read() below never sees EOF

    // Exec-status handshake: EOF => execv() succeeded (its write end was closed
    // on exec); a full sizeof(int) => execv() failed and the child sent errno.
    int child_errno = 0;
    size_t got = 0;
    char* ep = reinterpret_cast<char*>(&child_errno);
    for (;;) {
        ssize_t n = read(execpipe[0], ep + got, sizeof(child_errno) - got);
        if (n == 0) break;                                   // EOF: exec succeeded
        if (n < 0) { if (errno == EINTR) continue; break; }  // other error: assume success
        got += static_cast<size_t>(n);
        if (got >= sizeof(child_errno)) break;               // full errno: exec failed
    }
    close(execpipe[0]);

    if (got >= sizeof(child_errno)) {
        // execv() never replaced the child; it fprintf'd and _exit(127)'d. Reap
        // the already-dead zombie here — monitorProcess() is only started after
        // this function returns true, so there is no competing reaper.
        close(pipefd[0]);
        int status;
        waitpid(pid, &status, 0);
        last_error_ = "dragonxd could not be executed: " + std::string(strerror(child_errno)) +
                      " — not executable or wrong architecture";
        return false;
    }

    stdout_fd_ = pipefd[0];

    // Best-effort: the child already calls setpgid(0, 0); this parent-side call
    // just closes the fork/exec race window. A failure here is not fatal to
    // startup, so we log rather than abort.
    if (setpgid(pid, pid) != 0) {
        DEBUG_LOGF("[WARN] setpgid(%d) from parent failed: %s\n", (int)pid, strerror(errno));
    }

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

    process_pid_ = pid;
    return true;
}

double EmbeddedDaemon::getMemoryUsageMB() const
{
    if (process_pid_ <= 0) return 0.0;

#ifdef __APPLE__
    // macOS: use ps to read RSS for the daemon process and its children
    // in the same process group.
    char cmd[128];
    snprintf(cmd, sizeof(cmd), "ps -o rss= -g %d 2>/dev/null", process_pid_);
    FILE* fp = popen(cmd, "r");
    if (!fp) return 0.0;
    double total_rss_mb = 0.0;
    char line[64];
    while (fgets(line, sizeof(line), fp)) {
        long rss_kb = atol(line);
        if (rss_kb > 0) total_rss_mb += static_cast<double>(rss_kb) / 1024.0;
    }
    pclose(fp);
    return total_rss_mb;
#else
    // Linux: The tracked PID is often a bash wrapper script; the real daemon
    // (dragonxd) is a child in the same process group.  Sum VmRSS for every
    // process whose PGID matches our tracked PID.
    double total_rss_mb = 0.0;

    // Iterate /proc/<pid>/stat for all numeric entries
    for (const auto& entry : fs::directory_iterator("/proc")) {
        if (!entry.is_directory()) continue;
        const std::string name = entry.path().filename().string();
        if (name.empty() || !std::isdigit(name[0])) continue;

        // Read process group from /proc/<pid>/stat  (field 5 is pgrp)
        std::string statPath = entry.path().string() + "/stat";
        FILE* sf = fopen(statPath.c_str(), "r");
        if (!sf) continue;

        char statLine[512];
        if (!fgets(statLine, sizeof(statLine), sf)) { fclose(sf); continue; }
        fclose(sf);

        // Fields in /proc/<pid>/stat are space-separated, but field 2 (comm)
        // is wrapped in parens and may contain spaces.  Skip past ')'.
        const char* p = strrchr(statLine, ')');
        if (!p) continue;
        p++; // skip ')'

        // After ')' the fields are:  state(3), ppid(4), pgrp(5), ...
        int pgrp = 0;
        char state;
        int ppid;
        if (sscanf(p, " %c %d %d", &state, &ppid, &pgrp) != 3) continue;
        if (pgrp != process_pid_) continue;

        // This process belongs to our group — read its VmRSS
        std::string statusPath = entry.path().string() + "/status";
        FILE* f = fopen(statusPath.c_str(), "r");
        if (!f) continue;
        char line[256];
        while (fgets(line, sizeof(line), f)) {
            if (strncmp(line, "VmRSS:", 6) == 0) {
                long rss_kb = 0;
                if (sscanf(line + 6, "%ld", &rss_kb) == 1)
                    total_rss_mb += static_cast<double>(rss_kb) / 1024.0;
                break;
            }
        }
        fclose(f);
    }

    return total_rss_mb;
#endif // __APPLE__ / Linux
}

bool EmbeddedDaemon::isRunning() const
{
    // Read the atomic state_ instead of calling waitpid() here. monitorProcess()
    // is the sole thread allowed to waitpid() process_pid_ during normal operation.
    // Calling waitpid() from this method too (as it used to, and this is invoked
    // from the UI thread nearly every frame) meant whichever thread reaped the
    // child's exit first consumed the status; if isRunning() won that race,
    // monitorProcess() never saw the exit, so crash_count_ / the decoded exit
    // code / the State::Error transition were all silently lost. Mirrors the
    // fix already in XmrigManager::isRunning().
    if (process_pid_ <= 0) return false;

    const State s = state_.load(std::memory_order_relaxed);
    // State::Stopping is included: stop()'s graceful/SIGTERM wait loops poll
    // isRunning() while state_ == Stopping — before the process has actually
    // terminated — and must keep seeing "alive" to wait/escalate correctly.
    return (s == State::Running || s == State::Stopping);
}

void EmbeddedDaemon::drainOutput()
{
    if (stdout_fd_ < 0) return;
    char buffer[4096];
    for (;;) {
        ssize_t n = read(stdout_fd_, buffer, sizeof(buffer) - 1);
        if (n <= 0) break;
        buffer[n] = '\0';
        {
            std::lock_guard<std::mutex> lk(output_mutex_);
            appendOutput(buffer, static_cast<size_t>(n));
        }
        VERBOSE_LOGF("[dragonxd] %s", buffer);
    }
}

void EmbeddedDaemon::stop(int wait_ms)
{
    should_stop_ = true;
    
    if (process_pid_ > 0) {
        setState(State::Stopping, "Stopping dragonxd...");
        
        // Phase 1: Wait for the daemon to exit naturally.
        // The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
        // tells the daemon to flush LevelDB, close sockets, and exit cleanly.
        // On macOS/APFS the LevelDB flush can take several seconds — we must
        // NOT send SIGTERM until the daemon has had enough time to finish.
        DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
        auto start = std::chrono::steady_clock::now();
        while (isRunning()) {
            drainOutput();

            auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
                std::chrono::steady_clock::now() - start).count();
            
            if (elapsed >= wait_ms) {
                break;
            }
            
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }

        // Phase 2: If still running, send SIGTERM and wait a further 10s.
        if (isRunning()) {
            DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_);
            kill(-process_pid_, SIGTERM);
            
            auto sigterm_start = std::chrono::steady_clock::now();
            while (isRunning()) {
                drainOutput();
                auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
                    std::chrono::steady_clock::now() - sigterm_start).count();
                if (elapsed >= 10000) {
                    // Phase 3: Force kill
                    DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
                    kill(-process_pid_, SIGKILL);
                    break;
                }
                std::this_thread::sleep_for(std::chrono::milliseconds(100));
            }
        } else {
            DEBUG_LOGF("Daemon exited cleanly after RPC stop\n");
        }

        drainOutput();  // read any remaining output
        
        // Reap the child process
        int status;
        waitpid(process_pid_, &status, 0);
        process_pid_ = 0;
    }
    
    if (stdout_fd_ >= 0) {
        close(stdout_fd_);
        stdout_fd_ = -1;
    }
    
    if (monitor_thread_.joinable()) {
        monitor_thread_.join();
    }
    
    setState(State::Stopped, "dragonxd stopped");
}

void EmbeddedDaemon::monitorProcess()
{
    char buffer[4096];
    
    while (!should_stop_) {
        // Check if process exited (non-blocking waitpid captures exit status)
        if (process_pid_ > 0) {
            int status = 0;
            pid_t result = waitpid(process_pid_, &status, WNOHANG);
            if (result == process_pid_) {
                // Process exited — drain remaining output
                drainOutput();

                crash_count_++;
                std::string exitInfo;
                if (WIFEXITED(status)) {
                    exitInfo = "exit code: " + std::to_string(WEXITSTATUS(status));
                } else if (WIFSIGNALED(status)) {
                    exitInfo = "killed by signal " + std::to_string(WTERMSIG(status));
                } else {
                    exitInfo = "unknown exit status";
                }
                process_pid_ = 0;
                setState(State::Error, "dragonxd exited unexpectedly (" + exitInfo + ")");
                return;
            }
        }

        // Read process output (non-blocking)
        ssize_t bytes_read = read(stdout_fd_, buffer, sizeof(buffer) - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            {
                std::lock_guard<std::mutex> lk(output_mutex_);
                appendOutput(buffer, static_cast<size_t>(bytes_read));
            }
            VERBOSE_LOGF("[dragonxd] %s", buffer);
        }
        
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

#endif  // _WIN32

bool EmbeddedDaemon::isRpcPortInUse()
{
    int port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
    return isPortInUse(port);
}

bool EmbeddedDaemon::tcpPortInUse(int port)
{
    return isPortInUse(port);
}

bool EmbeddedDaemon::isDaemonProcessRunning()
{
#ifdef _WIN32
    return findProcessByName("dragonxd.exe") != 0;
#elif defined(__linux__)
    // Scan /proc for a process whose comm is exactly "dragonxd". Iterate with an error_code so a proc
    // entry vanishing mid-scan (a process exiting) can't throw.
    std::error_code ec;
    fs::directory_iterator it("/proc", ec), end;
    for (; !ec && it != end; it.increment(ec)) {
        const std::string pid = it->path().filename().string();
        if (pid.empty() || pid[0] < '0' || pid[0] > '9') continue;   // numeric pid dirs only
        std::ifstream f((it->path() / "comm").string());
        std::string comm;
        if (f && std::getline(f, comm) && comm == "dragonxd") return true;
    }
    return false;
#else
    // macOS has no /proc; fall back to the RPC-port probe (best-effort).
    return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT));
#endif
}

} // namespace daemon
} // namespace dragonx
