// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// settings.cpp — JSON settings persistence. Loads/saves user preferences
// to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows).

#include "settings.h"
#include "version.h"

#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
#include <ctime>
#include <algorithm>
#include <type_traits>

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

#ifdef _WIN32
#include <shlobj.h>
#else
#include <pwd.h>
#include <unistd.h>
#endif

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

namespace dragonx {
namespace config {

Settings::Settings() = default;
Settings::~Settings() = default;

namespace {

Settings::LiteServerSelectionPreferenceMode parseLiteServerSelectionPreferenceMode(
    const json& value)
{
    if (!value.is_string()) return Settings::LiteServerSelectionPreferenceMode::Sticky;
    const std::string mode = value.get<std::string>();
    if (mode == "random" || mode == "Random") {
        return Settings::LiteServerSelectionPreferenceMode::Random;
    }
    return Settings::LiteServerSelectionPreferenceMode::Sticky;
}

const char* liteServerSelectionPreferenceModeName(
    Settings::LiteServerSelectionPreferenceMode mode)
{
    switch (mode) {
        case Settings::LiteServerSelectionPreferenceMode::Sticky:
            return "sticky";
        case Settings::LiteServerSelectionPreferenceMode::Random:
            return "random";
    }
    return "sticky";
}

Settings::PoolSelectMode parsePoolSelectMode(const json& value)
{
    if (!value.is_string()) return Settings::PoolSelectMode::Manual;
    const std::string mode = value.get<std::string>();
    if (mode == "auto_balance" || mode == "auto") {
        return Settings::PoolSelectMode::AutoBalance;
    }
    return Settings::PoolSelectMode::Manual;
}

const char* poolSelectModeName(Settings::PoolSelectMode mode)
{
    switch (mode) {
        case Settings::PoolSelectMode::Manual:      return "manual";
        case Settings::PoolSelectMode::AutoBalance: return "auto_balance";
    }
    return "manual";
}

// True if j[key] exists and holds a JSON value convertible to T. Guards every scalar
// read so a malformed value (wrong type / missing key) leaves the field's default
// instead of throwing out of the whole load().
template <typename T>
bool jsonHasType(const json& v)
{
    if constexpr (std::is_same_v<T, bool>) return v.is_boolean();
    else if constexpr (std::is_same_v<T, std::string>) return v.is_string();
    else if constexpr (std::is_floating_point_v<T>) return v.is_number();
    else if constexpr (std::is_integral_v<T>) return v.is_number_integer();
    else return false;
}

// Reads j[key] into field only when present AND of the matching type.
template <typename T>
void loadScalar(const json& j, const char* key, T& field)
{
    if (j.contains(key) && jsonHasType<T>(j[key])) field = j[key].get<T>();
}

// Same as loadScalar but clamps the read value into [lo, hi].
template <typename T>
void loadClamped(const json& j, const char* key, T& field, T lo, T hi)
{
    if (j.contains(key) && jsonHasType<T>(j[key]))
        field = std::max(lo, std::min(hi, j[key].get<T>()));
}

} // namespace

std::vector<Settings::LiteServerPreference> Settings::defaultLiteServers()
{
    return {
        {"https://lite.dragonx.is", "DragonX Lite", true},
        {"https://lite1.dragonx.is", "DragonX Lite 1", true},
        {"https://lite2.dragonx.is", "DragonX Lite 2", true},
        {"https://lite3.dragonx.is", "DragonX Lite 3", true},
        {"https://lite4.dragonx.is", "DragonX Lite 4", true},
        {"https://lite5.dragonx.is", "DragonX Lite 5", true},
        {"https://lite6.dragonx.is", "DragonX Lite 6", true},
        {"https://lite7.dragonx.is", "DragonX Lite 7", true}
    };
}

std::string Settings::getDefaultPath()
{
    // Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
    // _WIN32 / __APPLE__ / XDG split and the DRAGONX_APP_NAME variant suffix in one place).
    const std::string dir = util::Platform::getConfigDir();
    fs::create_directories(dir);
    return (fs::path(dir) / "settings.json").string();
}

bool Settings::load()
{
    return load(getDefaultPath());
}

bool Settings::load(const std::string& path)
{
    settings_path_ = path;
    
    std::ifstream file(path);
    if (!file.is_open()) {
        return false;
    }
    
    try {
        json j;
        file >> j;
        
        loadScalar(j, "theme", theme_);
        loadScalar(j, "save_ztxs", save_ztxs_);
        loadScalar(j, "auto_shield", auto_shield_);
        loadScalar(j, "use_tor", use_tor_);
        loadScalar(j, "allow_custom_fees", allow_custom_fees_);
        loadScalar(j, "default_fee", default_fee_);
        loadScalar(j, "fetch_prices", fetch_prices_);
        loadScalar(j, "tx_explorer_url", tx_explorer_url_);
        loadScalar(j, "address_explorer_url", address_explorer_url_);
        loadScalar(j, "language", language_);
        loadScalar(j, "skin_id", skin_id_);
        loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
        if (j.contains("muted_chat_cids") && j["muted_chat_cids"].is_array()) {
            muted_chat_cids_.clear();
            for (const auto& c : j["muted_chat_cids"])
                if (c.is_string()) muted_chat_cids_.push_back(c.get<std::string>());
        }
        if (j.contains("hidden_chat_cids") && j["hidden_chat_cids"].is_array()) {
            hidden_chat_cids_.clear();
            for (const auto& c : j["hidden_chat_cids"])
                if (c.is_string()) hidden_chat_cids_.push_back(c.get<std::string>());
        }
        if (j.contains("blocked_chat_convs") && j["blocked_chat_convs"].is_array()) {
            blocked_chat_convs_.clear();
            for (const auto& c : j["blocked_chat_convs"])
                if (c.is_object() && c.contains("cid") && c["cid"].is_string())
                    blocked_chat_convs_.push_back({c["cid"].get<std::string>(),
                                                   c.value("name", std::string())});
        }
        // Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
        loadScalar(j, "chat_emoji_color", chat_emoji_color_);
        loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
        loadScalar(j, "chat_bubble_style", chat_bubble_style_);   setChatBubbleStyle(chat_bubble_style_);
        loadScalar(j, "chat_bubble_accent", chat_bubble_accent_); setChatBubbleAccent(chat_bubble_accent_);
        loadScalar(j, "chat_density", chat_density_);             setChatDensity(chat_density_);
        loadScalar(j, "chat_font_scale", chat_font_scale_);       setChatFontScale(chat_font_scale_);
        loadScalar(j, "chat_time_format", chat_time_format_);     setChatTimeFormat(chat_time_format_);
        loadScalar(j, "chat_enter_sends", chat_enter_sends_);
        loadScalar(j, "time_format", time_format_);               setTimeFormat(time_format_);
        loadScalar(j, "acrylic_enabled", acrylic_enabled_);
        loadScalar(j, "acrylic_quality", acrylic_quality_);
        loadScalar(j, "blur_multiplier", blur_multiplier_);
        loadScalar(j, "noise_opacity", noise_opacity_);
        loadScalar(j, "gradient_background", gradient_background_);
        // Migrate legacy reduced_transparency bool -> ui_opacity float
        if (j.contains("ui_opacity")) {
            ui_opacity_ = j["ui_opacity"].get<float>();
        } else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
            ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
        }
        loadScalar(j, "window_opacity", window_opacity_);
        if (j.contains("balance_layout")) {
            if (j["balance_layout"].is_string())
                balance_layout_ = j["balance_layout"].get<std::string>();
            else if (j["balance_layout"].is_number_integer()) {
                // Legacy migration: convert old int index to string ID
                static const char* legacyIds[] = {
                    "classic","donut","consolidated","dashboard",
                    "vertical-stack","shield","timeline","two-row","minimal"
                };
                int idx = j["balance_layout"].get<int>();
                if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
            }
        }
        loadScalar(j, "portfolio_style", portfolio_style_);
        if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
        loadScalar(j, "contacts_view_mode", contacts_view_mode_);
        if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
        loadScalar(j, "contacts_avatar_shape", contacts_avatar_shape_); setContactsAvatarShape(contacts_avatar_shape_);
        loadScalar(j, "contacts_list_scale", contacts_list_scale_); setContactsListScale(contacts_list_scale_);
        loadScalar(j, "animate_avatars", animate_avatars_);
        loadScalar(j, "scanline_enabled", scanline_enabled_);
        loadScalar(j, "console_line_accents", console_line_accents_);
        loadScalar(j, "console_text_color", console_text_color_);
        loadScalar(j, "console_zoom", console_zoom_);
        if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f;  // guard bad/NaN
        loadScalar(j, "console_auto_focus", console_auto_focus_);
        if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
            hidden_addresses_.clear();
            for (const auto& a : j["hidden_addresses"])
                if (a.is_string()) hidden_addresses_.insert(a.get<std::string>());
        }
        if (j.contains("favorite_addresses") && j["favorite_addresses"].is_array()) {
            favorite_addresses_.clear();
            for (const auto& a : j["favorite_addresses"])
                if (a.is_string()) favorite_addresses_.insert(a.get<std::string>());
        }
        if (j.contains("address_meta") && j["address_meta"].is_object()) {
            address_meta_.clear();
            for (auto& [addr, meta] : j["address_meta"].items()) {
                AddressMeta m;
                if (meta.contains("label") && meta["label"].is_string())
                    m.label = meta["label"].get<std::string>();
                if (meta.contains("icon") && meta["icon"].is_string())
                    m.icon = meta["icon"].get<std::string>();
                if (meta.contains("order") && meta["order"].is_number_integer())
                    m.sortOrder = meta["order"].get<int>();
                if (meta.contains("mining") && meta["mining"].is_boolean())
                    m.mining = meta["mining"].get<bool>();
                address_meta_[addr] = m;
            }
        }
        loadScalar(j, "wizard_completed", wizard_completed_);
        loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
        loadScalar(j, "large_wallet_warned", large_wallet_warned_);
        if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
            empty_wallet_warning_acked_.clear();
            for (const auto& w : j["empty_wallet_warning_acked"])
                if (w.is_string()) empty_wallet_warning_acked_.insert(w.get<std::string>());
        }
        loadScalar(j, "encryption_pending", encryption_pending_);
        loadScalar(j, "daemon_update_prompted_size", daemon_update_prompted_size_);
        loadScalar(j, "active_wallet_file", active_wallet_file_);
        loadScalar(j, "seed_migration_pending", seed_migration_pending_);
        loadScalar(j, "seed_migration_dest", seed_migration_dest_);
        loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
        loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_);
        loadScalar(j, "seed_migration_sweep_opid", seed_migration_sweep_opid_);
        loadScalar(j, "seed_migration_sweep_submitted", seed_migration_sweep_submitted_);
        loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
        loadScalar(j, "unlock_duration", unlock_duration_);
        loadScalar(j, "pin_enabled", pin_enabled_);
        loadScalar(j, "keep_daemon_running", keep_daemon_running_);
        loadScalar(j, "stop_external_daemon", stop_external_daemon_);
        loadScalar(j, "max_connections", max_connections_);
        loadScalar(j, "stratum_host_enabled", stratum_host_enabled_);
        loadScalar(j, "stratum_allowip", stratum_allowip_);
        if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
            const auto& lite = j["lite_wallet"];
            if (lite.contains("server_selection_mode")) {
                lite_server_selection_mode_ = parseLiteServerSelectionPreferenceMode(
                    lite["server_selection_mode"]);
            }
            if (lite.contains("sticky_server_url") && lite["sticky_server_url"].is_string()) {
                lite_sticky_server_url_ = lite["sticky_server_url"].get<std::string>();
            }
            if (lite.contains("chain_name") && lite["chain_name"].is_string()) {
                lite_chain_name_ = lite["chain_name"].get<std::string>();
            }
            // Migration: the SDXL backend only accepts main/test/regtest and hard-panics
            // (process abort) on any other chain name. Older builds persisted the "DRAGONX"
            // ticker here, which crashed the lite backend on launch. Rewrite any invalid
            // value to "main" and flag a re-save so the corrected setting persists.
            if (lite_chain_name_ != "main" && lite_chain_name_ != "test" &&
                lite_chain_name_ != "regtest") {
                lite_chain_name_ = "main";
                needs_upgrade_save_ = true;
            }
            if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_unsigned()) {
                lite_random_selection_seed_ = lite["random_selection_seed"].get<std::size_t>();
            } else if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_integer()) {
                const auto seed = lite["random_selection_seed"].get<long long>();
                lite_random_selection_seed_ = seed > 0 ? static_cast<std::size_t>(seed) : 0;
            }
            if (lite.contains("persist_selected_server") && lite["persist_selected_server"].is_boolean()) {
                lite_persist_selected_server_ = lite["persist_selected_server"].get<bool>();
            }
            if (lite.contains("servers") && lite["servers"].is_array()) {
                lite_servers_.clear();
                for (const auto& server : lite["servers"]) {
                    if (!server.is_object()) continue;
                    LiteServerPreference preference;
                    if (server.contains("url") && server["url"].is_string()) {
                        preference.url = server["url"].get<std::string>();
                    }
                    if (server.contains("label") && server["label"].is_string()) {
                        preference.label = server["label"].get<std::string>();
                    }
                    if (server.contains("enabled") && server["enabled"].is_boolean()) {
                        preference.enabled = server["enabled"].get<bool>();
                    }
                    lite_servers_.push_back(preference);
                }
                // Surface newly-shipped default servers (e.g. lite6/lite7) on an existing
                // install whose saved list predates them. Servers are hidden (see
                // lite_hidden_servers_), never deleted, so appending a missing default won't
                // resurrect one the user intentionally removed.
                for (const auto& def : defaultLiteServers()) {
                    bool present = false;
                    for (const auto& s : lite_servers_)
                        if (s.url == def.url) { present = true; break; }
                    if (!present) lite_servers_.push_back(def);
                }
            }
            if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
                const auto v = lite["rollout_override"].get<std::string>();
                lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
            }
            if (lite.contains("install_id") && lite["install_id"].is_string()) {
                lite_install_id_ = lite["install_id"].get<std::string>();
            }
            if (lite.contains("hidden_servers") && lite["hidden_servers"].is_array()) {
                lite_hidden_servers_.clear();
                for (const auto& u : lite["hidden_servers"])
                    if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
            }
        }
        loadScalar(j, "verbose_logging", verbose_logging_);
        if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
            debug_categories_.clear();
            for (const auto& c : j["debug_categories"])
                if (c.is_string()) debug_categories_.insert(c.get<std::string>());
        }
        loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
        loadScalar(j, "low_spec_mode", low_spec_mode_);
        loadScalar(j, "reduce_motion", reduce_motion_);
        loadScalar(j, "selected_exchange", selected_exchange_);
        loadScalar(j, "selected_pair", selected_pair_);
        loadScalar(j, "chart_interval", chart_interval_);
        loadScalar(j, "chart_style", chart_style_);
        loadScalar(j, "pool_url", pool_url_);
        // Migrate old default pool URL that was missing the stratum port
        if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
        loadScalar(j, "pool_algo", pool_algo_);
        loadScalar(j, "pool_worker", pool_worker_);
        loadScalar(j, "pool_threads", pool_threads_);
        loadScalar(j, "pool_tls", pool_tls_);
        loadScalar(j, "pool_hugepages", pool_hugepages_);
        loadScalar(j, "pool_mode", pool_mode_);
        if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]);
        loadScalar(j, "mine_when_idle", mine_when_idle_);
        loadScalar(j, "xmrig_version", xmrig_version_);
        // Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
        if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
            mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
        loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
        loadScalar(j, "idle_threads_active", idle_threads_active_);
        loadScalar(j, "idle_threads_idle", idle_threads_idle_);
        loadScalar(j, "idle_gpu_aware", idle_gpu_aware_);
        if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
            saved_pool_urls_.clear();
            for (const auto& u : j["saved_pool_urls"])
                if (u.is_string()) saved_pool_urls_.push_back(u.get<std::string>());
        }
        if (j.contains("saved_pool_workers") && j["saved_pool_workers"].is_array()) {
            saved_pool_workers_.clear();
            for (const auto& w : j["saved_pool_workers"])
                if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
        }
        if (j.contains("portfolio_entries") && j["portfolio_entries"].is_array()) {
            portfolio_entries_.clear();
            for (const auto& e : j["portfolio_entries"]) {
                if (!e.is_object()) continue;
                PortfolioEntry entry;
                if (e.contains("label") && e["label"].is_string())
                    entry.label = e["label"].get<std::string>();
                if (e.contains("addresses") && e["addresses"].is_array())
                    for (const auto& a : e["addresses"])
                        if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
                if (e.contains("icon") && e["icon"].is_string())
                    entry.icon = e["icon"].get<std::string>();
                if (e.contains("color") && e["color"].is_number())
                    entry.color = e["color"].get<unsigned int>();
                if (e.contains("outline_opacity") && e["outline_opacity"].is_number_integer())
                    entry.outlineOpacity = e["outline_opacity"].get<int>();
                if (e.contains("price_basis") && e["price_basis"].is_number_integer())
                    entry.priceBasis = e["price_basis"].get<int>();
                if (e.contains("manual_price") && e["manual_price"].is_number())
                    entry.manualPrice = e["manual_price"].get<double>();
                if (e.contains("manual_currency") && e["manual_currency"].is_string())
                    entry.manualCurrency = e["manual_currency"].get<std::string>();
                if (e.contains("show_drgx") && e["show_drgx"].is_boolean())
                    entry.showDrgx = e["show_drgx"].get<bool>();
                if (e.contains("show_value") && e["show_value"].is_boolean())
                    entry.showValue = e["show_value"].get<bool>();
                if (e.contains("show_24h") && e["show_24h"].is_boolean())
                    entry.show24h = e["show_24h"].get<bool>();
                if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
                    entry.showSparkline = e["show_sparkline"].get<bool>();
                if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
                    entry.sparklineInterval = e["sparkline_interval"].get<int>();
                entry.scope = e.value("scope", "");
                if (e.contains("grid_col") && e["grid_col"].is_number_integer())
                    entry.gridCol = e["grid_col"].get<int>();
                if (e.contains("grid_row") && e["grid_row"].is_number_integer())
                    entry.gridRow = e["grid_row"].get<int>();
                if (e.contains("grid_w") && e["grid_w"].is_number_integer())
                    entry.gridW = e["grid_w"].get<int>();
                if (e.contains("grid_h") && e["grid_h"].is_number_integer())
                    entry.gridH = e["grid_h"].get<int>();
                if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
            }
        }
        loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
        loadScalar(j, "window_width", window_width_);
        loadScalar(j, "window_height", window_height_);

        // Version tracking — detect upgrades so we can re-save with new defaults
        loadScalar(j, "settings_version", settings_version_);
        if (settings_version_ != DRAGONX_VERSION) {
            DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
                   settings_version_.empty() ? "(none)" : settings_version_.c_str(),
                   DRAGONX_VERSION);
            needs_upgrade_save_ = true;
        }

        return true;
    } catch (const std::exception& e) {
        DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
        // The file exists but is unparseable (truncated/corrupt). Quarantine it so the
        // next save() doesn't silently overwrite it with defaults — the user's data stays
        // recoverable. Proceed with in-memory defaults.
        file.close();
        std::error_code ec;
        const std::string quarantine =
            path + ".corrupt-" + std::to_string(static_cast<long long>(std::time(nullptr)));
        fs::rename(path, quarantine, ec);
        if (!ec) {
            DEBUG_LOGF("Quarantined corrupt settings to %s\n", quarantine.c_str());
        }
        return false;
    }
}

