// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Screenshot sweep: capture the UI under every skin. Two modes share one state machine:
//   - startScreenshotSweep()  — the legacy tab-only sweep (<config>/screenshots).
//   - startFullUiSweep()      — ALSO drives every modal / dialog / multi-step flow / state overlay
//     into view (offline, with injected demo data, firing no live ops) and captures each under
//     every skin (<config>/screenshots-full/<surface>/<skin>.png + index.md).
// The capture unit is a "surface" (SweepTarget): a base tab, optionally with a setup() that forces
// a modal/step/state on top and a teardown() that clears it. main.cpp reads the framebuffer on the
// settled frame and calls onScreenshotCaptured() to advance.

#include "app.h"

#include "config/settings.h"
#include "data/address_book.h"
#include "ui/schema/skin_manager.h"
#include "ui/notifications.h"
#include "ui/sidebar.h"
#include "ui/windows/send_tab.h"
#include "ui/windows/wallets_dialog.h"
#include "ui/windows/export_transactions_dialog.h"
#include "ui/windows/export_all_keys_dialog.h"
#include "ui/windows/bootstrap_download_dialog.h"
#include "ui/windows/daemon_download_dialog.h"
#include "ui/windows/xmrig_download_dialog.h"
#include "ui/windows/qr_popup_dialog.h"
#include "ui/windows/request_payment_dialog.h"
#include "ui/windows/validate_address_dialog.h"
#include "ui/windows/address_label_dialog.h"
#include "ui/windows/shield_dialog.h"
#include "ui/windows/address_transfer_dialog.h"
#include "ui/windows/key_export_dialog.h"
#include "ui/windows/contacts_tab.h"
#include "ui/pages/settings_page.h"
#include "util/platform.h"
#include "wallet/wallet_capabilities.h"

#include "imgui.h"
#include "imgui_internal.h"   // ClosePopupToLevel / OpenPopupStack — flush stuck popups after teardown

#include <sodium.h>

#include <cmath>
#include <filesystem>
#include <fstream>

namespace dragonx {

namespace {
namespace fs = std::filesystem;

// The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are
// forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct.
std::vector<config::Settings::PortfolioEntry> g_pfSnapshot;
int  g_pfStyleSnapshot = 0;
bool g_pfSnapshotValid = false;
std::vector<double> g_marketHistorySnapshot;   // real price history, restored at sweep end

// Filesystem-safe one-word tab name.
const char* sweepPageName(ui::NavPage page)
{
    switch (page) {
        case ui::NavPage::Overview: return "overview";
        case ui::NavPage::Send:     return "send";
        case ui::NavPage::Receive:  return "receive";
        case ui::NavPage::History:  return "history";
        case ui::NavPage::Contacts: return "contacts";
        case ui::NavPage::Chat:     return "chat";
        case ui::NavPage::Mining:   return "mining";
        case ui::NavPage::Market:   return "market";
        case ui::NavPage::Console:  return "console";
        case ui::NavPage::LiteConsole: return "console";
        case ui::NavPage::Peers:    return "network";
        case ui::NavPage::LiteNetwork: return "network";
        case ui::NavPage::Explorer: return "explorer";
        case ui::NavPage::Settings: return "settings";
        default: return "page";
    }
}

bool sweepPageEnabled(ui::NavPage page)
{
    return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), ui::NavPageSurface(page));
}

// Deterministic demo secrets/addresses (NON-secret — safe to hold in memory + display).
const char* kDemoMnemonic =
    "select milk exit banana type alcohol comic moral drama federal just green "
    "elevator render stumble lesson convince organ category caution panther misery pelican immune";
const char* kDemoZAddr =
    "zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000";
const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f";

// Contacts-view sweep: seed a few demo contacts (Z + T types, some global) so the Cards/List/Table
// modes render with data, and restore the real book afterward. Uses sweepSetEntries (no disk write),
// so the user's persisted address book is never touched even if the sweep is interrupted.
std::vector<data::AddressBookEntry> s_contactsSweepBackup;
void seedSweepContacts(App& a)
{
    s_contactsSweepBackup = a.addressBook().entries();
    data::AddressBookEntry e1("drgx pool payout address", kDemoZAddr, "mining pool payouts");   // Z, global
    e1.avatar = "icon:account_balance";   // icon avatar (verifies the icon-badge render path)
    data::AddressBookEntry e2("exchange deposit", "t1DemoTransparentAddressForUiSweep00000", ""); // T
    // Scope to the active wallet so it's visible (the tab hides contacts not in the active wallet);
    // non-empty hash also keeps it non-global -> no globe badge, so the list shows a global/non-global mix.
    e2.scope = a.activeWalletIdentityHash();
    data::AddressBookEntry e3("cold savings",
        "zs1sweepdemocoldsavingsaddressforuicapture0000000000000000000000000000", "long-term storage"); // Z, global
    a.addressBook().sweepSetEntries({ e1, e2, e3 });
}
void restoreSweepContacts(App& a)
{
    a.addressBook().sweepSetEntries(s_contactsSweepBackup);
    s_contactsSweepBackup.clear();
    if (a.settings()) a.settings()->setContactsViewMode(0);
}
}  // namespace

