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

#pragma once

#include <memory>
#include <string>
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <deque>
#include <condition_variable>
#include <nlohmann/json_fwd.hpp>
#include "data/transaction_history_cache.h"
#include "data/address_book.h"
#include "data/wallet_index.h"
#include "data/wallet_state.h"
#include "rpc/connection.h"
#include "services/network_refresh_service.h"
#include "services/wallet_security_controller.h"
#include "services/wallet_security_workflow.h"
#include "util/async_task_manager.h"
#include "util/pool_stats_service.h"
#include "wallet/wallet_capabilities.h"
#include "chat/chat_service.h"
#include "chat/chat_database.h"
#include "ui/sidebar.h"
#include "ui/windows/console_tab.h"
#include "imgui.h"

// Forward declarations
namespace dragonx {
    namespace rpc { 
        class RPCClient;
        class RPCWorker;
    }
    namespace config { class Settings; }
    namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
    namespace util { class Bootstrap; class SecureVault; }
    namespace wallet { class LiteWalletController; struct LiteWalletAppRefreshModel; }
}

namespace dragonx {

/**
 * @brief First-run wizard states
 */
enum class WizardPhase {
    None,               // No wizard — normal operation
    BootstrapOffer,     // Step 1: offer bootstrap download
    BootstrapInProgress,// downloading / extracting
    BootstrapFailed,    // error with retry/skip option
    Appearance,         // Step 2: visual effects / performance options
    EncryptOffer,       // Step 3: offer wallet encryption
    EncryptInProgress,  // encrypting + daemon restarting
    PinSetup,           // Step 4: optional PIN setup (after encryption)
    Done                // wizard complete, launch normally
};

/**
 * @brief Encrypt-wallet dialog phases (settings page flow)
 */
enum class EncryptDialogPhase {
    PassphraseEntry,    // Enter & confirm passphrase
    Encrypting,         // In-progress animation
    PinSetup,           // Offer PIN after successful encryption
    Done                // Finished — close dialog
};

// A status string written by a background/worker thread and read every frame by the UI thread. Its
// operator= locks, so all the plain `x = "..."` assignment sites stay unchanged; readers call get()
// for a consistent per-frame snapshot instead of racing a non-atomic std::string. (M-05, L-06)
class GuardedStatus {
public:
    GuardedStatus() = default;
    GuardedStatus& operator=(std::string v) { std::lock_guard<std::mutex> lk(m_); v_ = std::move(v); return *this; }
    std::string get() const { std::lock_guard<std::mutex> lk(m_); return v_; }
private:
    mutable std::mutex m_;
    std::string v_;
};

/**
 * @brief Main application class
 *
 * Manages application state, RPC connection, and coordinates UI rendering.
 */
class App {
public:
    App();
    void wipeSecrets();   // scrub all resident secret buffers; called from ~App() AND the forced-exit path (L-05)
    ~App();

    // Non-copyable
    App(const App&) = delete;
    App& operator=(const App&) = delete;

    /**
     * @brief Initialize the application
     * @return true if initialization succeeded
     */
    bool init();

    /**
     * @brief Update application state (called every frame)
     */
    void update();

    /**
     * @brief Pre-frame tasks that must run BEFORE ImGui::NewFrame().
     *
     * Font atlas rebuilds (hot-reload, user font scale changes) must
     * happen before NewFrame() because NewFrame() caches font pointers.
     * Rebuilding mid-frame causes dangling pointer crashes.
     */
    void preFrame();

    /**
     * @brief Render the application UI (called every frame)
     */
    void render();

    /**
     * @brief Clean shutdown
     */
    void shutdown();

    /**
     * @brief Check if app should exit (render loop can stop)
     * Enforces minimum 1-second display of shutdown screen so user sees feedback.
     */
    bool shouldQuit() const {
        if (!quit_requested_ || !shutdown_complete_) return false;
        auto elapsed = std::chrono::steady_clock::now() - shutdown_start_time_;
        return elapsed >= std::chrono::seconds(1);
    }

    /**
     * @brief Request application exit — begins async shutdown
     */
    void requestQuit();

    /**
     * @brief Begin graceful shutdown (called on window close)
     * Starts daemon stop on a background thread while UI keeps rendering.
     */
    void beginShutdown();

    /**
     * @brief Whether we are in the shutdown phase
     */
    bool isShuttingDown() const { return shutting_down_; }
    // True while the wallet-switch progress modal is open — keeps the frame loop redrawing so its live
    // phase/spinner update in real time even when the app is otherwise idle.
    bool isWalletSwitchInProgress() const { return wallet_switch_dialog_open_.load(); }
    wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
    bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
    bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
    bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }

    // Daemon (v1.3.0+) coinbase auto-shield status, from z_autoshieldstatus. "Not probed" / all-false on
    // pre-1.3.0 daemons (no such RPC) — callers treat that as "the wallet handles auto-shield itself".
    bool daemonAutoShieldProbed() const { return daemon_autoshield_probed_; }
    bool daemonAutoShieldActive() const { return daemon_autoshield_active_; }
    const std::string& daemonAutoShieldAddress() const { return daemon_autoshield_address_; }
    const std::string& daemonAutoShieldDisabledReason() const { return daemon_autoshield_disabled_reason_; }
    bool daemonAutoShieldSeedRecoverable() const { return daemon_autoshield_seed_recoverable_; }