bool Settings::save()
{
    if (settings_path_.empty()) {
        settings_path_ = getDefaultPath();
    }
    return save(settings_path_);
}

bool Settings::save(const std::string& path)
{
    json j;
    
    j["theme"] = theme_;
    j["save_ztxs"] = save_ztxs_;
    j["auto_shield"] = auto_shield_;
    j["use_tor"] = use_tor_;
    j["allow_custom_fees"] = allow_custom_fees_;
    j["default_fee"] = default_fee_;
    j["fetch_prices"] = fetch_prices_;
    j["tx_explorer_url"] = tx_explorer_url_;
    j["address_explorer_url"] = address_explorer_url_;
    j["language"] = language_;
    j["skin_id"] = skin_id_;
    j["chat_reply_zaddr"] = chat_reply_zaddr_;
    j["muted_chat_cids"] = json::array();
    for (const auto& c : muted_chat_cids_)
        j["muted_chat_cids"].push_back(c);
    j["hidden_chat_cids"] = json::array();
    for (const auto& c : hidden_chat_cids_)
        j["hidden_chat_cids"].push_back(c);
    j["blocked_chat_convs"] = json::array();
    for (const auto& b : blocked_chat_convs_) {
        json o;
        o["cid"] = b.cid;
        o["name"] = b.name;
        j["blocked_chat_convs"].push_back(o);
    }
    j["chat_emoji_color"] = chat_emoji_color_;
    j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
    j["chat_bubble_style"] = chat_bubble_style_;
    j["chat_bubble_accent"] = chat_bubble_accent_;
    j["chat_density"] = chat_density_;
    j["chat_font_scale"] = chat_font_scale_;
    j["chat_time_format"] = chat_time_format_;
    j["chat_enter_sends"] = chat_enter_sends_;
    j["time_format"] = time_format_;
    j["acrylic_enabled"] = acrylic_enabled_;
    j["acrylic_quality"] = acrylic_quality_;
    j["blur_multiplier"] = blur_multiplier_;
    j["noise_opacity"] = noise_opacity_;
    j["gradient_background"] = gradient_background_;
    j["ui_opacity"] = ui_opacity_;
    j["window_opacity"] = window_opacity_;
    j["balance_layout"] = balance_layout_;  // saved as string ID
    j["portfolio_style"] = portfolio_style_;
    j["contacts_view_mode"] = contacts_view_mode_;
    j["contacts_avatar_shape"] = contacts_avatar_shape_;
    j["contacts_list_scale"] = contacts_list_scale_;
    j["animate_avatars"] = animate_avatars_;
    j["scanline_enabled"] = scanline_enabled_;
    j["console_line_accents"] = console_line_accents_;
    j["console_text_color"] = console_text_color_;
    j["console_zoom"] = console_zoom_;
    j["console_auto_focus"] = console_auto_focus_;
    j["hidden_addresses"] = json::array();
    for (const auto& addr : hidden_addresses_)
        j["hidden_addresses"].push_back(addr);
    j["favorite_addresses"] = json::array();
    for (const auto& addr : favorite_addresses_)
        j["favorite_addresses"].push_back(addr);
    {
        json meta_obj = json::object();
        for (const auto& [addr, m] : address_meta_) {
            if (m.label.empty() && m.icon.empty() && m.sortOrder < 0 && !m.mining) continue;
            json entry = json::object();
            if (!m.label.empty()) entry["label"] = m.label;
            if (!m.icon.empty())  entry["icon"]  = m.icon;
            if (m.sortOrder >= 0) entry["order"] = m.sortOrder;
            if (m.mining) entry["mining"] = true;
            meta_obj[addr] = entry;
        }
        j["address_meta"] = meta_obj;
    }
    j["wizard_completed"] = wizard_completed_;
    j["seed_backup_reminded"] = seed_backup_reminded_;
    j["large_wallet_warned"] = large_wallet_warned_;
    j["empty_wallet_warning_acked"] = json::array();
    for (const auto& w : empty_wallet_warning_acked_)
        j["empty_wallet_warning_acked"].push_back(w);
    j["encryption_pending"] = encryption_pending_;
    j["daemon_update_prompted_size"] = daemon_update_prompted_size_;
    j["active_wallet_file"] = active_wallet_file_;
    j["seed_migration_pending"] = seed_migration_pending_;
    j["seed_migration_dest"] = seed_migration_dest_;
    j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
    j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_;
    j["seed_migration_sweep_opid"] = seed_migration_sweep_opid_;
    j["seed_migration_sweep_submitted"] = seed_migration_sweep_submitted_;
    j["auto_lock_timeout"] = auto_lock_timeout_;
    j["unlock_duration"] = unlock_duration_;
    j["pin_enabled"] = pin_enabled_;
    j["keep_daemon_running"] = keep_daemon_running_;
    j["stop_external_daemon"] = stop_external_daemon_;
    j["max_connections"] = max_connections_;
    j["stratum_host_enabled"] = stratum_host_enabled_;
    j["stratum_allowip"] = stratum_allowip_;
    {
        json lite = json::object();
        lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
        lite["sticky_server_url"] = lite_sticky_server_url_;
        lite["chain_name"] = lite_chain_name_;
        lite["random_selection_seed"] = lite_random_selection_seed_;
        lite["persist_selected_server"] = lite_persist_selected_server_;
        lite["servers"] = json::array();
        for (const auto& server : lite_servers_) {
            json entry = json::object();
            entry["url"] = server.url;
            entry["label"] = server.label;
            entry["enabled"] = server.enabled;
            lite["servers"].push_back(entry);
        }
        lite["rollout_override"] = lite_rollout_override_;
        lite["install_id"] = lite_install_id_;
        lite["hidden_servers"] = json::array();
        for (const auto& u : lite_hidden_servers_) lite["hidden_servers"].push_back(u);
        j["lite_wallet"] = lite;
    }
    j["verbose_logging"] = verbose_logging_;
    j["debug_categories"] = json::array();
    for (const auto& cat : debug_categories_)
        j["debug_categories"].push_back(cat);
    j["theme_effects_enabled"] = theme_effects_enabled_;
    j["low_spec_mode"] = low_spec_mode_;
    j["reduce_motion"] = reduce_motion_;
    j["selected_exchange"] = selected_exchange_;
    j["selected_pair"] = selected_pair_;
    j["chart_interval"] = chart_interval_;
    j["chart_style"] = chart_style_;
    j["pool_url"]       = pool_url_;
    j["pool_algo"]      = pool_algo_;
    j["pool_worker"]    = pool_worker_;
    j["pool_threads"]   = pool_threads_;
    j["pool_tls"]       = pool_tls_;
    j["pool_hugepages"] = pool_hugepages_;
    j["pool_mode"]      = pool_mode_;
    j["pool_select_mode"] = poolSelectModeName(pool_select_mode_);
    j["mine_when_idle"] = mine_when_idle_;
    j["xmrig_version"]  = xmrig_version_;
    j["mine_idle_delay"]= mine_idle_delay_;
    j["idle_thread_scaling"]  = idle_thread_scaling_;
    j["idle_threads_active"] = idle_threads_active_;
    j["idle_threads_idle"]   = idle_threads_idle_;
    j["idle_gpu_aware"]      = idle_gpu_aware_;
    j["saved_pool_urls"] = json::array();
    for (const auto& u : saved_pool_urls_)
        j["saved_pool_urls"].push_back(u);
    j["saved_pool_workers"] = json::array();
    for (const auto& w : saved_pool_workers_)
        j["saved_pool_workers"].push_back(w);
    j["portfolio_entries"] = json::array();
    for (const auto& e : portfolio_entries_) {
        json entry;
        entry["label"] = e.label;
        entry["addresses"] = json::array();
        for (const auto& a : e.addresses) entry["addresses"].push_back(a);
        entry["icon"] = e.icon;
        entry["color"] = e.color;
        entry["outline_opacity"] = e.outlineOpacity;
        entry["price_basis"] = e.priceBasis;
        entry["manual_price"] = e.manualPrice;
        entry["manual_currency"] = e.manualCurrency;
        entry["show_drgx"] = e.showDrgx;
        entry["show_value"] = e.showValue;
        entry["show_24h"] = e.show24h;
        entry["show_sparkline"] = e.showSparkline;
        entry["sparkline_interval"] = e.sparklineInterval;
        entry["scope"] = e.scope;
        entry["grid_col"] = e.gridCol;
        entry["grid_row"] = e.gridRow;
        entry["grid_w"] = e.gridW;
        entry["grid_h"] = e.gridH;
        j["portfolio_entries"].push_back(std::move(entry));
    }
    j["font_scale"]     = font_scale_;
    j["settings_version"] = std::string(DRAGONX_VERSION);
    if (window_width_ > 0 && window_height_ > 0) {
        j["window_width"]  = window_width_;
        j["window_height"] = window_height_;
    }
    
    try {
        // Atomic + durable: write to a temp file, fsync, then rename over the real file.
        // A crash mid-write can no longer truncate settings.json (which would silently
        // reset every preference on the next launch). Owner-only (0600) — it carries the
        // lite-server list and address metadata.
        return util::Platform::writeFileAtomically(path, j.dump(4), /*restrictPermissions=*/true);
    } catch (const std::exception& e) {
        DEBUG_LOGF("Failed to save settings: %s\n", e.what());
        return false;
    }
}

} // namespace config
} // namespace dragonx
