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

#include "platform.h"

#include <cstdlib>
#include <cstdio>
#include <cerrno>
#include <cctype>
#include <cstring>
#include <fstream>
#include <sstream>
#include <filesystem>

// Embedded ui.toml (generated at build time)
#if __has_include("ui_toml_embedded.h")
#include "ui_toml_embedded.h"
#define HAS_EMBEDDED_UI_TOML 1
#else
#define HAS_EMBEDDED_UI_TOML 0
#endif
#include <sys/stat.h>

#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
#include <shlobj.h>
#include <psapi.h>
#include <tlhelp32.h>
#else
#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
#include <dirent.h>
#include <dlfcn.h>
#include <spawn.h>
#include <sys/wait.h>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#include <sys/sysctl.h>
#include <mach/mach.h>
#endif
#endif

#include "../util/logger.h"
#include "../config/version.h"  // DRAGONX_APP_NAME — variant-specific config folder name

#ifndef _WIN32
extern char **environ;
#endif

namespace dragonx {
namespace util {

namespace {

bool hasAllowedUrlScheme(const std::string& url)
{
    auto startsWithNoCase = [&url](const char* prefix) {
        for (size_t i = 0; prefix[i] != '\0'; ++i) {
            if (i >= url.size()) return false;
            unsigned char lhs = static_cast<unsigned char>(url[i]);
            unsigned char rhs = static_cast<unsigned char>(prefix[i]);
            if (std::tolower(lhs) != std::tolower(rhs)) return false;
        }
        return true;
    };

    return startsWithNoCase("http://") || startsWithNoCase("https://");
}

#ifndef _WIN32
bool launchOpener(const char* opener, const std::string& target)
{
    char* const argv[] = {
        const_cast<char*>(opener),
        const_cast<char*>(target.c_str()),
        nullptr
    };

    pid_t pid = 0;
    int rc = posix_spawnp(&pid, opener, nullptr, nullptr, argv, environ);
    if (rc != 0) {
        DEBUG_LOGF("Failed to launch %s: %s\n", opener, std::strerror(rc));
        return false;
    }

    int status = 0;
    pid_t waited = 0;
    do {
        waited = waitpid(pid, &status, 0);
    } while (waited < 0 && errno == EINTR);

    if (waited < 0) {
        DEBUG_LOGF("Failed waiting for %s launcher: %s\n", opener, std::strerror(errno));
        return false;
    }
    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
        DEBUG_LOGF("Launcher %s exited with status %d\n", opener,
                   WIFEXITED(status) ? WEXITSTATUS(status) : -1);
        return false;
    }
    return true;
}
#endif

} // namespace

bool Platform::openUrl(const std::string& url)
{
    if (url.empty()) return false;
    if (!hasAllowedUrlScheme(url)) {
        DEBUG_LOGF("Refusing to open URL with unsupported scheme: %s\n", url.c_str());
        return false;
    }
    
#ifdef _WIN32
    // Windows: Use ShellExecute
    HINSTANCE result = ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
    return (reinterpret_cast<intptr_t>(result) > 32);
#elif defined(__APPLE__)
    return launchOpener("open", url);
#else
    return launchOpener("xdg-open", url);
#endif
}

bool Platform::ensureDirectory(const std::string& dir, std::string* outError)
{
    if (dir.empty()) {
        if (outError) *outError = "Cannot create directory: empty path.";
        return false;
    }
    std::error_code ec;
    if (std::filesystem::is_directory(dir, ec)) return true;
    ec.clear();
    std::filesystem::create_directories(dir, ec);
    if (ec) {
        if (outError) {
            *outError = "Cannot create " + dir + ": " + ec.message() +
                        ". Check permissions / free space.";
        }
        DEBUG_LOGF("[ERROR] ensureDirectory failed for %s: %s\n", dir.c_str(), ec.message().c_str());
        return false;
    }
    return true;
}

bool Platform::openFolder(const std::string& path, bool createIfMissing)
{
    if (path.empty()) return false;
    
    // Create directory if it doesn't exist
    if (createIfMissing) {
        std::error_code ec;
        std::filesystem::create_directories(path, ec);
        if (ec) {
            DEBUG_LOGF("Failed to create folder %s: %s\n", path.c_str(), ec.message().c_str());
            return false;
        }
    }
    
#ifdef _WIN32
    // Windows: Use explorer.exe to open folder
    HINSTANCE result = ShellExecuteA(nullptr, "explore", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
    return (reinterpret_cast<intptr_t>(result) > 32);
#elif defined(__APPLE__)
    return launchOpener("open", path);
#else
    return launchOpener("xdg-open", path);
#endif
}

uint64_t Platform::getFileSize(const std::string& path)
{
    struct stat st;
    if (stat(path.c_str(), &st) == 0) {
        return static_cast<uint64_t>(st.st_size);
    }
    return 0;
}

std::string Platform::formatFileSize(uint64_t bytes)
{
    char buf[64];
    
    if (bytes >= 1024ULL * 1024ULL * 1024ULL) {
        snprintf(buf, sizeof(buf), "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
    } else if (bytes >= 1024ULL * 1024ULL) {
        snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
    } else if (bytes >= 1024ULL) {
        snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
    } else {
        snprintf(buf, sizeof(buf), "%llu bytes", static_cast<unsigned long long>(bytes));
    }
    
    return std::string(buf);
}

std::string Platform::getHomeDir()
{
#ifdef _WIN32
    char path[MAX_PATH];
    if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_PROFILE, nullptr, 0, path))) {
        return std::string(path);
    }
    // Fallback
    const char* home = getenv("USERPROFILE");
    return home ? home : "C:\\";
#else
    const char* home = getenv("HOME");
    if (home) return home;
    