    // W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state)
    // for the "Copy diagnostics" action. Contains no secrets.
    std::string buildDiagnosticsReport();
    bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
    bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
    bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }

    /**
     * @brief Render the shutdown overlay (called instead of normal UI during shutdown)
     */
    void renderShutdownScreen();

    /**
     * @brief Tail the last N lines of the daemon's debug.log (best-effort, reads only the file tail).
     * Fallback for the shutdown screen when we have no captured stdout — e.g. an external daemon we
     * attached to rather than spawned — so the user can still see the node flushing/exiting.
     */
    std::vector<std::string> tailDaemonDebugLog(int maxLines) const;

    // True when the daemon's debug.log shows an in-progress Sapling witness-cache rebuild (best-effort
    // heuristic). Stopping the daemon during one discards it and forces a multi-minute redo next launch.
    bool daemonWitnessRebuildActive() const;
    // Whether beginShutdown() should pause and confirm before stopping the daemon (rebuild in progress).
    bool shouldConfirmDaemonStop() const;
    // The "node is rebuilding — stop anyway / keep running / cancel" modal, rendered from render().
    void renderDaemonStopConfirm();

    /**
     * @brief Render loading overlay in content area while daemon is starting/syncing
     * @param contentH Height of the content area child window
     */
    void renderLoadingOverlay(float contentH);

    // Accessors for subsystems
    rpc::RPCClient* rpc() { return rpc_.get(); }
    rpc::RPCWorker* worker() { return worker_.get(); }
    // Console backend accessors (fast-lane-preferring, defined in app.cpp where the
    // subsystem types are complete) used by the shared console executor.
    rpc::RPCClient* consoleRpc();
    rpc::RPCWorker* consoleWorker();
    daemon::EmbeddedDaemon* consoleDaemon();
    daemon::XmrigManager* consoleXmrig();
    config::Settings* settings() { return settings_.get(); }
    // Request a font-atlas rebuild before the next frame (e.g. after toggling color emoji). Handled in
    // preFrame() via Typography::reload — safe to call from UI code mid-frame.
    void requestFontRebuild() { font_rebuild_requested_ = true; }
    // Lite wallet controller (non-null only in lite builds with a linked backend).
    wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
    // HushChat service (identity + in-memory message store); the Chat tab reads its store.
    chat::ChatService& chatService() { return chat_service_; }
    // HushChat composing: construct, broadcast (broadcastChatMemos), and locally echo an outgoing
    // message (to a conversation whose peer key we know) / a new-conversation contact request.
    void sendChatMessage(const std::string& conversationId, const std::string& text);
    void startChatConversation(const std::string& peerZaddr, const std::string& text);
    // Send a contact request into an existing conversation (used to retry a failed request in place).
    void sendContactRequestForCid(const std::string& cid, const std::string& peerZaddr,
                                  const std::string& text);
    // Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
    // (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
    // the chat feature is off.
    void seedChatDemoData();
    // Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
    const std::string& liteOpenError() const { return lite_open_error_; }
    // Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
    void requestLiteUnlock() { lite_unlock_prompt_ = true; }
    // Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock`
    // doesn't update state_.locked until the next poll, so chat secrets would otherwise linger).
    bool lockLiteWallet();
    // (Re)build the lite controller from current settings so a changed lite-server selection
    // takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
    void rebuildLiteWallet(bool force = false);
    WalletState& state() { return state_; }
    const WalletState& state() const { return state_; }
    const WalletState& getWalletState() const { return state_; }
    // Shared contact store (Contacts tab / Send picker / future Chat roster). App-owned so
    // every surface reads one source of truth instead of a per-dialog singleton.
    data::AddressBook& addressBook() { return address_book_; }
    const data::AddressBook& addressBook() const { return address_book_; }

    // Hash of the active wallet's identity (derived from its address list). This is the tx-history
    // cache key — it changes when the address set changes, so DON'T scope persistent user data on it.
    std::string activeWalletIdentityHash() const;

    // Stable per-wallet id ("w:"+hex) for scoping persistent per-wallet data (address-book contacts).
    // Generated once and persisted in the wallet index (keyed by wallet file), never recomputed from
    // the mutable address set — so creating addresses / locking / disconnecting never changes it.
    // Empty only when there is no active wallet file. Establishes + persists the id on first call.
    std::string activeWalletScopeId();

    data::WalletIndex& walletIndex() { return wallet_index_; }
    const data::WalletIndex& walletIndex() const { return wallet_index_; }

    // Connection state (convenience wrappers)
    bool isConnected() const { return state_.connected; }
    int getBlockHeight() const { return state_.sync.blocks; }
    const std::string& getConnectionStatus() const { return connection_status_; }
    std::string getDaemonStatus() const;  // thread-safe copy (daemon_status_ is written off-thread)

    // Balance info (convenience wrappers)
    double getShieldedBalance() const { return state_.shielded_balance; }
    double getTransparentBalance() const { return state_.transparent_balance; }
    double getTotalBalance() const { return state_.total_balance; }
    double getUnconfirmedBalance() const { return state_.unconfirmed_balance; }

    // Addresses
    const std::vector<AddressInfo>& getZAddresses() const { return state_.z_addresses; }
    const std::vector<AddressInfo>& getTAddresses() const { return state_.t_addresses; }

    // Transactions
    const std::vector<TransactionInfo>& getTransactions() const { return state_.transactions; }

    // Mining
    const MiningInfo& getMiningInfo() const { return state_.mining; }
    void startMining(int threads);
    void stopMining();
    bool isMiningToggleInProgress() const { return mining_toggle_in_progress_.load(std::memory_order_relaxed); }

    // Pool mining (xmrig)
    void startPoolMining(int threads);
    void stopPoolMining();
    int getXmrigRequestedThreads() const {
        return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
    }
    // True while the pool miner process is live — used to refuse replacing the binary under it.
    bool isPoolMinerRunning() const {
        return xmrig_manager_ && xmrig_manager_->isRunning();
    }

    // Auto-balance: latest per-pool hashrate snapshot (for the mining tab pool list),
    // and a request to refresh it now (Refresh button / switching into Auto mode).
    util::PoolStatsService::Snapshot poolStatsSnapshot() const {
        return pool_stats_service_.snapshot();
    }
    void requestPoolBalanceRefresh() { balance_refresh_pending_ = true; }

    // Installed miner version (detected from `xmrig --version`, cached; kicks the one-shot
    // detection on first call) so the mining tab can show it before mining starts.
    std::string poolMiningInstalledVersion();

    // Mine-when-idle state query
    bool isIdleMiningActive() const { return idle_mining_active_; }

    // Peers
    const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
    const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
    bool isPeerRefreshInProgress() const {
        return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
    }
    void banPeer(const std::string& ip, int duration_seconds = 86400);
    void unbanPeer(const std::string& ip);
    void clearBans();

    // Address operations
    void createNewZAddress(std::function<void(const std::string&)> callback = nullptr);
    void createNewTAddress(std::function<void(const std::string&)> callback = nullptr);

    // Hide/unhide addresses from the address list (persisted in settings)
    void hideAddress(const std::string& addr);
    void unhideAddress(const std::string& addr);
    bool isAddressHidden(const std::string& addr) const;
    int getHiddenAddressCount() const;

    void favoriteAddress(const std::string& addr);
    void unfavoriteAddress(const std::string& addr);
    bool isAddressFavorite(const std::string& addr) const;

    // Address metadata (labels, icons, custom ordering)
    void setAddressLabel(const std::string& addr, const std::string& label);
    void setAddressIcon(const std::string& addr, const std::string& icon);
    std::string getAddressLabel(const std::string& addr) const;
    std::string getAddressIcon(const std::string& addr) const;
    int getAddressSortOrder(const std::string& addr) const;
    void setAddressSortOrder(const std::string& addr, int order);
    int getNextSortOrder() const;
    void swapAddressOrder(const std::string& a, const std::string& b);
    // Assign dense sort orders (0..N-1) to the given addresses in the given order and
    // persist once. Used by drag-reorder so a drop always takes effect (even from the
    // default un-ordered state, where a pairwise swap would be a no-op).
    void reorderAddresses(const std::vector<std::string>& orderedAddrs);
    bool isMiningAddress(const std::string& addr) const;
    void setMiningAddress(const std::string& addr, bool mining);
    void invalidateAddressValidationCache();

    // Key export/import
    void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
    // callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
    void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
    // callback(success, errorOrEmpty, importedAddress). address is "" on failure or when the RPC
    // returns none; the import routes to z_importviewingkey / z_importkey / importprivkey by key type.
    // startHeight > 0 rescans from that block (shielded RPCs only; ignored for transparent WIF).
    void importPrivateKey(const std::string& key, int startHeight,
                          std::function<void(bool, const std::string&, const std::string&)> callback);

    // Sweep a spending key: import it (rescan) then z_sendmany ALL its funds (balance − fee) to a
    // destination you own — a freshly generated shielded address when destMode == 0, else destExisting.
    // Drives the sweep_step_ / sweep_status_ / sweep_txid_ state; reuses the async-operation tracker.
    void sweepPrivateKey(const std::string& key, int startHeight, int destMode,
                         const std::string& destExisting);

    // Wallet backup
    void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
    // Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives
    // (ok, noMnemonic, phrase, error): ok+phrase on success; noMnemonic=true when the
    // wallet's seed is not mnemonic-derived (legacy wallet). Full-node only; the phrase
    // is a secret and is wiped after the callback returns.
    void exportSeedPhrase(std::function<void(bool ok, bool noMnemonic,
                                             const std::string& phrase,
                                             const std::string& error)> callback);

    // Transaction operations
    void sendTransaction(const std::string& from, const std::string& to,
                        double amount, double fee, const std::string& memo,
                        std::function<void(bool success, const std::string& result)> callback);

    // Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress /
    // auto-shield) with the shared opid poller so its eventual success/failure is
    // surfaced and balances/transactions refresh on completion. z_sendmany uses the
    // richer pending-send path internally; this is for operations with no optimistic
    // transaction row of their own.
    void trackOperation(const std::string& opid);

    // Force refresh
    void refreshNow();
    // Called on window restore: drop the daemon-output backlog that accumulated while minimized (the
    // per-frame update loop was paused), so a background witness rebuild that started+finished during
    // the minimize isn't parsed in one batch and mistaken for a completed rescan (spurious toast).
    void skipDaemonOutputBacklog();
    void refreshMiningInfo();
    void refreshPeerInfo();
    void refreshMarketData();
    // Fetch the live exchange/pair list from CoinGecko once per session (venues are
    // near-static); populates state.market.exchanges. Safe to call every frame.
    void refreshExchanges();
    // Fetch historical USD price series (CoinGecko market_chart) that back the portfolio
    // sparkline intervals; self-throttled to ~30 min. Safe to call every frame.
    void refreshMarketChart();
    // Fetch the SELECTED pair's candles from that exchange's own API (data/exchange_candles.h) so the
    // Market chart shows the real per-exchange price. Re-fetches on pair change; falls back to the
    // CoinGecko aggregate for unmapped venues / failed fetches. Safe to call every frame.
    void refreshExchangeChart();
    // True while a market price-history fetch is in flight (CoinGecko aggregate OR per-exchange). The
    // Market chart shows a loading indicator instead of the empty state during pair switches.
    bool isMarketChartLoading() const { return chart_fetch_in_flight_ || exchange_chart_fetch_in_flight_; }

    /// @brief Per-category refresh intervals, adjusted by active tab
    using RefreshIntervals = services::NetworkRefreshService::Intervals;

    /// @brief Get recommended refresh intervals for a given page
    static RefreshIntervals getIntervalsForPage(ui::NavPage page);

    // UI navigation
    void setCurrentPage(ui::NavPage page);
    ui::NavPage getCurrentPage() const { return current_page_; }

    // Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
    // timestamped folder under the config dir. Driven from App::render(); main.cpp polls
    // wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
    // onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
    void startScreenshotSweep();
    // Full UI sweep: like the tab sweep, but also drives every modal / dialog / multi-step flow /
    // state overlay into view (with injected demo data, offline, firing no live ops) and captures
    // each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
    void startFullUiSweep();
    std::string screenshotFullDir() const;
    // Debug option: restrict either sweep to just the currently-active theme instead of cycling all.
    bool sweepCurrentThemeOnly() const { return sweep_current_theme_only_; }
    void setSweepCurrentThemeOnly(bool v) { sweep_current_theme_only_ = v; }
    bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
    bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
    const std::string& screenshotSweepPath() const { return sweep_current_path_; }
    void onScreenshotCaptured();
    std::string screenshotDir() const;   // <config>/screenshots (fixed; sweeps overwrite in place)

    // Dialog triggers (used by settings page to open modal dialogs)
    void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; }        // spending
    void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; }  // watch-only
    void showExportKeyDialog() { show_export_key_ = true; }
    void showBackupDialog() { show_backup_ = true; }
    void showSeedBackupDialog() { show_seed_backup_ = true; }
    void showSeedMigrationDialog();  // opens the migration modal (resumes a pending one at Sweep)
    // True when the current full-node wallet is a legacy, pre-seed-phrase wallet (no BIP39 mnemonic)
    // that a capable daemon could migrate — the Migrate-to-seed button glows to nudge the user.
    bool isPreSeedWallet() const { return wallet_seed_status_ == WalletSeedStatus::NoMnemonic; }
    // Authoritative BIP39-mnemonic status of the ACTIVE wallet, decided at runtime by z_exportmnemonic
    // (not the offline file probe, which can't tell an HD-but-no-mnemonic wallet from a seed-phrase one).
    // 0 = unknown/undecidable, 1 = has a seed phrase, 2 = legacy (no mnemonic).
    int activeWalletSeedBadge() const {
        if (wallet_seed_status_ == WalletSeedStatus::HasMnemonic) return 1;
        if (wallet_seed_status_ == WalletSeedStatus::NoMnemonic)  return 2;
        return 0;
    }
    void showAboutDialog() { show_about_ = true; }
    void showFaqDialog() { show_faq_ = true; }
    
    // Legacy tab compat — maps int to NavPage
    void setCurrentTab(int tab);
    int getCurrentTab() const { return static_cast<int>(current_page_); }
    
    // Payment URI handling
    void handlePaymentURI(const std::string& uri);
    bool hasPendingPayment() const { return pending_payment_valid_; }
    void clearPendingPayment() { pending_payment_valid_ = false; }
    std::string getPendingToAddress() const { return pending_to_address_; }
    double getPendingAmount() const { return pending_amount_; }
    std::string getPendingMemo() const { return pending_memo_; }
    std::string getPendingLabel() const { return pending_label_; }
    
    // Embedded daemon control
    bool startEmbeddedDaemon();
    void stopEmbeddedDaemon();
    // Stop the node specifically for a wallet switch, which MUST free the RPC port to relaunch on
    // -wallet=<name>. Unlike stopEmbeddedDaemon() (whose DisconnectOnly policy leaves an adopted/external
    // daemon running), this sends a graceful RPC "stop" — using our own connection creds, so only our
    // daemon obeys it — even to an adopted daemon, then waits (bounded) for the port to actually release.
    // Returns true once the RPC port is free (or we're shutting down).
    bool stopDaemonForWalletSwitch();
    bool isEmbeddedDaemonRunning() const;
    bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
    void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
    void rescanBlockchain();  // restart daemon with -rescan flag (full-history nodes)
    // Runtime rescanblockchain RPC starting at a snapshot-available height. Unlike the
    // -rescan restart, this works on bootstrapped/pruned nodes (which lack pre-snapshot
    // block data), reconciling the wallet's stale spent-state without a daemon restart.
    void runtimeRescan(int startHeight);
    // Async binary-search probe for the lowest block height the node still has on disk.
    // cb(ok, lowestHeight, fullHistory): fullHistory==true when genesis is present (a normal,
    // non-bootstrapped node). Runs on the UI thread via the RPC worker callbacks.
    void detectLowestAvailableBlockHeight(std::function<void(bool ok, int lowestHeight, bool fullHistory)> cb);
    // Flag that a bootstrap just finished so the wallet auto-reconciles spent-state once the
    // daemon is back up (consumed in update()).
    void markPostBootstrapRescanPending() { post_bootstrap_rescan_pending_ = true; }
    bool runtimeRescanActive() const { return runtime_rescan_active_; }
    void repairWallet();  // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
    void reinstallBundledDaemon();  // stop daemon, overwrite installed binary with the bundled one, restart
    void deleteBlockchainData();  // stop daemon, delete chain data, restart fresh
    bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
    bool isBootstrapDownloading() const { return bootstrap_downloading_; }
    void setBootstrapDownloading(bool v) { bootstrap_downloading_ = v; }

    // Get daemon memory usage in MB (uses embedded daemon handle if available,
    // falls back to platform-level process scan for external daemons)
    double getDaemonMemoryUsageMB() const;

    // Diagnostic string describing daemon memory detection path (for debugging)
    const std::string& getDaemonMemDiag() const { return daemon_mem_diag_; }

    // Background gradient overlay texture
    void setGradientTexture(ImTextureID tex) { gradient_tex_ = tex; }
    ImTextureID getGradientTexture() const { return gradient_tex_; }

    // Logo texture accessor (wallet branding icon)
    ImTextureID getLogoTexture() const { return logo_tex_; }
    int getLogoWidth() const { return logo_w_; }
    int getLogoHeight() const { return logo_h_; }

    // Coin logo texture accessor (DragonX currency icon for balance tab)
    ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }

    // DragonX custom chat emoji (the ":drgx:" shortcode) — the mark recolored to the theme accent (like
    // the logo), re-rasterized on theme change. Used by the emoji picker tile + inline in chat bubbles.
    ImTextureID getDrgxEmojiTexture() const { return drgx_emoji_tex_; }

    /**
     * @brief Reload theme images (background gradient + logo) from new paths
     * @param bgPath Path to background image override (empty = use default)
     * @param logoPath Path to logo image override (empty = use default)
     */
    void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);

    // Load / recolor-per-theme the DragonX header logo (SVG rasterized to the theme accent). Called at
    // the top of render() so the wizard, lock screen, and main header all show it.
    void ensureLogoTexture();

    // Wizard / first-run
    WizardPhase getWizardPhase() const { return wizard_phase_; }
    bool isFirstRun() const;

    /**
     * @brief Stop daemon and re-run the setup wizard
     * Called from Settings. Daemon restarts automatically when wizard completes.
     */
    void restartWizard();

    /**
     * @brief Restart the embedded daemon (e.g. after changing debug categories)
     * Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
     */
    void restartDaemon();
    // Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
    // (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
    // via the identity-scoped caches (P1). No-op if that wallet is already active.
    // stopDaemonConfirmed: skip the "stop the running node?" confirmation (set true when re-entered from
    // that dialog). salvage: start the target node with -salvagewallet (repair a corrupt wallet).
    void switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed = false, bool salvage = false);
    // Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file.
    void processWalletSwitchRevert();

    // Wallet encryption helpers
    void encryptWalletWithPassphrase(const std::string& passphrase);
    // Post-encrypt daemon restart: the daemon shuts itself down after
    // encryptwallet, so restart it off the main thread. Shared by the
    // immediate and deferred encryption continuations. When
    // announceRestartStatus is true, connection_status_ is updated first so
    // the loading overlay explains the restart.
    void restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus);
    void unlockWallet(const std::string& passphrase, int timeout);
    void lockWallet();
    void changePassphrase(const std::string& oldPass, const std::string& newPass);

    // Dialog triggers for encryption (from settings page)
    void showEncryptDialog() {
        show_encrypt_dialog_ = true;
        encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
        encrypt_status_.clear();
        memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
        memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
        memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
        memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
        enc_dlg_saved_passphrase_.clear();
        enc_dlg_pin_status_.clear();
    }
    void showChangePassphraseDialog() { show_change_passphrase_ = true; }
    void showDecryptDialog() {
        show_decrypt_dialog_ = true;
        wallet_security_workflow_.reset();
        memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
    }

    // Dialog triggers for PIN (from settings page)
    void showPinSetupDialog() { show_pin_setup_ = true; pin_status_.clear(); }
    void showPinChangeDialog() { show_pin_change_ = true; pin_status_.clear(); }
    void showPinRemoveDialog() { show_pin_remove_ = true; pin_status_.clear(); }
    bool hasPinVault() const;

    // Debug-options gate: does revealing the debug dropdown need re-authentication (a PIN vault or
    // an encrypted wallet)? And verify the entered PIN/passphrase — cb(ok) is invoked on the main
    // thread (PIN via the vault, else the wallet passphrase via RPC).
    bool debugGateRequiresAuth() const;
    void verifyDebugCredential(const std::string& secret, std::function<void(bool)> cb);

    /// @brief Check if RPC worker has queued results waiting to be processed
    bool hasPendingRPCResults() const;
    bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
    std::string transactionSendProgressText() const;
    std::string transactionRefreshProgressText() const;

    // Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
    // short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
    // something the user copied afterwards). Only a hash of the secret is retained, never the
    // plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
    void copySecretToClipboard(const std::string& secret);
    void pumpSecretClipboardClear();
    // Immediately clear the clipboard if it still holds the armed secret (ignores the 45s timer).
    // Called on app shutdown so a copied key/seed does not outlive the process in the OS clipboard.
    void clearSecretClipboardIfArmed();
    bool isTransactionRefreshInProgress() const {
        return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
    }