std::string App::screenshotDir() const
{
    return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
}

std::string App::screenshotFullDir() const
{
    return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots-full").string();
}

// ── Demo data (full sweep only): make every data-dependent screen render offline ─────────────
void App::installDemoWalletData()
{
    // Snapshot the state_ fields we mutate (WalletState isn't copy-assignable — reference aliases).
    auto& s = sweep_state_snapshot_;
    s.connected = state_.connected; s.warming_up = state_.warming_up;
    s.daemon_initializing = state_.daemon_initializing;
    s.encrypted = state_.encrypted; s.locked = state_.locked;
    s.encryption_state_known = state_.encryption_state_known;
    s.warmup_status = state_.warmup_status; s.warmup_description = state_.warmup_description;
    s.sync = state_.sync;
    s.privateBalance = state_.privateBalance; s.transparentBalance = state_.transparentBalance;
    s.totalBalance = state_.totalBalance; s.unconfirmedBalance = state_.unconfirmedBalance;
    s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses;
    s.transactions = state_.transactions;
    s.market_price_usd = state_.market.price_usd;
    s.market_change_24h = state_.market.change_24h;
    s.valid = true;

    applyHealthyDemoState();
    state_.sync.blocks = state_.sync.headers = 3124322;
    state_.sync.verification_progress = 1.0; state_.sync.syncing = false;
    // Legacy (pre-seed) wallet so the Settings "Migrate to seed" button glows in the sweep.
    wallet_seed_status_ = WalletSeedStatus::NoMnemonic;

    state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
    state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
    state_.market.price_usd = 0.01336200;
    state_.market.change_24h = 5.24000000;
    // Wavy, gently-rising price history so the row sparklines render (restored at sweep end).
    g_marketHistorySnapshot = state_.market.price_history;
    {
        std::vector<double> hist;
        for (int i = 0; i < 48; i++) {
            double t = static_cast<double>(i);
            hist.push_back(0.01250 + 0.0000180 * t
                           + 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7));
        }
        state_.market.price_history = hist;
    }

    auto zaddr = [](const char* a, double bal, const char* label) {
        AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "shielded"; i.label = label; return i;
    };
    auto taddr = [](const char* a, double bal, const char* label) {
        AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "transparent"; i.label = label; return i;
    };
    state_.z_addresses = {
        zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
        zaddr("zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", 0.5, ""),
    };
    state_.t_addresses = {
        taddr("t1DemoTransparentAddressForUiSweep00000", 3.25, "Mining payouts"),
        taddr("t1DemoSecondTransparentAddressForUiSw00", 0.0, ""),
    };
    state_.rebuildAddressList();

    auto tx = [](const char* id, const char* type, double amt, int64_t ts, int conf,
                 const char* addr, const char* memo) {
        TransactionInfo t; t.txid = id; t.type = type; t.amount = amt; t.timestamp = ts;
        t.confirmations = conf; t.address = addr; t.memo = memo; return t;
    };
    state_.transactions = {
        tx(kDemoTxid, "receive", 15.75000000, 1751286000, 42,
           "zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", "welcome to DragonX"),
        tx("a1b2c3d4e5f600000000000000000000000000000000000000000000000000000000", "send", 2.50000000,
           1751200000, 120, "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000", ""),
        tx("c0ffee00000000000000000000000000000000000000000000000000000000000000", "mined", 0.30000000,
           1751100000, 300, "t1DemoTransparentAddressForUiSweep00000", ""),
        tx("deadbeef000000000000000000000000000000000000000000000000000000000000", "receive", 0.50000000,
           1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"),
    };

    // Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot
    // the user's real portfolio first; setPortfolio* only mutate memory (save() is never called
    // here), and clearDemoWalletData() restores them at sweep end.
    if (settings_) {
        g_pfSnapshot = settings_->getPortfolioEntries();
        g_pfStyleSnapshot = settings_->getPortfolioStyle();
        g_pfSnapshotValid = true;
        auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr,
                      bool drgx, bool value, bool ch, bool spark) {
            config::Settings::PortfolioEntry e;
            e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30;
            e.addresses = { addr }; e.priceBasis = 0;
            e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark;
            return e;
        };
        settings_->setPortfolioEntries({
            grp("Savings", "savings", 0xFFFF9D4Fu,
                "zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true),
            grp("Mining rewards", "pickaxe", 0xFF3DB0FFu,
                "t1DemoTransparentAddressForUiSweep00000", true, true, true, true),
            grp("Cold storage", "diamond", 0xFFFF7BB0u,
                "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false),
        });
        settings_->setPortfolioStyle(0);
    }

    seedChatDemoData();
}

void App::applyHealthyDemoState()
{
    state_.connected = true;
    state_.warming_up = false;
    state_.daemon_initializing = false;
    state_.encryption_state_known = true;
    state_.encrypted = false;
    state_.locked = false;
    state_.warmup_status.clear();
    state_.warmup_description.clear();
    // Dismiss any first-run wizard so the base tabs/modals aren't occluded (the wizard targets set
    // wizard_phase_ themselves and restore None on teardown).
    wizard_phase_ = WizardPhase::None;
}

void App::clearDemoWalletData()
{
    auto& s = sweep_state_snapshot_;
    if (!s.valid) return;
    wallet_seed_status_ = WalletSeedStatus::Unknown;   // re-probe on the next real connect
    state_.connected = s.connected; state_.warming_up = s.warming_up;
    state_.daemon_initializing = s.daemon_initializing;
    state_.encrypted = s.encrypted; state_.locked = s.locked;
    state_.encryption_state_known = s.encryption_state_known;
    state_.warmup_status = s.warmup_status; state_.warmup_description = s.warmup_description;
    state_.sync = s.sync;
    state_.privateBalance = s.privateBalance; state_.transparentBalance = s.transparentBalance;
    state_.totalBalance = s.totalBalance; state_.unconfirmedBalance = s.unconfirmedBalance;
    state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses;
    state_.transactions = s.transactions;
    state_.market.price_usd = s.market_price_usd;
    state_.market.change_24h = s.market_change_24h;
    state_.market.price_history = g_marketHistorySnapshot;
    g_marketHistorySnapshot.clear();
    s = SweepStateSnapshot{};  // invalidate

    // Restore the user's real portfolio (demo groups were in-memory only).
    if (g_pfSnapshotValid && settings_) {
        settings_->setPortfolioEntries(g_pfSnapshot);
        settings_->setPortfolioStyle(g_pfStyleSnapshot);
    }
    g_pfSnapshot.clear();
    g_pfSnapshotValid = false;
}

// ── Catalog ──────────────────────────────────────────────────────────────────────────────────
// Lambdas defined in this member function may touch App's private members through the App& arg.
void App::buildSweepCatalog()
{
    sweep_targets_.clear();

    // Tabs (both sweeps).
    for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
        ui::NavPage pg = static_cast<ui::NavPage>(p);
        if (sweepPageEnabled(pg)) sweep_targets_.push_back({ sweepPageName(pg), pg, nullptr, nullptr, 4 });
    }
    if (!sweep_full_) return;

    // Full sweep: modals / flows / states. Blur overlays need more settle frames.
    const int kOverlaySettle = 8;
    auto add = [&](const char* name, ui::NavPage pg, std::function<void(App&)> setup,
                   std::function<void(App&)> teardown) {
        sweep_targets_.push_back({ name, pg, std::move(setup), std::move(teardown), kOverlaySettle });
    };

    // Simple bool-flag modals.
    add("modal-import-key", ui::NavPage::Overview,
        [](App& a) { a.import_view_mode_ = false; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
    add("modal-import-viewkey", ui::NavPage::Overview,
        [](App& a) { a.import_view_mode_ = true; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; a.import_view_mode_ = false; });
    add("modal-export-key", ui::NavPage::Overview,
        [](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
    add("modal-export-transactions", ui::NavPage::Settings,
        [](App&) { ui::ExportTransactionsDialog::show(); }, [](App&) { ui::ExportTransactionsDialog::hide(); });
    add("modal-export-all-keys", ui::NavPage::Settings,
        [](App&) { ui::ExportAllKeysDialog::show(); }, [](App&) { ui::ExportAllKeysDialog::hide(); });
    add("modal-bootstrap", ui::NavPage::Settings,
        [](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
    add("modal-backup", ui::NavPage::Overview,
        [](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
    add("modal-faq", ui::NavPage::Overview,
        [](App& a) { a.show_faq_ = true; }, [](App& a) { a.show_faq_ = false; });
    // Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
    add("modal-encrypt", ui::NavPage::Settings,
        [](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
        [](App& a) {
            a.show_encrypt_dialog_ = false; a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
            a.encrypt_status_.clear();
            memset(a.encrypt_pass_buf_, 0, sizeof(a.encrypt_pass_buf_));
            memset(a.encrypt_confirm_buf_, 0, sizeof(a.encrypt_confirm_buf_));
        });
    add("modal-change-passphrase", ui::NavPage::Settings,
        [](App& a) { a.show_change_passphrase_ = true; },
        [](App& a) {
            a.show_change_passphrase_ = false; a.encrypt_status_.clear();
            memset(a.change_old_pass_buf_, 0, sizeof(a.change_old_pass_buf_));
            memset(a.change_new_pass_buf_, 0, sizeof(a.change_new_pass_buf_));
            memset(a.change_confirm_buf_, 0, sizeof(a.change_confirm_buf_));
        });
    // Remove-encryption dialog — the redesigned passphrase-entry phase (reset() keeps the
    // workflow in PassphraseEntry; nothing fires the async unlock/export/restart pyramid).
    add("modal-decrypt", ui::NavPage::Settings,
        [](App& a) { a.wallet_security_workflow_.reset(); a.show_decrypt_dialog_ = true; },
        [](App& a) {
            a.show_decrypt_dialog_ = false; a.wallet_security_workflow_.reset();
            memset(a.decrypt_pass_buf_, 0, sizeof(a.decrypt_pass_buf_));
        });
    // PIN setup / change / remove dialogs (never fire the async vault store/verify).
    add("modal-pin-setup", ui::NavPage::Settings,
        [](App& a) { a.show_pin_setup_ = true; },
        [](App& a) {
            a.show_pin_setup_ = false; a.pin_status_.clear();
            memset(a.pin_passphrase_buf_, 0, sizeof(a.pin_passphrase_buf_));
            memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
            memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
        });
    add("modal-pin-change", ui::NavPage::Settings,
        [](App& a) { a.show_pin_change_ = true; },
        [](App& a) {
            a.show_pin_change_ = false; a.pin_status_.clear();
            memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
            memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
            memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
        });
    add("modal-pin-remove", ui::NavPage::Settings,
        [](App& a) { a.show_pin_remove_ = true; },
        [](App& a) {
            a.show_pin_remove_ = false; a.pin_status_.clear();
            memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
        });
    // Wave-1 standalone dialogs (rendered globally from App::render, so any page works).
    add("modal-qr-popup", ui::NavPage::Receive,
        [](App&) { ui::QRPopupDialog::show(kDemoZAddr, "Savings"); },
        [](App&) { ui::QRPopupDialog::close(); });
    add("modal-request-payment", ui::NavPage::Receive,
        [](App&) { ui::RequestPaymentDialog::show(kDemoZAddr); },
        [](App&) { ui::RequestPaymentDialog::hide(); });
    add("modal-validate-address", ui::NavPage::Receive,
        [](App&) { ui::ValidateAddressDialog::show(); },
        [](App&) { ui::ValidateAddressDialog::hide(); });
    add("modal-address-label", ui::NavPage::Overview,
        [](App& a) { ui::AddressLabelDialog::show(&a, kDemoZAddr, true); },
        [](App&) { ui::AddressLabelDialog::hide(); });
    // (No modal-daemon-prompt surface: renderDaemonUpdatePrompt is gated behind !capture_mode_,
    //  so it can't render during a sweep. Its migration is verified by build + the shared overlay pattern.)
    add("modal-antivirus", ui::NavPage::Mining,
        [](App& a) { a.pending_antivirus_dialog_ = true; },
        [](App& a) { a.pending_antivirus_dialog_ = false; });
    add("modal-switch-stopnode", ui::NavPage::Settings,
        [](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; },
        [](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); });
    add("modal-switch-progress", ui::NavPage::Settings,
        [](App& a) { a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::Stopping));
                     a.wallet_switch_dialog_open_.store(true); },
        [](App& a) { a.wallet_switch_dialog_open_.store(false);
                     a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::None)); });
    // Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
    add("modal-shield", ui::NavPage::Send,
        [](App&) { ui::ShieldDialog::showShieldCoinbase(); },
        [](App&) { ui::ShieldDialog::hide(); });
    add("modal-merge", ui::NavPage::Send,
        [](App&) { ui::ShieldDialog::showMerge(); },
        [](App&) { ui::ShieldDialog::hide(); });
    // z->t transfer so the (converted) deshielding DialogWarningHeader renders.
    add("modal-transfer", ui::NavPage::Overview,
        [](App& a) {
            ui::AddressTransferInfo info;
            info.fromAddr = kDemoZAddr;
            info.toAddr = "t1DemoTransparentReceiveAddress00000";
            info.fromBalance = 12.5; info.toBalance = 3.0;
            info.fromIsZ = true; info.toIsZ = false;
            ui::AddressTransferDialog::show(&a, info);
        },
        [](App&) { ui::AddressTransferDialog::close(); });
    // Distinct from the settings "modal-export-key" (App::renderExportKeyDialog) — this is the
    // per-address KeyExportDialog opened from Overview address rows.
    add("modal-key-export", ui::NavPage::Overview,
        [](App&) { ui::KeyExportDialog::show(kDemoZAddr, ui::KeyExportDialog::KeyType::Private); },
        [](App&) { ui::KeyExportDialog::hide(); });
    // Contacts address-list view modes, each seeded with demo contacts (restored after; no disk write).
    add("contacts-cards", ui::NavPage::Contacts,
        [](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(0); },
        [](App& a) { restoreSweepContacts(a); });
    add("contacts-list", ui::NavPage::Contacts,
        [](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1); },
        [](App& a) { restoreSweepContacts(a); });
    add("contacts-table", ui::NavPage::Contacts,
        [](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(2); },
        [](App& a) { restoreSweepContacts(a); });
    // Revamped edit dialog: live preview + avatar picker, one surface per avatar mode.
    add("contacts-edit-icon", ui::NavPage::Contacts,
        [](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
                     ui::ContactsSweepOpenEditDialog(1); },
        [](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
    add("contacts-edit-badge", ui::NavPage::Contacts,
        [](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
                     ui::ContactsSweepOpenEditDialog(0); },
        [](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
    add("contacts-edit-image", ui::NavPage::Contacts,
        [](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
                     ui::ContactsSweepOpenEditDialog(2); },
        [](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
    add("modal-about", ui::NavPage::Overview,
        [](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
    add("modal-settings", ui::NavPage::Settings,
        [](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; });

    // Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC.
    add("modal-seed-backup", ui::NavPage::Overview,
        [](App& a) {
            a.show_seed_backup_ = true; a.seed_backup_fetch_started_ = true;
            a.seed_backup_loading_ = false; a.seed_backup_no_mnemonic_ = false;
            a.seed_backup_status_.clear();
            if (a.seed_backup_phrase_.empty()) a.seed_backup_phrase_ = kDemoMnemonic;
        },
        [](App& a) {
            a.show_seed_backup_ = false; a.seed_backup_fetch_started_ = false;
            if (!a.seed_backup_phrase_.empty())
                sodium_memzero(&a.seed_backup_phrase_[0], a.seed_backup_phrase_.size());
            a.seed_backup_phrase_.clear();
        });

    // Migrate-to-seed — one surface per step (full-node only). Setting the step directly never
    // fires the async create/sweep/adopt (those are button-triggered).
    if (supportsFullNodeLifecycleActions()) {
        auto mig = [&](const char* name, SeedMigrationStep step, std::function<void(App&)> extra) {
            add(name, ui::NavPage::Settings,
                [step, extra](App& a) {
                    a.show_seed_migration_ = true; a.seed_migration_step_ = step;
                    a.seed_migration_dest_ = kDemoZAddr;
                    if (extra) extra(a);
                },
                [](App& a) {
                    a.show_seed_migration_ = false;
                    if (!a.seed_migration_seed_.empty())
                        sodium_memzero(&a.seed_migration_seed_[0], a.seed_migration_seed_.size());
                    a.seed_migration_seed_.clear(); a.seed_migration_status_.clear();
                });
        };
        // Intro pre-flight branches: pre-set the probe result so the sweep captures each variant
        // (the probe itself is guarded off during capture_mode_).
        mig("modal-migrate-intro", SeedMigrationStep::Intro, [](App& a) {
            a.seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
            a.seed_migration_precheck_started_ = true;
        });
        mig("modal-migrate-intro-seeded", SeedMigrationStep::Intro, [](App& a) {
            a.seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
            a.seed_migration_precheck_started_ = true;
        });
        mig("modal-migrate-intro-oldnode", SeedMigrationStep::Intro, [](App& a) {
            a.seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
            a.seed_migration_precheck_started_ = true;
        });
        mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
            [](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; });
        mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
            [](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
        mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
            [](App& a) { a.seed_migration_balance_ = 0.0; a.seed_migration_balance_loaded_ = true; });
        mig("modal-migrate-confirming", SeedMigrationStep::Confirming, [](App& a) {
            a.seed_migration_sweep_confs_ = 1; a.seed_migration_sweep_txid_ = kDemoTxid;
            a.seed_migration_legacy_remaining_ = 0.0;
        });
        mig("modal-migrate-done", SeedMigrationStep::Done, nullptr);
        mig("modal-migrate-error", SeedMigrationStep::Error,
            [](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; });

        // Multi-wallet: the wallet-files list. Drop a couple of demo wallet files in the (throwaway)
        // datadir + cache their metadata so the list renders populated.
        add("modal-wallets", ui::NavPage::Settings,
            [](App& a) {
                std::error_code ec;
                const std::string dd = util::Platform::getDragonXDataDir();
                fs::create_directories(dd, ec);
                if (!fs::exists(dd + "/wallet.dat", ec))
                    std::ofstream(dd + "/wallet.dat", std::ios::binary) << std::string(122880, '\0');
                if (!fs::exists(dd + "/wallet-savings.dat", ec))
                    std::ofstream(dd + "/wallet-savings.dat", std::ios::binary) << std::string(65536, '\0');
                data::WalletIndexEntry e1; e1.fileName = "wallet.dat"; e1.displayName = "wallet.dat";
                e1.cachedBalance = 15.7526; e1.cachedAddressCount = 4;
                e1.lastOpenedEpoch = 1720000000; e1.syncedHere = true;
                data::WalletIndexEntry e2; e2.fileName = "wallet-savings.dat"; e2.displayName = "wallet-savings.dat";
                e2.cachedBalance = 250.0; e2.cachedAddressCount = 2;
                e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true;
                a.walletIndex().upsert(e1);
                a.walletIndex().upsert(e2);
                // An out-of-datadir wallet (in an extra folder) so the Import action shows too.
                const std::string extra = util::Platform::getConfigDir() + "extra-wallets";
                fs::create_directories(extra, ec);
                if (!fs::exists(extra + "/my-wallet.dat", ec))
                    std::ofstream(extra + "/my-wallet.dat", std::ios::binary) << std::string(80000, '\0');
                a.walletIndex().addExtraFolder(extra);
                if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
                ui::WalletsDialog::show(&a);
            },
            [](App&) { ui::WalletsDialog::hide(); });

        // Many wallets — exercises the viewport-cap path: the card can't fit all rows, so the list
        // must scroll internally while the create/scan/footer controls stay pinned (esp. at 150%).
        // Teardown removes the extra files it dropped so the plain modal-wallets surface stays a
        // clean 3-wallet reference (both surfaces share the one throwaway datadir).
        static const char* kManyExtra[] = {"wallet-cold.dat", "wallet-trading.dat", "wallet-mining.dat",
                                           "wallet-donations.dat", "wallet-2023.dat", "wallet-payroll.dat"};
        add("modal-wallets-many", ui::NavPage::Settings,
            [](App& a) {
                std::error_code ec;
                const std::string dd = util::Platform::getDragonXDataDir();
                fs::create_directories(dd, ec);
                const char* names[] = {"wallet.dat", "wallet-savings.dat", "wallet-cold.dat",
                                       "wallet-trading.dat", "wallet-mining.dat", "wallet-donations.dat",
                                       "wallet-2023.dat", "wallet-payroll.dat"};
                for (int i = 0; i < 8; ++i) {
                    if (!fs::exists(dd + "/" + names[i], ec))
                        std::ofstream(dd + "/" + names[i], std::ios::binary) << std::string(64000 + i * 4096, '\0');
                    data::WalletIndexEntry e; e.fileName = names[i]; e.displayName = names[i];
                    e.cachedBalance = 5.0 * (i + 1); e.cachedAddressCount = 2 + i;
                    e.lastOpenedEpoch = 1720000000 - i * 86400; e.syncedHere = true;
                    a.walletIndex().upsert(e);
                }
                if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
                ui::WalletsDialog::show(&a);
            },
            [](App& a) {
                ui::WalletsDialog::hide();
                std::error_code ec;
                const std::string dd = util::Platform::getDragonXDataDir();
                for (const char* n : kManyExtra) fs::remove(dd + "/" + n, ec);
            });
    }

    // First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown).
    if (!isLiteBuild()) {
        auto wiz = [&](const char* name, WizardPhase phase) {
            add(name, ui::NavPage::Overview,
                [phase](App& a) { a.wizard_phase_ = phase; },
                [](App& a) { a.wizard_phase_ = WizardPhase::None; });
        };
        wiz("wizard-appearance", WizardPhase::Appearance);
        wiz("wizard-bootstrap", WizardPhase::BootstrapOffer);
        wiz("wizard-encrypt", WizardPhase::EncryptOffer);
        wiz("wizard-pin", WizardPhase::PinSetup);
    }

    // App-state overlays. Teardown restores the healthy demo flags (full restore at sweep end).
    add("overlay-lock", ui::NavPage::Overview,
        [](App& a) { a.state_.encrypted = true; a.state_.locked = true; },
        [](App& a) { a.applyHealthyDemoState(); });
    add("overlay-warmup", ui::NavPage::Overview,
        [](App& a) {
            a.state_.warming_up = true;
            a.state_.warmup_status = "Processing blocks…";
            a.state_.warmup_description = "The node is loading the block index.";
        },
        [](App& a) { a.applyHealthyDemoState(); });
    add("overlay-not-ready", ui::NavPage::Overview,
        [](App& a) { a.state_.connected = false; a.state_.encryption_state_known = false; },
        [](App& a) { a.applyHealthyDemoState(); });

    // Send-confirm popup (state lives in send_tab statics → driven via a debug hook).
    add("popup-send-confirm", ui::NavPage::Send,
        [](App& a) { ui::SweepShowSendConfirm(&a, true); },
        [](App& a) { ui::SweepShowSendConfirm(&a, false); });

    // Market portfolio row styles — capture all three so the redesign is reviewable per skin.
    add("market-rows-compact", ui::NavPage::Market,
        [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); },
        [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
    add("market-rows-detailed", ui::NavPage::Market,
        [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); },
        [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
    add("market-rows-featured", ui::NavPage::Market,
        [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); },
        [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });

    // Console RPC command-reference popup (fixed-height dialog with a fill-height command list).
    add("modal-console-commands", ui::NavPage::Console,
        [](App& a) { a.console_tab_.sweepSetCommandsPopup(true); },
        [](App& a) { a.console_tab_.sweepSetCommandsPopup(false); });

    // Debug-options gate: confirmation + warning, with the passphrase re-auth field (encrypted).
    add("modal-debug-gate", ui::NavPage::Settings,
        [](App& a) { a.state_.encrypted = true; ui::SweepOpenDebugGate(true); },
        [](App& a) { ui::SweepOpenDebugGate(false); a.applyHealthyDemoState(); });

    // Daemon updater — the two-pane version picker (versions left, selected-version detail right).
    // Seeds fake releases so the offline sweep renders it without a network fetch / live updater.
    add("modal-daemon-update", ui::NavPage::Settings,
        [](App& a) {
            auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
                util::DaemonRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
                r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
                util::DaemonReleaseAsset as;
                as.name = std::string("dragonx-") + tag + "-linux-amd64.zip";
                as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 43000000;
                r.assets.push_back(as);
                return r;
            };
            std::vector<util::DaemonRelease> rels;
            rels.push_back(mk("v1.0.4", "DragonX v1.0.4", "2026-06-28",
                "## What is DragonX?\n\n"
                "DragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. "
                "It enforces mandatory z2z (shielded-to-shielded) transactions after block 340,000.\n\n"
                "## Bug Fixes\n"
                "* Fix sapling pool persistence \xE2\x80\x94 pool total no longer resets to 0 on restart\n"
                "* Add `subsidy` and `fees` fields to the `getblock` RPC response\n\n"
                "## Key Features\n"
                "* **RandomX Proof-of-Work** \xE2\x80\x94 CPU-mineable, ASIC-resistant\n"
                "* **Sapling zk-SNARKs** \xE2\x80\x94 zero-knowledge proofs for private transactions\n"
                "* **Encrypted P2P** \xE2\x80\x94 all connections secured with TLS 1.3 via WolfSSL\n\n"
                "## Checksums\n| File | SHA-256 |\n"
                "|---|---|\n| dragonx-v1.0.4-linux-amd64.zip | `ab12cd34` |\n", false));
            rels.push_back(mk("v1.0.3", "DragonX v1.0.3", "2026-05-14",
                "## Notes\n- Stability improvements\n- RPC fixes\n", false));
            rels.push_back(mk("v1.0.2", "DragonX v1.0.2", "2026-04-02",
                "- First tagged mainnet build\n", false));
            rels.push_back(mk("v1.1.0-rc1", "DragonX v1.1.0-rc1", "2026-07-01",
                "Release candidate for the 1.1.0 series. Testing only.\n", true));
            ui::DaemonUpdateDialog::sweepSeed(&a, rels, util::DaemonUpdater::State::ReleaseList,
                                              "v1.0.3-dc45e7d90");
        },
        [](App&) { ui::DaemonUpdateDialog::sweepClose(); });

    // Miner (xmrig) updater — same two-pane version picker, seeded with fake releases for the sweep.
    add("modal-xmrig-update", ui::NavPage::Mining,
        [](App& a) {
            auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
                util::XmrigRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
                r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
                util::XmrigReleaseAsset as;
                as.name = std::string("drg-xmrig-") + tag + "-linux-x64.zip";
                as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 8000000;
                r.assets.push_back(as);
                return r;
            };
            std::vector<util::XmrigRelease> rels;
            rels.push_back(mk("v6.25.3", "DRG-XMRig v6.25.3", "2026-06-20",
                "## What's new\n- Rebased on upstream XMRig 6.25.3\n- **RandomX** JIT speedups on modern CPUs\n"
                "- Fix `--cpu-priority` parsing on Windows\n\n## Checksums\n| File | SHA-256 |\n"
                "|---|---|\n| drg-xmrig-v6.25.3-linux-x64.zip | `ab12cd34` |\n", false));
            rels.push_back(mk("v6.24.0", "DRG-XMRig v6.24.0", "2026-04-30",
                "## Notes\n- Pool TLS fixes\n- Lower idle CPU\n", false));
            rels.push_back(mk("v6.23.0", "DRG-XMRig v6.23.0", "2026-03-11",
                "- First DRG-XMRig build\n", false));
            rels.push_back(mk("v6.26.0-rc1", "DRG-XMRig v6.26.0-rc1", "2026-07-02",
                "Release candidate. **Testing only.**\n", true));
            ui::XmrigDownloadDialog::sweepSeed(&a, rels, util::XmrigUpdater::State::ReleaseList, "v6.24.0");
        },
        [](App&) { ui::XmrigDownloadDialog::sweepClose(); });

    // In-app folder picker (Wallets → Scan another folder). Populate a small demo tree so the
    // list shows sub-folders + wallet files, then open the wallets dialog + the picker over it.
    add("modal-folder-picker", ui::NavPage::Settings,
        [](App& a) {
            std::error_code ec;
            const std::string demo = util::Platform::getConfigDir() + "picker-demo";
            for (const char* sub : {"Documents", "Downloads", "Backups", "wallet-archive"})
                fs::create_directories(demo + "/" + sub, ec);
            for (const char* f : {"wallet-cold.dat", "wallet-2023.dat"})
                if (!fs::exists(demo + "/" + f, ec))
                    std::ofstream(demo + "/" + f, std::ios::binary) << std::string(66000, '\0');
            ui::WalletsDialog::show(&a);
            ui::FolderPicker::open(demo, [](const std::string&) {});
        },
        [](App&) { ui::FolderPicker::close(); ui::WalletsDialog::hide(); });
}

// ── State machine ───────────────────────────────────────────────────────────────────────────
void App::startSweepImpl(bool full)
{
    if (screenshot_sweep_active_) return;

    sweep_skins_.clear();
    for (const auto& sk : ui::schema::SkinManager::instance().available())
        if (sk.valid) sweep_skins_.push_back(sk.id);
    if (sweep_skins_.empty()) return;

    // Debug Options "Current theme only": sweep just the active skin instead of cycling every theme.
    if (sweep_current_theme_only_)
        sweep_skins_.assign(1, ui::schema::SkinManager::instance().activeSkinId());

    // DEV/TEST hook (dormant unless the env is set): DRAGONX_SWEEP_ONLY="send,receive" restricts the
    // sweep to the named surfaces and the dark skin, so a slow large-window run captures just the tab
    // under review instead of all surfaces x every skin.
    const char* sweepOnly = std::getenv("DRAGONX_SWEEP_ONLY");
    std::string sweepOnlyStr = sweepOnly ? sweepOnly : "";
    if (!sweepOnlyStr.empty()) sweep_skins_.assign(1, std::string("dark"));

    sweep_full_ = full;
    if (full) { capture_mode_ = true; installDemoWalletData(); }
    buildSweepCatalog();
    if (!sweepOnlyStr.empty()) {
        std::vector<SweepTarget> keep;
        for (const auto& t : sweep_targets_)
            if (sweepOnlyStr.find(t.name) != std::string::npos) keep.push_back(t);
        sweep_targets_.swap(keep);
    }
    if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }

    sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
    std::error_code ec; fs::create_directories(sweep_dir_, ec);

    sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
    sweep_saved_page_ = current_page_;
    sweep_skin_idx_ = 0; sweep_target_idx_ = 0;
    screenshot_sweep_active_ = true;
    sweep_capture_this_frame_ = false;

    ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
    applySweepTarget();
    ui::Notifications::instance().info(full ? "Full UI sweep running…" : "Screenshot sweep running…");
    DEBUG_LOGF("[Sweep] %s -> %s (%d skins x %d surfaces)\n", full ? "FULL" : "tabs",
               sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_targets_.size());
}

void App::startScreenshotSweep() { startSweepImpl(false); }
void App::startFullUiSweep()     { startSweepImpl(true); }

void App::applySweepTarget()
{
    const SweepTarget& t = sweep_targets_[sweep_target_idx_];
    current_page_ = t.page;
    page_alpha_ = 1.0f;  // skip the page-switch fade so the shot isn't captured mid-animation
    if (t.setup) t.setup(*this);
    // <dir>/<surface>/<skin>.png — one subfolder per surface, one PNG per theme, overwritten in
    // place next sweep. (writePng in main.cpp creates the parent subfolder.)
    sweep_current_path_ = (fs::path(sweep_dir_) / t.name / (sweep_skins_[sweep_skin_idx_] + ".png")).string();
    sweep_settle_frames_ = t.settle;
    sweep_capture_this_frame_ = false;
}

void App::updateScreenshotSweep()
{
    if (!screenshot_sweep_active_) return;
    const SweepTarget& t = sweep_targets_[sweep_target_idx_];
    current_page_ = t.page;  // keep the surface pinned each frame
    page_alpha_ = 1.0f;
    // Re-run setup every frame: idempotent for flags/enums, and required for OpenPopup-based popups
    // (must re-fire while open) + to keep the surface state fixed against any refresh.
    if (t.setup) t.setup(*this);
    if (sweep_settle_frames_ > 0) { sweep_settle_frames_--; sweep_capture_this_frame_ = false; }
    else sweep_capture_this_frame_ = true;  // settled — main.cpp captures this frame
}

void App::onScreenshotCaptured()
{
    sweep_capture_this_frame_ = false;
    if (!screenshot_sweep_active_) return;

    { const SweepTarget& t = sweep_targets_[sweep_target_idx_]; if (t.teardown) t.teardown(*this); }

    // Some surfaces leave an ImGui popup open (e.g. the send-confirm dialog calls OpenPopup but is
    // torn down by just clearing its flag). The headless sweep never clicks, so ImGui's normal
    // click-to-dismiss cleanup never runs and the popup lingers on the stack — which keeps
    // IsPopupOpen(AnyPopup) true forever and disables the smooth-scroll wheel capture on
    // Settings/Explorer/Console (draw_helpers::ApplySmoothScroll). Flush any lingering popup here so
    // it can't bleed into the next surface's capture or survive past the sweep.
    if (ImGuiContext* g = ImGui::GetCurrentContext(); g && g->OpenPopupStack.Size > 0)
        ImGui::ClosePopupToLevel(0, false);

    if (++sweep_target_idx_ >= static_cast<int>(sweep_targets_.size())) {
        sweep_target_idx_ = 0;
        if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
            // Done — restore the original skin + page, and (full) the real state.
            const bool wasFull = sweep_full_;
            if (wasFull) writeSweepManifest();
            ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
            current_page_ = sweep_saved_page_;
            page_alpha_ = 1.0f;
            if (wasFull) { clearDemoWalletData(); capture_mode_ = false; }
            sweep_full_ = false;
            screenshot_sweep_active_ = false;
            ui::Notifications::instance().success(
                (wasFull ? std::string("Full UI screenshots saved to ") : std::string("Screenshots saved to ")) + sweep_dir_);
            DEBUG_LOGF("[Sweep] done -> %s\n", sweep_dir_.c_str());
            return;
        }
        ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[sweep_skin_idx_]);
    }
    applySweepTarget();
}

void App::writeSweepManifest() const
{
    std::error_code ec; fs::create_directories(sweep_dir_, ec);
    std::ofstream out((fs::path(sweep_dir_) / "index.md").string(), std::ios::trunc);
    if (!out) return;
    out << "# Full UI sweep\n\n";
    out << sweep_targets_.size() << " surfaces x " << sweep_skins_.size() << " skins\n\n";
    for (const auto& t : sweep_targets_) {
        out << "## " << t.name << "  (base: " << sweepPageName(t.page) << ")\n\n";
        for (const auto& skin : sweep_skins_)
            out << "- " << skin << ": `" << t.name << "/" << skin << ".png`\n";
        out << "\n";
    }
}

}  // namespace dragonx