    // Fallback to passwd entry
    struct passwd* pw = getpwuid(getuid());
    if (pw && pw->pw_dir) return pw->pw_dir;
    
    return "/tmp";
#endif
}

std::string Platform::getDragonXDataDir()
{
#ifdef _WIN32
    char path[MAX_PATH];
    if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
        return std::string(path) + "\\Hush\\DRAGONX\\";
    }
    return getHomeDir() + "\\AppData\\Roaming\\Hush\\DRAGONX\\";
#elif defined(__APPLE__)
    return getHomeDir() + "/Library/Application Support/Hush/DRAGONX/";
#else
    return getHomeDir() + "/.hush/DRAGONX/";
#endif
}

std::string Platform::getDataDir()
{
    return getDragonXDataDir();
}

std::string Platform::getLiteWalletDataDir()
{
    // Mirror the SilentDragonXLite backend's get_zcash_data_path() for the "main" chain:
    //   Windows/macOS: dirs::data_dir()/silentdragonxlite   (data_dir = %APPDATA%, ~/Library/App Support)
    //   Linux:         ~/.silentdragonxlite
#ifdef _WIN32
    char path[MAX_PATH];
    if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
        return std::string(path) + "\\silentdragonxlite\\";
    }
    return getHomeDir() + "\\AppData\\Roaming\\silentdragonxlite\\";
#elif defined(__APPLE__)
    return getHomeDir() + "/Library/Application Support/silentdragonxlite/";
#else
    return getHomeDir() + "/.silentdragonxlite/";
#endif
}

std::string Platform::getConfigDir()
{
#ifdef _WIN32
    char path[MAX_PATH];
    if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
        return std::string(path) + "\\" DRAGONX_APP_NAME "\\";
    }
    return getHomeDir() + "\\AppData\\Roaming\\" DRAGONX_APP_NAME "\\";
#elif defined(__APPLE__)
    // macOS-native config location (matches what settings.json / addressbook.json use).
    return getHomeDir() + "/Library/Application Support/" DRAGONX_APP_NAME "/";
#else
    return getHomeDir() + "/.config/" DRAGONX_APP_NAME "/";
#endif
}

bool Platform::deleteFile(const std::string& path)
{
#ifdef _WIN32
    return DeleteFileA(path.c_str()) != 0 || GetLastError() == ERROR_FILE_NOT_FOUND;
#else
    return (remove(path.c_str()) == 0 || errno == ENOENT);
#endif
}