private:
    friend class AppDaemonLifecycleRuntime;
    friend class AppDaemonLifecycleTaskContext;

    // Global keyboard-shortcut handling, dispatched once per frame from update().
    void handleGlobalShortcuts();

    bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
    void maybeFinishTransactionSendProgress();
    // Shared body of createNewZAddress/createNewTAddress (which are thin public forwarders).
    // `shielded` selects the z_getnewaddress/getnewaddress RPC, the "shielded"/"transparent" type
    // string, and the z_addresses/t_addresses target (the new AddressInfo is pushed into both that
    // list and state_.addresses). Lite builds derive locally via the controller and early-return.
    void createNewAddress(bool shielded, std::function<void(const std::string&)> callback);
    void upsertPendingSendTransaction(const std::string& opid,
                                      const std::string& from,
                                      const std::string& to,
                                      double amount,
                                      const std::string& memo,
                                      double fee = 0.0);
    // Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
    // total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
    // fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
    // failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
    // self-output that lifts the daemon's selection target past the boundary so it grabs another
    // note; the recipient still receives the exact amount. Returns true if a retry was issued.
    bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
    void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
                                    double amount, double fee, const std::string& memo,
                                    std::function<void(bool, const std::string&)> callback);
    // Shared z_sendmany submit path for sendTransaction (single recipient, markFeeGapRetry=false)
    // and resendWithFeeGapWorkaround (recipient + self-output, markFeeGapRetry=true). Owns the
    // in-flight increment, the worker post + call, and the result closure (dirty flags, opid
    // tracking, pending-send bookkeeping, callback delivery). Callers build `recipients` (and pass
    // the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
    // When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
    // retry of a retry is reported as a real error.
    // background=true (autonomous chat sends / note-buffer splits): keep the single-flight + opid
    // accounting but DON'T raise the global "transaction in progress" UI (status is on the chat message).
    void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
                         const std::string& memo, const nlohmann::json& recipients,
                         const char* traceLabel, bool markFeeGapRetry,
                         std::function<void(bool, const std::string&)> callback,
                         bool background = false);
    void markPendingSendTransactionSucceeded(const std::string& opid,
                                             const std::string& txid);
    void removePendingSendTransactions(const std::vector<std::string>& opids,
                                       bool restoreBalances);
    // Apply a signed per-address balance delta for a pending send: walk z_addresses then
    // t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
    // restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
    // private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
    // same clamp. Shared by the three pending-send delta sites so they can't drift.
    void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
                               bool includeAggregates);
    // Deliver a deferred z_sendmany result to its waiting UI callback once the opid
    // reaches a terminal status. Returns true if a callback was registered (and fired).
    bool invokeSendResultCallback(const std::string& opid, bool ok,
                                  const std::string& result);
    void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
    std::string transactionHistoryCacheWalletIdentity() const;
    bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
    // Record the active wallet's cached metadata (balance, address count, identity, size) into the
    // wallet index (wallets.json). Cheap + throttled: only writes when a value changed. markOpened
    // stamps last-opened + syncedHere (call once per connect).
    void updateWalletIndexForActiveWallet(bool markOpened);
    void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
    // Shared main-thread continuations for a wallet unlock attempt, so the passphrase
    // and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
    // curve on every failed path (a PIN RPC failure used to skip it).
    void applyUnlockSuccess(const std::string& passphrase, int timeout);
    void applyUnlockFailure(const std::string& errorMessage);
    // Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
    // rescan-completion sites so a new witness field can't be forgotten in one copy.
    void resetWitnessRescanProgress();
    void loadTransactionHistoryCacheIfAvailable();
    void storeTransactionHistoryCacheIfAvailable();
    void wipePendingTransactionHistoryCachePassphrase();
    void resetTransactionHistoryCacheSession();
    void pruneShieldedHistoryScanProgress();
    void invalidateShieldedHistoryScanProgress(bool persistCache);

    // Auto-balance pool selection: drive the periodic hashrate refresh and apply a
    // freshly-completed snapshot (weighted-random pick + optional miner restart).
    void updatePoolAutoBalance();
    void applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap);

    // Subsystems
    std::unique_ptr<rpc::RPCClient> rpc_;
    std::unique_ptr<rpc::RPCWorker> worker_;

    // Fast-lane: dedicated RPC connection + worker for 1-second mining polls.
    // Runs on its own thread with its own curl handle so it never blocks behind
    // the main refresh batch.
    std::unique_ptr<rpc::RPCClient> fast_rpc_;
    std::unique_ptr<rpc::RPCWorker> fast_worker_;

    // Saved connection credentials (needed to open the fast-lane connection)
    rpc::ConnectionConfig saved_config_;

    std::unique_ptr<config::Settings> settings_;
    std::unique_ptr<wallet::LiteWalletController> lite_wallet_;  // lite builds w/ linked backend
    // Pending send_tab callback for an in-flight lite send (delivered in update() once the
    // controller's async broadcast result arrives). Only one lite send runs at a time.
    std::function<void(bool, const std::string&)> lite_send_callback_;
    // One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
    // init() so a slow initialize_existing network call doesn't freeze startup before the window).
    bool lite_autoopen_done_ = false;
    double lite_open_last_attempt_ = 0.0;  // ImGui time of the last async open attempt (retry timer)
    // Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
    // the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
    std::string lite_open_error_;

    // HushChat (experimental; gated by DRAGONX_ENABLE_CHAT — inert when OFF). App owns the chat
    // service so the transaction-refresh harvest can decrypt incoming memos into threaded messages.
    // The identity is derived from the wallet's OWN SDXLite-compatible seed phrase (full-node
    // z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
    chat::ChatService chat_service_;
    chat::ChatDatabase chat_db_;                  // persistent backing (seed-derived encryption at rest)
    bool chat_identity_provisioned_ = false;      // identity set on the service this session
    bool chat_identity_fetch_in_flight_ = false;  // a z_exportmnemonic worker job is pending
    // Bumped by resetChatSession() on every wallet change; an in-flight identity fetch captures the
    // value at post time and its completion callback discards its (previous-wallet) secret if the id
    // no longer matches — so wallet A's seed can't be provisioned under wallet B via a stale job.
    int chat_session_generation_ = 0;
    bool chat_identity_unavailable_ = false;      // provisioning failed definitively (e.g. non-mnemonic wallet)
    // Per-conversation "last seen" watermark (message timestamp) for unread tracking (Q1). Updated when a
    // thread is viewed (markChatConversationSeen); wiped in resetChatSession so unread doesn't leak across
    // wallets. In-memory only (resets on app restart).
    std::map<std::string, std::int64_t> chat_seen_watermark_;

    // ── Per-frame render caches (avoid O(N) recompute every frame; see the respective call sites) ──
    // Chat nav-badge unread count — recomputed only when the store revision changes or after a short
    // interval (mute/hide/seen changes don't bump the store). See App::chatUnreadCount().
    mutable std::uint64_t chat_unread_rev_ = ~0ull;   // ~0 forces the first compute
    mutable int    chat_unread_cached_ = 0;
    mutable double chat_unread_computed_at_ = 0.0;
    // Sidebar unconfirmed-tx badge — recomputed only when the tx list changes (keyed on last_tx_update +
    // size), not every frame. See App::render().
    std::int64_t sb_unconf_key_ts_ = -1;
    std::size_t  sb_unconf_key_n_  = 0;
    int          sb_unconf_count_  = 0;
    // Daemon-memory probe is expensive (/proc scan on Linux, popen on macOS); throttle it to ~1.5s so the
    // Mining tab's per-frame read doesn't hammer the OS. See App::getDaemonMemoryUsageMB().
    mutable double daemon_mem_cached_mb_ = 0.0;
    mutable double daemon_mem_probe_at_  = 0.0;

    // ── Chat note buffer (BOTH variants) ────────────────────────────────────────────────────────
    // Each chat message is a shielded tx that spends a note; its change needs a few confirmations before
    // it's spendable again (lite: backend ANCHOR_OFFSET+1 = 5; full node: z_sendmany minconf = 1), so
    // rapid sends run out of verified funds. We keep a buffer of ~kChatBufferTarget small self-notes so a
    // burst of messages each spends a separate verified note, refilled from change + background self-
    // splits. Chat sends, self-splits and user sends share the wallet's single send channel; inflight_op_
    // + (lite) lite_send_callback_ / (full node) send_submissions_in_flight_+pending_opids_ serialize them
    // so exactly one send is ever outstanding. Implemented in app_network.cpp; pumped from update().
    enum class LiteOpKind { None, ChatSend, ContactRequest, Split, UserSend };
    struct LiteInflightOp {
        LiteOpKind  kind = LiteOpKind::None;
        std::string echoLocalId;    // ChatSend/ContactRequest: the echo to resolve when it completes
        int         sessionGen = 0; // chat_session_generation_ snapshot at submit (stale-guard)
        double      submittedAt = 0.0;
    };
    struct QueuedChatOp {
        LiteOpKind kind = LiteOpKind::ChatSend;
        chat::OutgoingChatMemos memos;   // kept so a transient-funds retry re-broadcasts, never recomposes
        std::string echoLocalId;
        int sessionGen = 0;
        int retries = 0;
    };
    LiteInflightOp inflight_op_;                 // the single chat/split op currently on the send channel
    std::deque<QueuedChatOp> chat_send_queue_;   // chat/contact sends awaiting a free channel + verified note
    // Note-availability estimate between refreshes/scans: reset from a fresh count, decremented on each chat
    // submit (the count lags a spend by a cycle, but the wallet still picks a fresh note per send). Zeroed on
    // a transient-funds failure so we stop draining until the next refresh/scan restores the truth.
    int           chat_verified_note_budget_ = 0;
    int           chat_pipeline_note_count_  = 0;     // verified + maturing self-notes (drives shouldSplit)
    std::uint64_t chat_verified_shielded_zat_ = 0;    // verified shielded balance (split affordability)
    bool          chat_note_model_seen_ = false;      // saw a refresh/scan carrying per-note visibility
    // Single-split-in-flight guard: a self-split's OUTPUT notes are invisible until mined (~1 block), far
    // longer than any wall-clock cooldown — so we permit only ONE outstanding split and clear the flag when
    // the pipeline recovers (outputs mined) or a watchdog expires (a split that never mines mustn't wedge
    // refill forever). Prevents runaway splitting that would drain balance into fees.
    bool          chat_split_outstanding_ = false;
    double        chat_split_submitted_at_ = 0.0;     // ImGui time the outstanding split was submitted (watchdog)
    // Full-node only: a coordinator-owned z_listunspent worker scan feeds the per-note counts (the shared
    // balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
    bool          chat_note_scan_in_flight_ = false;
    double        chat_note_scan_last_ = 0.0;         // ImGui time of the last note scan (rate limit)
    double        chat_note_scan_ms_   = 0.0;         // measured cost of the last note scan (adaptive back-off)
    // Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
    // may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
    // this cache rather than requiring the current model to have both — else the budget flickers to 0.
    std::int64_t  chat_last_chain_height_ = 0;
    int           chat_fast_scan_last_seen_ = -1;  // dedup: last memo-note count logged by the 0-conf scan

    // Coordinator helpers (both variants unless noted; see app_network.cpp).
    void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model);  // lite: recompute caches on a fresh model
    void refreshChatNoteBudgetNode();             // full node: rate-limited z_listunspent worker scan
    // Feeds the SAME chat send-budget from an already-collected z_listunspent (the address refresh),
    // so the dedicated scan above is skipped while the address refresh is active (dedup — see #3).
    void updateChatNoteBudgetFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes);
    void pumpChatNoteBuffer();                     // per-frame: drain the queue / build the buffer
    int  verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model);   // lite
    int  pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model);   // lite
    bool shouldSplitChatBuffer();
    bool broadcastSelfSplitLite(int noteCount);   // lite: self-send minting noteCount reply-address notes
    bool broadcastSelfSplitNode(int noteCount);   // full node: z_sendmany self-send minting noteCount notes
    void enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId);
    void onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::string& error);
    static bool isTransientVerifiedFundsError(const std::string& error);
    int  chatConfsRequired() const;               // verified-note confs threshold: 5 (lite) / 1 (full node)
public:
    // Status-bar summary of the chat note buffer (empty when not applicable). Shown while the Chat tab is
    // active so the buffer's state (ready / building / sending) is visible.
    std::string chatBufferStatusText();
private:
public:
    // Total unread incoming chat messages across all conversations (for the sidebar badge). 0 when the
    // feature is off / no identity.
    int chatUnreadCount() const;
    // Mark a conversation read up to latestTs (called by the Chat tab while a thread is displayed).
    void markChatConversationSeen(const std::string& cid, std::int64_t latestTs);
    // Drop the seen-watermark for a conversation (used on revive-delete so a re-imported message — even one
    // whose stamped time predates the deleted thread's last message — still badges as unread).
    void forgetChatConversationSeen(const std::string& cid);
private:
    // Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
    // variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
    // feature is off, already provisioned, in flight, or unavailable.
    void maybeProvisionChatIdentity();
    // Drop the in-memory chat identity + decrypted message store, lock the chat DB, and re-arm
    // provisioning. MUST be called whenever the loaded wallet changes (switch / seed-migration adopt)
    // so wallet A's private chat can't surface — or be signed with A's keys — under wallet B.
    void resetChatSession();
    // One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
    // install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
    void maybeRemindSeedBackup();
    void maybeWarnEmptyWalletWithFundedSiblings();  // full-node: empty active wallet + a funded sibling → warn once
    void maybeWarnLargeWallet();                    // full-node: wallet.dat past bloat threshold → one-time toast + clickable alert
    void scanFundedSiblingsAsync();                 // off-UI-thread probe of sibling wallet files
    static void scrubAndRemoveExport(const std::string& path);  // zero + delete a plaintext key export (H-02)
    void sweepStaleDecryptExports();                // startup net: purge stale obsidiandecryptexport* files (H-02)

    // Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
    void beginCreateSeedWallet();  // starts the isolated create on a background thread
    void pumpSeedMigration();      // main thread: pick up background progress/result each frame
    // Phase 2: sweep all legacy funds into the new wallet, then adopt it as the primary wallet.
    void refreshSeedMigrationBalance();  // query the legacy total (shown on the Sweep step)
    // W3-3: the terminal callback for the sweep opid, shared by the initial submit and a resume
    // re-track. `resumed` selects the failure behaviour: a fresh sweep that fails -> Error; a resumed
    // opid the daemon no longer knows (stale) -> back to the dismissable Sweep gate (re-check balance).
    std::function<void(bool, const std::string&)> makeSweepCompletionCallback(bool resumed);
    void beginSweepToSeedWallet();       // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
    void pollSweepStatus();              // Confirming step: poll sweep confirmations + legacy balance
    void locateSweepTxid();              // recover a lost sweep txid (send to dest) -> Confirming gate
    void beginAdoptSeedWallet();         // stop daemon -> swap wallet.dat -> restart with -rescan
    void provisionChatIdentityFromSecret(std::string secret);
    std::string chatReplyZaddr();                                // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
    // A spendable z-address that can actually PAY the fee (balance >= fee), preferring the identity
    // reply address. The reply-to in the memo stays the identity address, so paying from a different
    // funded note is transport-transparent. Empty if no z-address can cover the fee.
    std::string chatPayFromZaddr(double fee) const;
    std::string generateChatLocalId(const char* prefix, int numBytes) const;  // unique echo id / cid
    // Broadcast the memos and, when the async op resolves, flip the echo (echoLocalId) to Sent/Failed.
    bool broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::string& echoLocalId);  // true if submitted
    bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos);  // lite two-recipient send
    void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model);  // lite chat receive harvest
    // Full-node 0-conf fast path: re-scan just the chat reply address at minconf=0 every refresh cycle
    // so incoming messages surface at mempool speed (before a block). Hidden conversations are skipped
    // (they still un-hide via the normal confirmed harvest). Self-gated; no-op without a chat identity.
    void fastScanChatMemos();
    bool chat_fast_scan_in_flight_ = false;   // guard against overlapping fast-scan RPCs
    float chat_fast_scan_accum_ = 0.0f;       // seconds since the last fast-scan (dedicated ~2.5s poll)
    double chat_fast_scan_last_ = 0.0;        // ImGui time of the last 0-conf fast scan (adaptive back-off)
    double chat_fast_scan_ms_   = 0.0;        // measured cost of the last fast scan (z_listreceivedbyaddress)
    bool font_rebuild_requested_ = false;     // set by requestFontRebuild(); consumed in preFrame()
    // Lite first-run welcome prompt: dismissed for the session once the user picks an action.
    bool lite_firstrun_dismissed_ = false;
    // Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
    bool lite_unlock_prompt_ = false;
    // One-shot: prompt to unlock on startup once we learn the auto-opened wallet is encrypted+locked.
    bool lite_startup_lock_checked_ = false;
    std::unique_ptr<daemon::DaemonController> daemon_controller_;
    std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
    // Serialized async mining-control queue: xmrig start/stop (SIGTERM->SIGKILL->join, up to ~3s) run on
    // this dedicated FIFO thread instead of the render thread, so the UI never blocks and stop/start
    // ordering is preserved across the ~13 call sites. (M-03/L-06/L-08/L-09/L-13)
    std::thread mining_ctl_thread_;
    std::mutex mining_ctl_mutex_;
    std::condition_variable mining_ctl_cv_;
    std::deque<std::function<void()>> mining_ctl_queue_;
    bool mining_ctl_stop_ = false;
    void postMiningControl(std::function<void()> job);  // enqueue a blocking xmrig op onto the FIFO thread
    void stopMiningControlThread();                     // signal + join the control thread (shutdown)
    // Auto-balance runtime state (pool mining, full-node only). The service fetches
    // pool hashrates off-thread; the RNG drives the weighted-random pick.
    util::PoolStatsService pool_stats_service_;
    std::mt19937 balance_rng_;
    long long last_balance_eval_ms_ = 0;      // steady-clock ms of the last refresh kick
    bool balance_refresh_pending_ = false;    // UI asked for an immediate refresh
    bool balance_snapshot_seen_ = false;      // the current in-flight snapshot was applied
    bool exchanges_fetch_started_ = false;    // once-per-session CoinGecko tickers fetch
    bool chart_fetch_in_flight_ = false;      // a market_chart history fetch is on the worker
    // Per-exchange candle chart (refreshExchangeChart): which pair the loaded series is for, an
    // in-flight guard, and a slow refresh timer (candles move slowly, like the aggregate chart).
    std::string exchange_chart_key_;          // "<identifier>:<BASE>/<QUOTE>" of the loaded series ("" = none)
    bool exchange_chart_fetch_in_flight_ = false;
    std::chrono::steady_clock::time_point exchange_chart_last_fetch_{};
    // Per-pair candle cache: switching back to a recently-viewed venue loads instantly (no re-fetch)
    // instead of overwriting the single active buffer. Keyed like exchange_chart_key_.
    struct ExchangeChartCache {
        std::vector<std::pair<std::time_t, double>> closeIntraday, closeDaily;
        std::vector<data::Candle> ohlcIntraday, ohlcDaily;
        std::chrono::steady_clock::time_point fetchedAt{};
    };
    std::unordered_map<std::string, ExchangeChartCache> exchange_chart_cache_;
    util::AsyncTaskManager async_tasks_;
    bool pending_antivirus_dialog_ = false;  // Show Windows Defender help dialog

    // Wallet state
    WalletState state_;

    // Shutdown state
    std::atomic<bool> shutting_down_{false};
    std::atomic<bool> shutdown_complete_{false};
    bool address_list_dirty_ = false;   // P8: dedup rebuildAddressList
    GuardedStatus shutdown_status_;   // thread-safe: written by the shutdown thread, read by the UI (M-05)
    std::thread shutdown_thread_;
    // Confirm-before-stopping-daemon-mid-witness-rebuild guard (see beginShutdown / renderDaemonStopConfirm)
    bool pending_shutdown_confirm_ = false;       // a quit is deferred, waiting to open the confirm modal
    bool daemon_stop_confirm_open_ = false;       // the confirm modal is currently showing
    bool shutdown_confirmed_ = false;             // user chose to proceed — bypass the guard on re-entry
    bool shutdown_keep_daemon_override_ = false;  // user chose "keep node running" for this shutdown only
    float shutdown_timer_ = 0.0f;
    bool force_quit_confirm_ = false;
    std::chrono::steady_clock::time_point shutdown_start_time_;

    // Daemon restart (e.g. after changing debug log categories)
    std::atomic<bool> daemon_restarting_{false};

    // Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new
    // wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to
    // wallet_switch_prev_file_ (settings writes stay on the main thread) so a broken wallet isn't
    // persisted across restarts. daemon_restarting_ stays set until the revert re-arms the reconnect.
    std::atomic<bool> wallet_switch_failed_{false};
    // Distinguishes the failure reason for the revert message: true when the switch failed because the
    // running node wouldn't release the RPC port in time (not because the target wallet is bad).
    std::atomic<bool> switch_stop_failed_{false};
    // True from a switch until the new wallet's daemon actually connects (onConnected). If the daemon
    // instead crash-wedges (a wallet that fails LATE in init, past the fast start grace), the connect
    // loop flags a revert so a broken wallet still can't stick.
    std::atomic<bool> wallet_switch_pending_confirm_{false};
    std::string wallet_switch_prev_file_;
    // Confirm-before-stop for a switch that would stop an ADOPTED (externally-running) node. switchToWallet
    // sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true).
    bool show_switch_stop_daemon_confirm_ = false;
    std::string pending_switch_wallet_file_;
    // Block-database recovery: set when the embedded node aborts because its block DB is unreadable
    // (a daemon-vs-chaindata format mismatch after an update, or a corrupt index). While set, the
    // connect loop STOPS crash-restarting into the same abort and offers a one-click reindex instead.
    bool block_db_reindex_available_ = false;   // node needs its block DB rebuilt (gates restart loop)
    bool show_block_db_reindex_confirm_ = false; // auto-shown offer dialog
    // Wallet auto-recovery: the daemon moved wallet.dat to wallet.<ts>.bak and loaded a salvaged copy
    // (BDB-verify failure — often a false positive from stale/cross-platform env state). We warn loudly
    // so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
    bool wallet_auto_recovered_ = false;         // a salvage happened this session
    bool wallet_auto_recovered_warned_ = false;  // guard: only surface it once per session
    bool wallet_degraded_ = false;               // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
    bool wallet_degraded_warned_ = false;        // guard: surface the degraded-mode notice once per session
    bool show_wallet_recovered_dialog_ = false;  // auto-shown warning dialog
    // Complementary on-disk safety net for a salvage we DIDN'T witness this launch (happened on a prior
    // run, or under an external daemon whose startup output we never captured): if the active wallet loads
    // empty while a sibling wallet file in the datadir still holds keys, warn once so the user's funds
    // (likely in a wallet.<ts>.bak) aren't mistaken for loss. See maybeWarnEmptyWalletWithFundedSiblings().
    struct FundedSibling { std::string fileName; int transparentKeys = 0; int shieldedKeys = 0; };
    bool show_empty_wallet_warning_ = false;             // auto-shown warning modal
    bool empty_wallet_warn_checked_ = false;             // evaluated this wallet-open already (reset in onConnected)
    bool empty_wallet_scan_in_flight_ = false;           // a sibling scan is running (main-thread only)
    bool empty_wallet_has_salvage_bak_ = false;          // modal variant: a funded salvage .bak → offer Restore
    std::vector<FundedSibling> empty_wallet_funded_siblings_;  // scan result (main-thread only)
    // The recovery dialog is the ONE authoritative surface: it stays open through the async rebuild/
    // restore, driven Offer → Working → Done/Failed (pumpWalletRestore sets the outcome). Presentation
    // only — the fund-safety file ops in rebuildWalletDatabase()/restoreOriginalWallet() are unchanged.
    enum class RecoveryPhase { Offer, Working, Done, Failed };
    RecoveryPhase recovery_phase_ = RecoveryPhase::Offer;
    int         recovery_outcome_sev_ = 0;               // 0 ok / 1 warn / 2 error, set at Done/Failed
    std::string recovery_outcome_msg_;                   // honest result string for the Done/Failed body
    bool        recovery_last_action_rebuild_ = false;   // which handler ran (for "try the other option")
    // After a successful repair the daemon restarts with a full rescan — minutes long, and it won't
    // answer RPC yet. This makes the loading overlay show a calm "finishing your wallet repair" screen
    // (instead of the generic "daemon stuck / RPC timeout / restart daemon" text) and suppresses the
    // daemon-crash toast. Set on repair success; cleared on connect (onConnected).
    bool        post_recovery_rescan_ = false;
    double      post_recovery_rescan_since_ = 0.0;       // stamped on first overlay frame (ImGui::GetTime)
    // "Restore original wallet" background op: worker sets these under the mutex, pumpWalletRestore()
    // (main thread) shows the result. 0 = success, 1 = warning, 2 = error.
    std::mutex wallet_restore_mutex_;
    bool wallet_restore_done_ = false;
    int  wallet_restore_severity_ = 0;
    std::string wallet_restore_msg_;
    // Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start →
    // reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated;
    // dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread.
    // error_ is written only on the main thread (processWalletSwitchRevert) and read by the (main-thread) UI.
    enum class WalletSwitchPhase : int { None = 0, Stopping, Starting, Reconnecting, Failed };
    std::atomic<int>  wallet_switch_phase_{0};
    std::atomic<bool> wallet_switch_dialog_open_{false};
    std::string       wallet_switch_error_;
    double            wallet_switch_started_time_ = 0.0;  // ImGui::GetTime() captured on first progress frame (elapsed)
    // Set when a failed switch's node output indicates the target wallet is CORRUPT — the Failed modal then
    // offers a one-click "-salvagewallet" repair, retrying the switch to wallet_switch_target_file_.
    std::atomic<bool> switch_wallet_corrupt_{false};
    std::string       wallet_switch_target_file_;  // the wallet we were switching TO (for a salvage retry)

    // Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
    // and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
    std::atomic<int> pending_delete_result_{-1};

    // Encryption state check timeout
    float encryption_check_timer_ = 0.0f;

    // UI State
    bool quit_requested_ = false;
    bool show_demo_window_ = false;
    bool show_settings_ = false;
    bool show_about_ = false;
    bool show_faq_ = false;
    bool show_import_key_ = false;
    bool show_export_key_ = false;
    bool show_backup_ = false;
    bool show_seed_backup_ = false;

    // Seed-phrase backup dialog state. seed_backup_phrase_ holds a SECRET (the revealed
    // mnemonic) and is wiped with sodium_memzero when the dialog closes.
    std::string seed_backup_phrase_;
    std::string seed_backup_status_;
    bool seed_backup_fetch_started_ = false;
    bool seed_backup_loading_ = false;
    bool seed_backup_no_mnemonic_ = false;
    bool seed_backup_reminder_in_flight_ = false;  // guards the one-time backup nudge probe
    bool large_wallet_checked_ = false;            // gate: stat wallet.dat for the bloat nudge once per launch

    // Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
    // once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
    // capable daemon can migrate; Incapable = the daemon lacks z_exportmnemonic (can't tell / can't
    // migrate). Reset to Unknown on wallet switch so it re-probes the new wallet.
    enum class WalletSeedStatus { Unknown, HasMnemonic, NoMnemonic, Incapable };
    WalletSeedStatus wallet_seed_status_ = WalletSeedStatus::Unknown;
    bool wallet_seed_status_in_flight_ = false;
    int  wallet_seed_status_attempts_ = 0;   // give up (Incapable) after a few transient probe failures
    void probeWalletSeedStatus();   // one-shot per connect; classifies the wallet's mnemonic status

    // --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) ---
    enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
    bool show_seed_migration_ = false;
    SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;

    // Intro pre-flight: probe the current wallet before offering to create a seed wallet, so we can
    // skip a pointless migration (AlreadyMnemonic → offer backup) or explain why it can't run
    // (DaemonTooOld). Set from beginSeedMigrationPrecheck()'s async callback (main thread).
    enum class SeedMigrationPrecheck { Pending, Legacy, AlreadyMnemonic, DaemonTooOld, CheckFailed };
    SeedMigrationPrecheck seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
    bool seed_migration_precheck_started_ = false;
    bool seed_migration_balance_loaded_ = false;  // Sweep step: distinguishes "0 funds" from "not loaded yet"
    bool seed_migration_nofunds_confirmed_ = false;  // "replace my wallet" gate for the no-funds adopt

    // "A newer node is bundled — update?" startup prompt (see maybeOfferDaemonUpdate).
    bool show_daemon_update_prompt_ = false;
    unsigned long long daemon_update_bundled_size_ = 0;  // bundled daemon size the prompt offers
    bool seed_migration_in_flight_ = false;      // main-thread guard while the bg create task runs
    std::string seed_migration_seed_;            // SECRET — the revealed phrase, wiped on close
    std::string seed_migration_dest_;            // new shielded z-address (Phase 2 sweep target)
    std::string seed_migration_temp_dir_;        // temp datadir root holding the new wallet (kept)
    bool seed_migration_backed_up_ = false;      // "I've written it down" confirmation
    std::string seed_migration_status_;          // main-thread display (progress / error text)
    double seed_migration_balance_ = 0.0;        // legacy total to sweep (shown on the Sweep step)
    std::string seed_migration_sweep_txid_;      // the z_mergetoaddress sweep transaction id
    int seed_migration_sweep_confs_ = 0;         // confirmations of the sweep tx (adopt gate: >= 1)
    double seed_migration_legacy_remaining_ = -1.0;  // legacy balance after sweep (-1 = unknown)
    float seed_migration_poll_timer_ = 0.0f;     // throttles the Confirming-step poll
    bool seed_migration_confirm_in_flight_ = false;  // guards the confirm poll
    // Cross-thread handoff from the background create task (guarded by seed_migration_mutex_).
    std::mutex seed_migration_mutex_;
    std::string seed_migration_progress_;        // latest progress line
    bool seed_migration_done_ = false;           // create result ready to consume
    bool seed_migration_ok_ = false;
    std::string seed_migration_r_seed_, seed_migration_r_dest_, seed_migration_r_tmp_, seed_migration_r_err_;
    // Cross-thread handoff from the background adopt task (guarded by seed_migration_mutex_).
    bool seed_migration_adopt_done_ = false;
    bool seed_migration_adopt_ok_ = false;
    std::string seed_migration_adopt_err_;

    // Embedded daemon state
    bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
    // Written by the DaemonController state callback (which fires on EmbeddedDaemon's monitor thread)
    // AND read on the UI thread — so all access goes through set/getDaemonStatus() under this mutex.
    // getDaemonStatus() returns a COPY: a returned reference could dangle if the monitor thread
    // reassigned the string mid-read (data race / use-after-free).
    mutable std::mutex daemon_status_mutex_;
    std::string daemon_status_;  // guarded by daemon_status_mutex_ — use set/getDaemonStatus()
    void setDaemonStatus(std::string s);
    mutable std::string daemon_mem_diag_;  // diagnostic info for daemon memory detection
    size_t daemon_output_offset_ = 0;      // for incremental output parsing (rescan detection)
    
    // Export/Import state
    char export_result_[256] = {0};   // SECRET exported key — fixed buffer so it can be sodium_memzero'd
    bool export_in_progress_ = false; // async key fetch running (spinner + disable Export)
    bool export_error_ = false;       // last export returned no key (locked wallet / failure)
    char import_key_input_[512] = {0};
    std::string export_address_;
    std::string import_status_;
    bool import_success_ = false;
    bool import_key_reveal_ = false;      // show the key in plaintext (default masked)
    bool import_in_progress_ = false;     // an import + rescan is running (disable/spinner)
    std::string import_result_address_;   // address imported on success (shown as a copy field)
    bool import_view_mode_ = false;       // dialog mode: false = spending key, true = viewing key
    char import_key_scan_height_[16] = {0};  // optional rescan start height (shielded spend / viewing-key imports)
    // --- Sweep: import a spending key then move all its funds to one of your own addresses, instead
    //     of keeping the key in the wallet (spending-key / non-view mode only). ---
    bool import_sweep_mode_ = false;      // sweep instead of a plain import
    int  sweep_dest_mode_ = 0;            // destination: 0 = fresh shielded address, 1 = an existing one
    char sweep_dest_pick_[128] = {0};     // chosen existing destination (sweep_dest_mode_ == 1)
    enum class SweepStep { Idle, Running, Done, Error };
    SweepStep sweep_step_ = SweepStep::Idle;
    std::string sweep_status_;            // progress / error text
    std::string sweep_txid_;              // sweep transaction id (on success)
    std::string sweep_dest_shown_;        // the destination address the funds were swept to
    std::string backup_status_;
    bool backup_success_ = false;

    // Connection
    std::string connection_status_ = "Disconnected";
    bool connection_in_progress_ = false;
    bool remote_rpc_plaintext_warning_shown_ = false;
    // Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
    // and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect.
    int daemon_wait_attempts_ = 0;
    bool daemon_start_error_shown_ = false;
    int daemon_last_seen_crashes_ = 0;  // surface each new embedded-daemon crash reason once
    bool refresh_policy_syncing_ = false;  // whether the sync-throttle refresh profile is active
    // Sync-settle hysteresis + adaptive balance-poll throttle. Balance polling (z_gettotalbalance) is
    // O(mapWallet) and holds the daemon's cs_main, which starves block connection on a large shielded
    // wallet — so we keep the low-impact profile briefly after catching up, and back the balance poll
    // off in proportion to its own measured cost. See effectivelySyncing() / balanceRefreshDue().
    bool was_core_syncing_ = false;        // previous Core-refresh sync state, to detect the caught-up edge
    std::time_t sync_settle_until_ = 0;    // hold the sync-throttle until this wall-clock time (0 = not settling)
    double last_balance_scan_ms_ = 0.0;    // measured cost of the last z_gettotalbalance scan
    bool force_balance_refresh_ = false;   // a wallet mutation forces the next balance poll through the throttle
    // Same adaptive back-off applied to the other two O(mapWallet) scans that hold the daemon's cs_main:
    // the address scan (z_listunspent) and the history scan (z_listreceivedbyaddress). Without this, a
    // large shielded wallet re-scans them every tab cadence (~seconds each), saturating cs_main and
    // starving block connection near the tip (where effectivelySyncing() reads false). See
    // addressRefreshDue() / txRefreshDue(); only the routine periodic poll is throttled — explicit
    // refreshes (tab switch, dirty set, in-flight send) call the refresh directly and bypass this.
    double last_address_scan_ms_ = 0.0;    // measured cost of the last address scan
    double last_tx_scan_ms_ = 0.0;         // measured cost of the last history scan
    // Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
    std::uint64_t clipboard_secret_hash_ = 0;
    double clipboard_clear_deadline_ = 0.0;
    float loading_timer_ = 0.0f;  // spinner animation for loading overlay
    double connect_stall_since_ = 0.0;  // ImGui::GetTime() when the daemon first went "reachable but not ready"; 0 = not stalling (see util/connect_stall.h)
    bool encryption_incomplete_warned_ = false;  // W2-2: once-per-session guard for the "encryption didn't complete" warning
    bool lock_failure_warned_ = false;           // W2-4: guard so a repeatedly-failing auto-lock warns once, not every retry
    std::uint64_t alerts_seen_total_ = 0;        // Notifications::totalPushed() at last alert-panel open; drives the bell's unread dot

    // Current page (sidebar navigation)
    ui::NavPage current_page_ = ui::NavPage::Overview;
    ui::NavPage prev_page_    = ui::NavPage::Overview;
    float page_alpha_         = 1.0f;  // 0→1 fade on page switch
    bool  sidebar_collapsed_  = false; // true = icon-only mode

    // Debug screenshot sweep state.
    bool  screenshot_sweep_active_    = false;
    bool  sweep_current_theme_only_   = false;   // Debug Options: sweep only the active theme
    bool  sweep_capture_this_frame_   = false;
    int   sweep_skin_idx_             = 0;
    int   sweep_settle_frames_        = 0;   // frames to let a new skin/surface settle before capture
    std::vector<std::string>  sweep_skins_;  // skin ids to cycle
    std::string sweep_dir_;                  // output folder for this sweep
    std::string sweep_current_path_;         // PNG path for the frame about to be captured
    std::string sweep_saved_skin_;           // restore on completion
    ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
    void updateScreenshotSweep();            // called at the top of render() while active
    void applySweepTarget();                 // apply current (skin,page/surface), path, arm settle

    // --- Full UI sweep: the capture unit is a "surface" (a tab, optionally with a modal / step /
    //     state forced on top). A tab is a surface with a null setup. ---
    struct SweepTarget {
        std::string               name;      // fs-safe id, e.g. "modal-seed-backup" / "overview"
        ui::NavPage               page = ui::NavPage::Overview;  // base tab under the surface
        std::function<void(App&)> setup;     // reveal the surface (null = plain tab)
        std::function<void(App&)> teardown;  // clear it (null = nothing to undo)
        int                       settle = 4;// blur overlays override to 8
    };
    std::vector<SweepTarget> sweep_targets_;
    int  sweep_target_idx_ = 0;
    bool sweep_full_   = false;   // full-UI sweep (drives surfaces) vs the legacy tab-only sweep
    // capture_mode_: set only during a full sweep. CONTRACT: while true, NO live op may fire — no
    // RPC, no auto-lock, no async pump. New async paths that could run mid-sweep must guard on it.
    bool capture_mode_ = false;
    void startSweepImpl(bool full);
    void buildSweepCatalog();
    void installDemoWalletData();
    void clearDemoWalletData();
    void applyHealthyDemoState();  // reset the connection/encryption flags to the healthy demo values
    void writeSweepManifest() const;
    // Snapshot of the state_ fields the demo installer mutates, restored at sweep end. Kept as a
    // plain struct because WalletState has reference-alias members and isn't copy-assignable.
    struct SweepStateSnapshot {
        bool valid = false;
        bool connected=false, warming_up=false, daemon_initializing=false;
        bool encrypted=false, locked=false, encryption_state_known=false;
        std::string warmup_status, warmup_description;
        SyncInfo sync;
        double privateBalance=0, transparentBalance=0, totalBalance=0, unconfirmedBalance=0;
        std::vector<AddressInfo> addresses, z_addresses, t_addresses;
        std::vector<TransactionInfo> transactions;
        double market_price_usd=0;
        double market_change_24h=0;
    } sweep_state_snapshot_;
    bool  sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
    float sidebar_width_anim_ = 0.0f;  // animated width (0 = uninitialized)
    float prev_dpi_scale_     = 0.0f;  // detect DPI changes to snap sidebar width

    // Background gradient overlay
    ImTextureID gradient_tex_ = 0;

    // Logo texture (reload on skin change / dark↔light switch)
    ImTextureID logo_tex_ = 0;
    int logo_w_ = 0;
    int logo_h_ = 0;
    bool logo_loaded_ = false;
    bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
    ImU32 logo_accent_ = 0;            // theme accent the SVG logo was last rasterized with (re-render on change)
    ImTextureID drgx_emoji_tex_ = 0;   // ":drgx:" custom chat emoji (themed to the accent, like the logo)
    int drgx_emoji_w_ = 0, drgx_emoji_h_ = 0;

    // Coin logo texture (DragonX currency icon, separate from wallet branding)
    ImTextureID coin_logo_tex_ = 0;
    int coin_logo_w_ = 0;
    int coin_logo_h_ = 0;
    bool coin_logo_loaded_ = false;

    // Console tab + its backend executor (full-node RPC or lite backend), created lazily.
    ui::ConsoleTab console_tab_;
    std::unique_ptr<ui::ConsoleCommandExecutor> console_exec_;
    
    // Pending payment from URI
    bool pending_payment_valid_ = false;
    std::string pending_to_address_;
    double pending_amount_ = 0.0;
    std::string pending_memo_;
    std::string pending_label_;

    // Per-category refresh timers, policy, and worker queue guards.
    services::NetworkRefreshService network_refresh_;
    int mining_slow_counter_ = 0;  // counts fast ticks; fires slow refresh every N

    // Mining toggle guard (prevents concurrent setgenerate calls)
    std::atomic<bool> mining_toggle_in_progress_{false};

    // True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
    // poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
    std::atomic<bool> pool_starting_{false};

    // Auto-shield guard (prevents concurrent auto-shield operations)
    std::atomic<bool> auto_shield_pending_{false};
    // v1.3.0+ daemons auto-shield coinbase themselves; probe z_autoshieldstatus once per connection and
    // defer the wallet's own client-side auto-shield when the daemon is doing it (otherwise both race for
    // the same coinbase UTXOs and split funds across different z-addresses). Fail-closed: a pre-1.3.0
    // daemon lacks the RPC → active stays false → the wallet keeps shielding client-side (no regression).
    bool daemon_autoshield_probed_ = false;
    bool daemon_autoshield_active_ = false;
    std::atomic<bool> daemon_autoshield_probe_inflight_{false};
    std::string daemon_autoshield_address_;          // z_autoshieldstatus fields (O1); empty on old daemons
    std::string daemon_autoshield_disabled_reason_;  // daemon's reason auto-shield is off (e.g. seed not recoverable)
    bool daemon_autoshield_seed_recoverable_ = false;

    // P4: Incremental transaction cache
    int last_tx_block_height_ = -1;  // block height at last full tx fetch
    static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
    std::size_t shielded_history_scan_cursor_ = 0;
    bool shielded_history_scan_pending_ = false;
    // False until the first full shielded-history scan finishes. Drives the History tab's
    // "Loading older history…" progress so the user knows transactions are still streaming in
    // after the first batch appears; goes quiet for the routine per-block re-scans afterward.
    bool initial_history_scan_complete_ = false;
    std::unordered_map<std::string, int> shielded_history_scan_heights_;

    // P4b: z_viewtransaction result cache — avoids re-calling the RPC for
    // txids we've already enriched.  Keyed by txid.
    using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
    services::NetworkRefreshService::TransactionViewCache viewtx_cache_;

    // P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
    // are accumulated here and reused across refresh cycles.  Only
    // recent/unconfirmed txns are re-fetched from the daemon each time.
    std::vector<TransactionInfo> confirmed_tx_cache_;
    std::unordered_set<std::string> confirmed_tx_ids_;  // fast lookup
    int confirmed_cache_block_ = -1;  // block height when cache was last built

    // Dirty flags for demand-driven refresh
    bool addresses_dirty_ = true;           // true → refreshAddresses() will run
    bool address_validation_cache_dirty_ = true;
    bool transactions_dirty_ = false;       // true → force tx refresh regardless of block height
    bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
    bool rescan_status_poll_in_progress_ = false;
    // True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
    // Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
    // pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
    bool rescan_confirmed_active_ = false;
    // A runtime rescanblockchain RPC is in flight (vs the -rescan daemon restart). While set,
    // the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
    // the whole scan and would block them); completion is signalled by the rescan RPC callback.
    bool runtime_rescan_active_ = false;
    // True only for a rescan the WALLET/USER initiated (the Rescan button, a -rescan/salvage/zap/reindex
    // restart, key import, seed migration) — not an autonomous background witness rebuild the daemon does
    // on its own. Gates the "Blockchain rescan complete" toast so background rebuilds don't fire it;
    // cleared when the toast is shown.
    std::atomic<bool> user_initiated_rescan_{false};   // atomic: some rescan triggers run on worker threads
    // Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
    // that reconciles the preserved wallet.dat against the freshly-imported chain.
    bool post_bootstrap_rescan_pending_ = false;
    // Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
    // "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
    // block, each walking from its own start height to the tip), so we derive a stable, monotonic
    // overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
    int witness_rebuild_total_blocks_ = 0;
    // The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
    // of <N>", logged once per wallet tx as its initial witness is set. The <i> is the tx's slot in
    // an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest
    // monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also
    // dedups the daemon's occasional double-prints) over the reported total N.
    std::unordered_set<std::string> witness_seen_txids_;
    int witness_total_txs_ = 0;
    bool opid_poll_in_progress_ = false;
    // Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
    // connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
    int consecutive_core_failures_ = 0;

    // Pending z_sendmany operation tracking
    bool send_progress_active_ = false;
    int send_submissions_in_flight_ = 0;
    std::vector<std::string> pending_opids_;  // opids to poll for completion
    struct PendingSendInfo {
        std::string from;
        std::string to;
        std::string memo;
        double amount = 0.0;
        double fee = 0.0;
        std::int64_t timestamp = 0;
    };
    std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
    // Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
    // fails again is reported to the user instead of looping.
    std::unordered_set<std::string> send_feegap_retried_opids_;
    // z_sendmany UI callbacks held until the opid reaches a terminal status, so the
    // user isn't told "sent successfully" before the tx is actually built/broadcast.
    std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
        pending_send_callbacks_;
    // Txids from completed z_sendmany operations.
    // Ensures shielded sends are discoverable by z_viewtransaction
    // even when they don't appear in listtransactions or
    // z_listreceivedbyaddress.
    std::unordered_set<std::string> send_txids_;

    // First-run wizard state
    WizardPhase wizard_phase_ = WizardPhase::None;
    std::unique_ptr<util::Bootstrap> bootstrap_;
    bool bootstrap_downloading_ = false;  // true while settings bootstrap dialog is active
    std::string wizard_pending_passphrase_;  // held until daemon connects
    std::string wizard_saved_passphrase_;    // held until PinSetup completes/skipped

    // Wallet security flow state shared by wizard/settings encryption paths.
    services::WalletSecurityController wallet_security_;
    services::WalletSecurityWorkflow wallet_security_workflow_;

    // Wizard: stopping an external daemon before bootstrap
    std::atomic<bool> wizard_stopping_external_{false};   // written by the stop worker, read by the UI (L-06)
    GuardedStatus wizard_stop_status_;   // thread-safe: written by the stop worker, read by the UI (L-06)

    // PIN vault
    std::unique_ptr<util::SecureVault> vault_;
    data::TransactionHistoryCache transaction_history_cache_;
    data::AddressBook address_book_;   // shared contact store; loaded once in init(), self-saves on mutation
    data::WalletIndex wallet_index_;   // per-wallet metadata cache (wallets.json); populated after each load
    std::string pending_transaction_history_cache_passphrase_;
    bool transaction_history_cache_loaded_ = false;

    // Lock screen state
    bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
    bool lock_use_pin_ = true;  // true = PIN input, false = passphrase input
    char lock_pin_buf_[16] = {};
    char lock_passphrase_buf_[256] = {};
    std::string lock_error_msg_;
    float lock_error_timer_ = 0.0f;
    int lock_attempts_ = 0;
    float lock_lockout_timer_ = 0.0f;
    bool lock_unlock_in_progress_ = false;

    // Encrypt wallet dialog state
    bool show_encrypt_dialog_ = false;
    bool show_change_passphrase_ = false;
    EncryptDialogPhase encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
    char encrypt_pass_buf_[256] = {};
    char encrypt_confirm_buf_[256] = {};
    char change_old_pass_buf_[256] = {};
    char change_new_pass_buf_[256] = {};
    char change_confirm_buf_[256] = {};
    std::string encrypt_status_;
    bool encrypt_in_progress_ = false;
    std::string enc_dlg_saved_passphrase_;   // held for PIN setup after encrypt
    char enc_dlg_pin_buf_[16] = {};
    char enc_dlg_pin_confirm_buf_[16] = {};
    std::string enc_dlg_pin_status_;

    // PIN setup dialog state (settings page)
    bool show_pin_setup_ = false;
    bool show_pin_change_ = false;
    bool show_pin_remove_ = false;
    char pin_buf_[16] = {};
    char pin_confirm_buf_[16] = {};
    char pin_old_buf_[16] = {};
    char pin_passphrase_buf_[256] = {};
    std::string pin_status_;
    bool pin_in_progress_ = false;

    // Decrypt wallet dialog state
    bool show_decrypt_dialog_ = false;
    char decrypt_pass_buf_[256] = {};

    // Wizard PIN setup state
    char wizard_pin_buf_[16] = {};
    char wizard_pin_confirm_buf_[16] = {};
    std::string wizard_pin_status_;

    // Auto-lock on idle
    std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();

    // Mine-when-idle: auto-start/stop mining based on system idle state
    bool idle_mining_active_ = false;  // true when mining was auto-started by idle detection
    bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle

    // Private methods - rendering
    void renderStatusBar();
    // Persistent node/RPC error strip at the top of the content column when the wallet can't
    // reach its node (or the embedded daemon gave up crashing). Decision logic is the pure
    // evaluateNodeStatusBanner() in ui/node_status_banner.h; this draws it and wires the action.
    void renderNodeStatusBanner();
    // Body of the status-bar alert-history popup: recent alerts (incl. ones whose toast faded),
    // newest first, with severity icon + relative age + a Clear action. See src/ui/notifications.h.
    void renderAlertHistoryPanel();
    void renderLiteFirstRunPrompt();  // lite-only welcome modal when no wallet exists yet
    void renderLiteUnlockPrompt();    // lite-only send-time unlock modal
    void renderImportKeyDialog();
    void renderExportKeyDialog();
    void renderBackupDialog();
    void renderSeedBackupDialog();  // full-node "Back up seed phrase" modal (z_exportmnemonic)
    void renderSeedMigrationDialog();  // "Migrate to a seed wallet" guided modal (Phase 1: create)
    void beginSeedMigrationPrecheck(); // Intro: probe whether the wallet is legacy / already-seeded
    void maybeOfferDaemonUpdate();     // at startup, flag the prompt if a newer daemon is bundled
    void renderDaemonUpdatePrompt();   // "a newer node is bundled — update the installed daemon?"
    void renderFirstRunWizard();
    void renderLockScreen();
    void renderEncryptWalletDialog();
    void renderDecryptWalletDialog();
    void renderPinDialogs();
    void renderAntivirusHelpDialog();
    void renderSwitchStopDaemonDialog();  // confirm before stopping an adopted node to switch wallets
    void renderBlockDbReindexDialog();    // offer to rebuild an unreadable block database (-reindex)
    void reindexBlockDatabase();          // restart the daemon with -reindex to rebuild the block DB
    void renderWalletRecoveredDialog();   // warn that the node auto-recovered/salvaged wallet.dat
    void renderEmptyWalletWarningDialog();// warn that the active wallet is empty while a sibling holds funds
    void detectWalletAutoRecovery();      // scan daemon output for a salvage; fire the warning once/session
    void detectWalletDegraded();          // scan daemon output for a DEGRADED-mode open; warn once/session
    void restoreOriginalWallet();         // swap the wallet.<ts>.bak back over the salvaged copy + restart
    void pumpWalletRestore();             // main-thread: surface the restore/rebuild op's result
    void rebuildWalletDatabase();         // rebuild a BDB-inconsistent wallet into a loadable one (helper)
    bool walletRebuildAvailable() const;  // the dragonx-wallet-rebuild helper is present
    void processDeferredEncryption();

    // Private methods - connection
    void tryConnect();
    void onConnected();
    void onDisconnected(const std::string& reason);
    // Set the "node is initializing" UI state (status line + overlay description) from the
    // embedded/external daemon's launch state and its own console output (current phase + block
    // height), so a connect probe that times out while the daemon loads shows WHAT it's doing.
    // `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
    // false when the daemon is merely launching (not bound yet). Returns the status title.
    std::string applyDaemonInitStatus(bool reachableButBusy);
    // Tear down a connection that died mid-session (daemon crash / restart / dropped
    // socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
    // alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
    void handleLostConnection(const std::string& reason);
    void applyDefaultBanlist();

    // Private methods - data refresh
    void refreshData();           // Orchestrator: dispatches per-category refreshes
    void refreshCoreData();       // Balance + blockchain info (can use fast_worker_)
    void refreshAddressData();    // Address lists + balances
    void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
    void refreshRecentTransactionData(); // Lightweight recent/unconfirmed tx poll
    bool refreshEncryptionState(); // Wallet encryption/lock state
    void refreshBalance();        // Legacy: balance-only refresh (used by specific callers)
    void refreshAddresses();      // Legacy: standalone address refresh
    void refreshPrice();
    void refreshWalletEncryptionState();
    void applyRefreshPolicy(ui::NavPage page);
    bool effectivelySyncing() const;   // syncing, or within the post-sync settle window (hysteresis)
    bool balanceRefreshDue() const;    // adaptive: enough time elapsed given the last balance-scan cost?
    bool addressRefreshDue() const;    // same adaptive back-off for the address scan (z_listunspent)
    bool txRefreshDue() const;         // same adaptive back-off for the history scan (z_listreceivedbyaddress)
    // Shared duty-cycle rule: a scan may resume only once (lastScanMs / kScanDutyCycle) has elapsed since
    // lastUpdate, so any single O(mapWallet) scan can occupy at most ~kScanDutyCycle of wall-clock.
    bool scanRefreshDue(std::int64_t lastUpdate, double lastScanMs) const;
    bool currentPageNeedsWalletDataRefresh() const;
    bool shouldRunWalletTransactionRefresh() const;
    bool shouldRefreshTransactions() const;
    bool shouldRefreshRecentTransactions() const;
    void checkAutoLock();
    void checkIdleMining();
};

} // namespace dragonx