bool Platform::writeFileAtomically(const std::string& path,
                                   const std::string& content,
                                   bool restrictPermissions)
{
    namespace fs = std::filesystem;
    std::error_code ec;
    fs::path target(path);
    fs::path dir = target.parent_path();
    if (!dir.empty()) {
        fs::create_directories(dir, ec); // ignore ec; the open below will fail if truly unwritable
    }
    const std::string tmp = path + ".tmp";

#ifdef _WIN32
    {
        std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
        if (!out.is_open()) return false;
        out.write(content.data(), static_cast<std::streamsize>(content.size()));
        out.flush();
        if (!out) { out.close(); DeleteFileA(tmp.c_str()); return false; }
    }
    // Flush the temp file's contents to stable storage before the rename.
    HANDLE h = CreateFileA(tmp.c_str(), GENERIC_WRITE, 0, nullptr,
                           OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (h != INVALID_HANDLE_VALUE) {
        FlushFileBuffers(h);
        CloseHandle(h);
    }
    (void)restrictPermissions; // owner-only ACLs rely on the per-user profile dir on Windows
    // MoveFileEx with REPLACE_EXISTING is atomic on NTFS; WRITE_THROUGH makes it durable.
    if (MoveFileExA(tmp.c_str(), path.c_str(),
                    MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) {
        DeleteFileA(tmp.c_str());
        return false;
    }
    return true;
#else
    const mode_t mode = restrictPermissions ? 0600 : 0644;
    int fd = ::open(tmp.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode);
    if (fd < 0) return false;
    if (restrictPermissions) {
        ::fchmod(fd, 0600); // enforce even if the temp file pre-existed with looser perms
    }
    bool ok = true;
    size_t written = 0;
    const char* data = content.data();
    while (written < content.size()) {
        ssize_t n = ::write(fd, data + written, content.size() - written);
        if (n < 0) {
            if (errno == EINTR) continue;
            ok = false;
            break;
        }
        written += static_cast<size_t>(n);
    }
    if (ok && ::fsync(fd) != 0) ok = false;
    ::close(fd);
    if (!ok) {
        ::remove(tmp.c_str());
        return false;
    }
    if (::rename(tmp.c_str(), path.c_str()) != 0) {
        ::remove(tmp.c_str());
        return false;
    }
    // fsync the directory so the rename (the metadata that makes the new file
    // visible) survives a crash, not just the file contents.
    int dfd = ::open(dir.empty() ? "." : dir.c_str(), O_RDONLY);
    if (dfd >= 0) {
        ::fsync(dfd);
        ::close(dfd);
    }
    return true;
#endif
}

std::string Platform::getExecutableDirectory()
{
#ifdef _WIN32
    char path[MAX_PATH];
    DWORD len = GetModuleFileNameA(nullptr, path, MAX_PATH);
    if (len > 0 && len < MAX_PATH) {
        std::string fullPath(path);
        size_t pos = fullPath.find_last_of("\\/");
        if (pos != std::string::npos) {
            return fullPath.substr(0, pos);
        }
    }
    return ".";
#elif defined(__APPLE__)
    char path[1024];
    uint32_t size = sizeof(path);
    if (_NSGetExecutablePath(path, &size) == 0) {
        char* resolved = realpath(path, nullptr);
        if (resolved) {
            std::string fullPath(resolved);
            free(resolved);
            size_t pos = fullPath.find_last_of('/');
            if (pos != std::string::npos) {
                return fullPath.substr(0, pos);
            }
        }
    }
    return ".";
#else
    // Linux: read /proc/self/exe
    char path[1024];
    ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1);
    if (len > 0) {
        path[len] = '\0';
        std::string fullPath(path);
        size_t pos = fullPath.find_last_of('/');
        if (pos != std::string::npos) {
            return fullPath.substr(0, pos);
        }
    }
    return ".";
#endif
}

std::string getExecutableDirectory()
{
    return Platform::getExecutableDirectory();
}

std::string Platform::getObsidianDragonDir()
{
#ifdef _WIN32
    const char* appdata = std::getenv("APPDATA");
    if (appdata) {
        return (std::filesystem::path(appdata) / DRAGONX_APP_NAME).string();
    }
    return (std::filesystem::path(getHomeDir()) / "AppData" / "Roaming" / DRAGONX_APP_NAME).string();
#elif defined(__APPLE__)
    return (std::filesystem::path(getHomeDir()) / "Library" / "Application Support" / DRAGONX_APP_NAME).string();
#else
    const char* xdg_config = std::getenv("XDG_CONFIG_HOME");
    if (xdg_config) {
        return (std::filesystem::path(xdg_config) / DRAGONX_APP_NAME).string();
    }
    return (std::filesystem::path(getHomeDir()) / ".config" / DRAGONX_APP_NAME).string();
#endif
}

void Platform::ensureObsidianDragonSetup()
{
    namespace fs = std::filesystem;

    fs::path base = getObsidianDragonDir();
    fs::path themes_dir = base / "themes";
    fs::path example_dir = base / "example" / "example_theme";
    fs::path example_img = example_dir / "img";

    // Create directory structure
    std::error_code ec;
    fs::create_directories(themes_dir, ec);
    fs::create_directories(example_img, ec);
    if (ec) {
        DEBUG_LOGF("Warning: Could not create ObsidianDragon directories: %s\n", ec.message().c_str());
        return;
    }
    DEBUG_LOGF("ObsidianDragon: Config directory: %s\n", base.string().c_str());

    // Write example theme.toml — a full copy of ui.toml so users can see
    // and modify the complete layout+theme structure.  UISchema can load
    // themes from TOML format.
    // Regenerate whenever the binary is newer than the example file, so
    // layout changes in ui.toml are always reflected.
    fs::path example_toml = example_dir / "theme.toml";
    bool needsWrite = !fs::exists(example_toml);
    if (!needsWrite) {
        // Regenerate if the running binary is newer than the example file
        fs::path exePath = fs::path(getExecutableDirectory()) /
#ifdef _WIN32
            DRAGONX_APP_NAME ".exe";
#else
            DRAGONX_APP_NAME;
#endif
        std::error_code tec;
        auto exeTime  = fs::last_write_time(exePath, tec);
        auto fileTime = fs::last_write_time(example_toml, tec);
        if (!tec && exeTime > fileTime)
            needsWrite = true;
    }
    if (needsWrite) {
        std::ofstream f(example_toml);
        if (f.is_open()) {
            // Use the build-time embedded copy first; fall back to the
            // bundled file on disk.
            std::string content;
#if HAS_EMBEDDED_UI_TOML
            content.assign(reinterpret_cast<const char*>(embedded::ui_toml_data),
                           embedded::ui_toml_size);
#else
            {
                fs::path bundled = fs::path(getExecutableDirectory()) / "res" / "themes" / "ui.toml";
                std::ifstream src(bundled);
                if (src.is_open()) {
                    std::ostringstream ss;
                    ss << src.rdbuf();
                    content = ss.str();
                }
            }
#endif

            if (!content.empty()) {
                // Patch the theme metadata so the example is clearly
                // distinguishable from the bundled default.
                // In TOML: name = "DragonX" → name = "Example Theme"
                auto replaceFirst = [&](const std::string& key,
                                        const std::string& oldVal,
                                        const std::string& newVal) {
                    // Find 'key = "oldVal"' pattern and replace oldVal
                    std::string needle = key + " = ";
                    auto pos = content.find(needle);
                    if (pos == std::string::npos) return;
                    auto vpos = content.find(oldVal, pos);
                    if (vpos != std::string::npos && vpos - pos < 80)
                        content.replace(vpos, oldVal.size(), newVal);
                };
                replaceFirst("name",   "\"DragonX\"",           "\"Example Theme\"");
                replaceFirst("author", "\"The Hush Developers\"", "\"Your Name\"");

                // Prepend comment block (TOML uses # for comments natively)
                std::string header =
                    "# === DragonX Wallet Theme File ===\n"
                    "# Copy this entire folder into the 'themes' folder to use it.\n"
                    "# The folder name becomes the theme ID.\n"
                    "#\n"
                    "# === Images ===\n"
                    "# Place image files in the 'img' subfolder.\n"
                    "# Specify custom filenames below, or use the defaults:\n"
                    "#   backgrounds/gradient/dark_gradient.png    - background gradient overlay\n"
                    "#   logos/logo_ObsidianDragon_dark.png - sidebar logo (128x128 recommended)\n"
                    "#\n"
                    "# === Colors ===\n"
                    "# Colors use CSS-style formats: #RRGGBB, #RRGGBBAA, or rgba(r,g,b,a)\n"
                    "# Palette variables can be referenced elsewhere with var(--name)\n\n";
                content = header + content;
            } else {
                DEBUG_LOGF("ObsidianDragon: Warning — ui.toml not available, skipping example theme\n");
            }

            f << content;
            f.close();
            DEBUG_LOGF("ObsidianDragon: Created example theme: %s\n", example_toml.string().c_str());
        }
    }

    // Copy bundled images into example/example_theme/img/ for reference
    fs::path exe_dir = getExecutableDirectory();
    fs::path res_img = exe_dir / "res" / "img";

    auto copyIfMissing = [&](const char* srcRelPath, const char* dstFilename) {
        fs::path src = res_img / srcRelPath;
        fs::path dst = example_img / dstFilename;
        if (fs::exists(src) && !fs::exists(dst)) {
            std::error_code cpec;
            fs::copy_file(src, dst, cpec);
            if (!cpec) {
                DEBUG_LOGF("ObsidianDragon: Copied %s to example theme\n", dstFilename);
            }
        }
    };
    copyIfMissing("backgrounds/gradient/dark_gradient.png", "dark_gradient.png");
    copyIfMissing("logos/logo_ObsidianDragon_dark.png", "logo_ObsidianDragon_dark.png");
    copyIfMissing("logos/logo_ObsidianDragon_light.png", "logo_ObsidianDragon_light.png");
}

double Platform::getTotalSystemRAM_MB()
{
    static double cached = 0.0;
    if (cached > 0.0) return cached;

#ifdef _WIN32
    MEMORYSTATUSEX memInfo;
    memInfo.dwLength = sizeof(memInfo);
    if (GlobalMemoryStatusEx(&memInfo)) {
        cached = (double)memInfo.ullTotalPhys / (1024.0 * 1024.0);
    }
#elif defined(__APPLE__)
    int64_t memSize = 0;
    size_t len = sizeof(memSize);
    if (sysctlbyname("hw.memsize", &memSize, &len, nullptr, 0) == 0) {
        cached = (double)memSize / (1024.0 * 1024.0);
    }
#else
    // Linux: use sysconf
    long pages = sysconf(_SC_PHYS_PAGES);
    long pageSize = sysconf(_SC_PAGE_SIZE);
    if (pages > 0 && pageSize > 0) {
        cached = (double)pages * (double)pageSize / (1024.0 * 1024.0);
    }
#endif
    return cached;
}

double Platform::getUsedSystemRAM_MB()
{
#ifdef _WIN32
    MEMORYSTATUSEX memInfo;
    memInfo.dwLength = sizeof(memInfo);
    if (GlobalMemoryStatusEx(&memInfo)) {
        double total = (double)memInfo.ullTotalPhys / (1024.0 * 1024.0);
        double avail = (double)memInfo.ullAvailPhys / (1024.0 * 1024.0);
        return total - avail;
    }
    return 0.0;
#elif defined(__APPLE__)
    // macOS: vm_statistics64 for free/active/inactive/wired pages
    vm_size_t pageSize = 0;
    host_page_size(mach_host_self(), &pageSize);
    vm_statistics64_data_t vmStats;
    mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
    if (host_statistics64(mach_host_self(), HOST_VM_INFO64,
                          (host_info64_t)&vmStats, &count) == KERN_SUCCESS) {
        // "used" = total - free - inactive (matches Activity Monitor "Memory Used")
        double total = getTotalSystemRAM_MB();
        double freeMB = (double)(vmStats.free_count + vmStats.inactive_count) * (double)pageSize / (1024.0 * 1024.0);
        return total - freeMB;
    }
    return 0.0;
#else
    // Linux: parse /proc/meminfo for MemAvailable
    FILE* f = fopen("/proc/meminfo", "r");
    if (!f) return 0.0;
    double totalMB = 0.0, availMB = 0.0;
    char line[256];
    int found = 0;
    while (fgets(line, sizeof(line), f) && found < 2) {
        long val = 0;
        if (strncmp(line, "MemTotal:", 9) == 0) {
            if (sscanf(line + 9, "%ld", &val) == 1) { totalMB = (double)val / 1024.0; found++; }
        } else if (strncmp(line, "MemAvailable:", 13) == 0) {
            if (sscanf(line + 13, "%ld", &val) == 1) { availMB = (double)val / 1024.0; found++; }
        }
    }
    fclose(f);
    if (found == 2) return totalMB - availMB;
    return 0.0;
#endif
}

double Platform::getSelfMemoryUsageMB()
{
#ifdef _WIN32
    PROCESS_MEMORY_COUNTERS pmc;
    if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
        return (double)pmc.WorkingSetSize / (1024.0 * 1024.0);
    }
    return 0.0;
#elif defined(__APPLE__)
    struct mach_task_basic_info info;
    mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
    if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
                  (task_info_t)&info, &count) == KERN_SUCCESS) {
        return (double)info.resident_size / (1024.0 * 1024.0);
    }
    return 0.0;
#else
    // Linux: read /proc/self/status for VmRSS
    FILE* f = fopen("/proc/self/status", "r");
    if (!f) return 0.0;
    char line[256];
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "VmRSS:", 6) == 0) {
            long val = 0;
            if (sscanf(line + 6, "%ld", &val) == 1) {
                fclose(f);
                return (double)val / 1024.0; // kB -> MB
            }
        }
    }
    fclose(f);
    return 0.0;
#endif
}

double Platform::getDaemonMemoryUsageMB()
{
#ifdef _WIN32
    // Windows: use CreateToolhelp32Snapshot to find dragonxd.exe processes
    // and query their working set size.
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snap == INVALID_HANDLE_VALUE) return 0.0;

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

    double totalMB = 0.0;
    if (Process32First(snap, &entry)) {
        do {
            if (_stricmp(entry.szExeFile, "dragonxd.exe") == 0) {
                // GetProcessMemoryInfo requires PROCESS_QUERY_INFORMATION + PROCESS_VM_READ.
                // Try full access first; fall back to limited for elevated processes.
                HANDLE hProc = OpenProcess(
                    PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
                    FALSE, entry.th32ProcessID);
                if (!hProc) {
                    hProc = OpenProcess(
                        PROCESS_QUERY_LIMITED_INFORMATION,
                        FALSE, entry.th32ProcessID);
                }
                if (hProc) {
                    PROCESS_MEMORY_COUNTERS pmc;
                    ZeroMemory(&pmc, sizeof(pmc));
                    pmc.cb = sizeof(pmc);
                    if (GetProcessMemoryInfo(hProc, &pmc, sizeof(pmc)))
                        totalMB += (double)pmc.WorkingSetSize / (1024.0 * 1024.0);
                    CloseHandle(hProc);
                }
            }
        } while (Process32Next(snap, &entry));
    }
    CloseHandle(snap);
    return totalMB;
#elif defined(__APPLE__)
    // macOS: use pgrep to find dragonxd PIDs, then read RSS via proc_pidinfo
    FILE* fp = popen("pgrep -x dragonxd", "r");
    if (!fp) return 0.0;
    double totalMB = 0.0;
    char line[64];
    while (fgets(line, sizeof(line), fp)) {
        pid_t pid = (pid_t)atoi(line);
        if (pid <= 0) continue;
        struct mach_task_basic_info info;
        mach_msg_type_number_t cnt = MACH_TASK_BASIC_INFO_COUNT;
        // We can't directly task_for_pid without entitlements, so fall back to ps
        char cmd[128];
        snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d", pid);
        FILE* ps = popen(cmd, "r");
        if (ps) {
            char rssLine[64];
            if (fgets(rssLine, sizeof(rssLine), ps)) {
                long rssKB = atol(rssLine);
                if (rssKB > 0) totalMB += (double)rssKB / 1024.0;
            }
            pclose(ps);
        }
    }
    pclose(fp);
    return totalMB;
#else
    // Linux: iterate /proc/<pid>/comm looking for "dragonxd"
    DIR* procDir = opendir("/proc");
    if (!procDir) return 0.0;
    double totalMB = 0.0;
    pid_t selfPid = getpid();
    struct dirent* entry;
    while ((entry = readdir(procDir)) != nullptr) {
        // Only numeric directories (PIDs)
        if (entry->d_name[0] < '0' || entry->d_name[0] > '9') continue;
        pid_t pid = (pid_t)atoi(entry->d_name);
        if (pid <= 0 || pid == selfPid) continue;

        // Read comm (process name)
        char commPath[64];
        snprintf(commPath, sizeof(commPath), "/proc/%d/comm", pid);
        FILE* cf = fopen(commPath, "r");
        if (!cf) continue;
        char comm[256] = {};
        if (fgets(comm, sizeof(comm), cf)) {
            // Strip trailing newline
            size_t len = strlen(comm);
            if (len > 0 && comm[len - 1] == '\n') comm[len - 1] = '\0';
        }
        fclose(cf);

        if (strcmp(comm, "dragonxd") != 0) continue;

        // Read VmRSS from /proc/<pid>/status
        char statusPath[64];
        snprintf(statusPath, sizeof(statusPath), "/proc/%d/status", pid);
        FILE* sf = fopen(statusPath, "r");
        if (!sf) continue;
        char sline[256];
        while (fgets(sline, sizeof(sline), sf)) {
            if (strncmp(sline, "VmRSS:", 6) == 0) {
                long val = 0;
                if (sscanf(sline + 6, "%ld", &val) == 1)
                    totalMB += (double)val / 1024.0; // kB -> MB
                break;
            }
        }
        fclose(sf);
    }
    closedir(procDir);
    return totalMB;
#endif
}

// ============================================================================
// System-wide idle time detection
// ============================================================================

int Platform::getSystemIdleSeconds()
{
#ifdef _WIN32
    LASTINPUTINFO lii;
    lii.cbSize = sizeof(lii);
    if (GetLastInputInfo(&lii)) {
        DWORD elapsed = GetTickCount() - lii.dwTime;
        return (int)(elapsed / 1000);
    }
    return 0;
#elif defined(__APPLE__)
    // macOS: use IOKit HIDSystem idle time
    // Fallback to 0 (feature not supported) to keep it simple
    return 0;
#else
    // Linux: dynamically load libXss to query X11 screensaver idle time
    // This avoids a hard link dependency on X11/Xss libraries.
    typedef struct {
        int type;
        unsigned long serial;
        int/*Bool*/ send_event;
        void* /*Display**/ display;
        unsigned long/*Drawable*/ window;
        int state;
        int kind;
        unsigned long til_or_since;
        unsigned long idle;
        unsigned long eventMask;
    } XScreenSaverInfo;

    // Function pointer types
    typedef void* (*XOpenDisplay_t)(const char*);
    typedef unsigned long (*XDefaultRootWindow_t)(void*);
    typedef XScreenSaverInfo* (*XScreenSaverAllocInfo_t)(void);
    typedef int (*XScreenSaverQueryInfo_t)(void*, unsigned long, XScreenSaverInfo*);
    typedef int (*XCloseDisplay_t)(void*);
    typedef void (*XFree_t)(void*);

    static bool s_tried = false;
    static void* s_x11 = nullptr;
    static void* s_xss = nullptr;
    static XOpenDisplay_t s_XOpenDisplay = nullptr;
    static XDefaultRootWindow_t s_XDefaultRootWindow = nullptr;
    static XScreenSaverAllocInfo_t s_XScreenSaverAllocInfo = nullptr;
    static XScreenSaverQueryInfo_t s_XScreenSaverQueryInfo = nullptr;
    static XCloseDisplay_t s_XCloseDisplay = nullptr;
    (void)s_XCloseDisplay; // retained for potential cleanup
    static XFree_t s_XFree = nullptr;
    static void* s_display = nullptr;

    if (!s_tried) {
        s_tried = true;
        s_x11 = dlopen("libX11.so.6", RTLD_LAZY);
        s_xss = dlopen("libXss.so.1", RTLD_LAZY);
        if (s_x11 && s_xss) {
            s_XOpenDisplay = (XOpenDisplay_t)dlsym(s_x11, "XOpenDisplay");
            s_XDefaultRootWindow = (XDefaultRootWindow_t)dlsym(s_x11, "XDefaultRootWindow");
            s_XCloseDisplay = (XCloseDisplay_t)dlsym(s_x11, "XCloseDisplay");  // NOLINT
            s_XFree = (XFree_t)dlsym(s_x11, "XFree");
            s_XScreenSaverAllocInfo = (XScreenSaverAllocInfo_t)dlsym(s_xss, "XScreenSaverAllocInfo");
            s_XScreenSaverQueryInfo = (XScreenSaverQueryInfo_t)dlsym(s_xss, "XScreenSaverQueryInfo");

            if (s_XOpenDisplay && s_XDefaultRootWindow && s_XScreenSaverAllocInfo && s_XScreenSaverQueryInfo) {
                s_display = s_XOpenDisplay(nullptr);
            }
        }
    }

    if (!s_display || !s_XScreenSaverAllocInfo || !s_XScreenSaverQueryInfo || !s_XDefaultRootWindow) {
        return 0;
    }

    XScreenSaverInfo* info = s_XScreenSaverAllocInfo();
    if (!info) return 0;

    int idleSec = 0;
    unsigned long root = s_XDefaultRootWindow(s_display);
    if (s_XScreenSaverQueryInfo(s_display, root, info)) {
        idleSec = (int)(info->idle / 1000); // idle is in milliseconds
    }
    if (s_XFree) s_XFree(info);
    return idleSec;
#endif
}

// ============================================================================
// GPU utilization detection
// ============================================================================

std::string Platform::runHiddenCapture(const std::string& cmdLine, bool mergeStderr, int* exitCode)
{
    if (exitCode) *exitCode = -1;
#ifdef _WIN32
    SECURITY_ATTRIBUTES sa;
    ZeroMemory(&sa, sizeof(sa));
    sa.nLength = sizeof(sa);
    sa.bInheritHandle = TRUE;
    HANDLE hRead = NULL, hWrite = NULL;
    if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return {};
    SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);   // parent's read end stays private

    HANDLE hNul = INVALID_HANDLE_VALUE;
    if (!mergeStderr) {
        hNul = CreateFileA("NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
                           OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    }

    STARTUPINFOA si;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;
    si.hStdInput  = GetStdHandle(STD_INPUT_HANDLE);
    si.hStdOutput = hWrite;
    si.hStdError  = mergeStderr ? hWrite : hNul;

    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(pi));
    std::string cl = cmdLine;   // CreateProcessA may modify lpCommandLine → needs a mutable buffer
    std::string out;
    if (CreateProcessA(NULL, cl.empty() ? NULL : &cl[0], NULL, NULL, TRUE,
                       CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
        CloseHandle(hWrite); hWrite = NULL;   // close our copy so ReadFile hits EOF when the child exits
        if (hNul != INVALID_HANDLE_VALUE) { CloseHandle(hNul); hNul = INVALID_HANDLE_VALUE; }
        char buf[4096];
        DWORD n = 0;
        while (ReadFile(hRead, buf, sizeof(buf), &n, NULL) && n > 0) out.append(buf, n);
        WaitForSingleObject(pi.hProcess, INFINITE);
        if (exitCode) {
            DWORD code = 0;
            if (GetExitCodeProcess(pi.hProcess, &code)) *exitCode = static_cast<int>(code);
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    if (hWrite != NULL) CloseHandle(hWrite);
    if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul);
    CloseHandle(hRead);
    return out;
#else
    const std::string full = cmdLine + (mergeStderr ? " 2>&1" : " 2>/dev/null");
    std::string out;
    FILE* f = popen(full.c_str(), "r");
    if (!f) return out;
    char buf[512];
    size_t n;
    while ((n = fread(buf, 1, sizeof(buf), f)) > 0) out.append(buf, n);
    const int st = pclose(f);
    if (exitCode) *exitCode = st;   // raw status (matches prior pclose-based rc checks)
    return out;
#endif
}

int Platform::getGpuUtilization()
{
#ifdef _WIN32
    // Windows: read GPU utilization via SetupAPI / D3DKMT
    // Not all GPUs expose this; return -1 if unavailable.
    // Use a popen fallback: nvidia-smi for NVIDIA, or return -1.
    static bool s_tried_nvidia = false;
    static bool s_has_nvidia = false;
    if (!s_tried_nvidia) {
        s_tried_nvidia = true;
        // Windowless (runHiddenCapture) so GPU-aware idle detection never flashes a cmd.exe console.
        const std::string w = runHiddenCapture("where nvidia-smi");
        s_has_nvidia = (w.find_first_not_of(" \t\r\n") != std::string::npos);
    }
    if (s_has_nvidia) {
        const std::string o = runHiddenCapture(
            "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits");
        if (!o.empty()) {
            int util = atoi(o.c_str());
            if (util < 0 || util > 100) util = -1;
            return util;
        }
    }
    return -1;
#elif defined(__APPLE__)
    return -1;
#else
    // Linux: try multiple GPU sysfs paths

    // AMD: /sys/class/drm/card*/device/gpu_busy_percent
    {
        // Try card0 through card3
        char path[128];
        for (int i = 0; i < 4; i++) {
            snprintf(path, sizeof(path), "/sys/class/drm/card%d/device/gpu_busy_percent", i);
            std::ifstream ifs(path);
            if (ifs.is_open()) {
                int val = -1;
                ifs >> val;
                if (val >= 0 && val <= 100)
                    return val;
            }
        }
    }

    // NVIDIA: nvidia-smi (binary may exist even without sysfs)
    {
        static bool s_tried = false;
        static bool s_has_nvidia_smi = false;
        if (!s_tried) {
            s_tried = true;
            FILE* f = popen("which nvidia-smi 2>/dev/null", "r");
            if (f) {
                char buf[256];
                s_has_nvidia_smi = (fgets(buf, sizeof(buf), f) != nullptr);
                pclose(f);
            }
        }
        if (s_has_nvidia_smi) {
            FILE* f = popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "r");
            if (f) {
                char buf[64];
                int util = -1;
                if (fgets(buf, sizeof(buf), f)) {
                    util = atoi(buf);
                    if (util < 0 || util > 100) util = -1;
                }
                pclose(f);
                return util;
            }
        }
    }

    // Intel: compare current vs max freq as a rough proxy
    {
        std::ifstream curF("/sys/class/drm/card0/gt_cur_freq_mhz");
        std::ifstream maxF("/sys/class/drm/card0/gt_max_freq_mhz");
        if (curF.is_open() && maxF.is_open()) {
            int cur = 0, mx = 0;
            curF >> cur;
            maxF >> mx;
            if (mx > 0)
                return std::min(100, (cur * 100) / mx);
        }
    }

    return -1;
#endif
}

} // namespace util
} // namespace dragonx
