// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// app_security.cpp — Wallet encryption, lock screen, PIN management
// Split from app.cpp for maintainability.

#include "app.h"
#include "rpc/rpc_client.h"
#include "rpc/rpc_worker.h"
#include "rpc/connection.h"
#include "services/wallet_security_workflow_executor.h"
#include "config/settings.h"
#include "daemon/embedded_daemon.h"
#include "ui/notifications.h"
#include "ui/material/color_theme.h"
#include "ui/material/type.h"
#include "ui/material/typography.h"
#include "ui/material/draw_helpers.h"
#include "ui/schema/ui_schema.h"
#include "ui/theme.h"
#include "ui/effects/imgui_acrylic.h"
#include "ui/windows/mining_tab.h"
#include "util/platform.h"
#include "util/secure_vault.h"
#include "util/perf_log.h"
#include "embedded/IconsMaterialDesign.h"

#include "imgui.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cstring>
#include <ctime>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <vector>
#include <utility>
#include <sodium.h>
#include <functional>
#include <memory>
#include <utility>

namespace dragonx {

using json = nlohmann::json;

namespace {

class WalletSecurityRpcAdapter : public services::WalletSecurityController::RpcGateway {
public:
    explicit WalletSecurityRpcAdapter(rpc::RPCClient* rpc, std::string source = "Security settings")
        : rpc_(rpc), source_(std::move(source)) {}

    bool encryptWallet(const std::string& passphrase, std::string& error) override {
        return callWithError([&] { rpc_->call("encryptwallet", {passphrase}); }, error);
    }

    bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) override {
        return callWithError([&] { rpc_->call("walletpassphrase", {passphrase, timeoutSeconds}); }, error);
    }

    bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) override {
        return callWithError([&] { rpc_->call("z_exportwallet", {fileName}, timeoutSeconds); }, error);
    }

    bool importWallet(const std::string& filePath, long timeoutSeconds, std::string& error) override {
        return callWithError([&] { rpc_->call("z_importwallet", {filePath}, timeoutSeconds); }, error);
    }

private:
    template <typename Fn>
    bool callWithError(Fn&& fn, std::string& error) {
        if (!rpc_) {
            error = "RPC client unavailable";
            return false;
        }
        try {
            rpc::RPCClient::TraceScope trace(source_);
            fn();
            return true;
        } catch (const std::exception& e) {
            error = e.what();
            return false;
        }
    }

    rpc::RPCClient* rpc_ = nullptr;
    std::string source_;
};

class WalletSecurityVaultAdapter : public services::WalletSecurityController::VaultGateway {
public:
    explicit WalletSecurityVaultAdapter(util::SecureVault* vault) : vault_(vault) {}

    bool storePin(const std::string& pin, const std::string& passphrase) override {
        return vault_ && vault_->store(pin, passphrase);
    }

private:
    util::SecureVault* vault_ = nullptr;
};

class WalletSecurityDecryptRpcAdapter : public services::WalletSecurityWorkflowExecutor::RpcGateway {
public:
    using StopFn = std::function<bool(rpc::RPCClient&, const char*)>;

    WalletSecurityDecryptRpcAdapter(rpc::RPCClient* rpc, StopFn stopFn,
                                    std::string source = "Security / Decrypt wallet workflow")
        : rpc_(rpc), stopFn_(std::move(stopFn)), source_(std::move(source)) {}

    bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) override {
        return callWithError([&] { rpc_->call("walletpassphrase", {passphrase, timeoutSeconds}); }, error);
    }

    bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) override {
        return callWithError([&] { rpc_->call("z_exportwallet", {fileName}, timeoutSeconds); }, error);
    }

    bool requestDaemonStop(std::string& error) override {
        if (!rpc_) {
            error = "RPC client unavailable";
            return false;
        }
        bool ok = stopFn_ ? stopFn_(*rpc_, "Decrypt export daemon stop") : false;
        if (!ok) error = "Stop RPC failed";
        return ok;
    }

    bool probeDaemon(std::string& error) override {
        return callWithError([&] { rpc_->call("getinfo"); }, error);
    }

private:
    template <typename Fn>
    bool callWithError(Fn&& fn, std::string& error) {
        if (!rpc_) {
            error = "RPC client unavailable";
            return false;
        }
        try {
            rpc::RPCClient::TraceScope trace(source_);
            fn();
            return true;
        } catch (const std::exception& e) {
            error = e.what();
            return false;
        }
    }

    rpc::RPCClient* rpc_ = nullptr;
    StopFn stopFn_;
    std::string source_;
};

class WalletSecurityImportRpcAdapter : public services::WalletSecurityWorkflowExecutor::ImportGateway {
public:
    WalletSecurityImportRpcAdapter(rpc::RPCClient* fallbackRpc, rpc::ConnectionConfig config)
        : fallbackRpc_(fallbackRpc), config_(std::move(config)) {}

    bool importWallet(const std::string& exportPath, long timeoutSeconds, std::string& error) override {
        auto importRpc = std::make_unique<rpc::RPCClient>();
        bool importRpcOk = importRpc->connect(config_.host, config_.port,
                                              config_.rpcuser, config_.rpcpassword,
                                              config_.use_tls);
        if (!importRpcOk) importRpc.reset();

        auto* rpcForImport = importRpc ? importRpc.get() : fallbackRpc_;
        if (!rpcForImport) {
            error = "RPC client unavailable";
            return false;
        }

        try {
            rpc::RPCClient::TraceScope trace("Security / Import wallet workflow");
            rpcForImport->call("z_importwallet", {exportPath}, timeoutSeconds);
            if (importRpc) importRpc->disconnect();
            return true;
        } catch (const std::exception& e) {
            if (importRpc) importRpc->disconnect();
            error = e.what();
            return false;
        }
    }

private:
    rpc::RPCClient* fallbackRpc_ = nullptr;
    rpc::ConnectionConfig config_;
};

class WalletSecurityFileAdapter : public services::WalletSecurityWorkflowExecutor::FileGateway {
public:
    std::string dataDir() override { return util::Platform::getDragonXDataDir(); }

    bool backupEncryptedWallet(const services::WalletSecurityWorkflowExecutor::WalletFilePlan& filePlan,
                               std::string& error) override {
        std::error_code ec;
        if (!std::filesystem::exists(filePlan.walletPath, ec)) return true;

        std::filesystem::remove(filePlan.backupPath, ec);
        ec.clear();
        std::filesystem::rename(filePlan.walletPath, filePlan.backupPath, ec);
        if (ec) {
            error = ec.message();
            return false;
        }
        return true;
    }
};

class WalletSecurityDaemonAdapter : public services::WalletSecurityWorkflowExecutor::DaemonGateway {
public:
    WalletSecurityDaemonAdapter(App& app, const util::AsyncTaskManager::Token& token)
        : app_(app), token_(token) {}

    bool isUsingEmbeddedDaemon() const override { return app_.isUsingEmbeddedDaemon(); }
    void stopEmbeddedDaemon() override { app_.stopEmbeddedDaemon(); }
    bool startEmbeddedDaemon() override { return app_.startEmbeddedDaemon(); }
    bool cancelled() const override { return token_.cancelled(); }
    bool shuttingDown() const override { return app_.isShuttingDown(); }
    void sleepForMs(int milliseconds) override {
        std::this_thread::sleep_for(std::chrono::milliseconds(std::max(0, milliseconds)));
    }

private:
    App& app_;
    const util::AsyncTaskManager::Token& token_;
};

} // namespace


// ===========================================================================
// Wallet encryption helpers
// ===========================================================================

// The daemon shuts itself down after encryptwallet. Restart the embedded
// daemon off the main thread (to avoid stalling the UI), or ask the user to
// restart an external daemon. Shared by encryptWalletWithPassphrase() and
// processDeferredEncryption(); must be called on the main thread.
// Zero (overwrite) then delete a plaintext key export so a full cleartext dump of every private key is
// never left readable on disk. Idempotent + error-tolerant (safe on a missing/locked file). (H-02)
void App::scrubAndRemoveExport(const std::string& path)
{
    if (path.empty()) return;
    std::error_code ec;
    const auto sz = std::filesystem::file_size(path, ec);
    if (!ec && sz > 0) {
        std::fstream scrub(path, std::ios::binary | std::ios::in | std::ios::out);
        if (scrub) {
            const std::vector<char> zeros(static_cast<size_t>(sz), 0);
            scrub.write(zeros.data(), static_cast<std::streamsize>(sz));
            scrub.flush();
        }
    }
    std::filesystem::remove(path, ec);
}

// Startup net for H-02: a crash/kill/early-return between exporting the cleartext keys and scrubbing them
// could leave an obsidiandecryptexport* file behind. Purge any found in the data dir on launch.
void App::sweepStaleDecryptExports()
{
    std::error_code ec;
    const std::string dir = util::Platform::getDragonXDataDir();
    std::filesystem::directory_iterator it(dir, ec), end;
    for (; it != end; it.increment(ec)) {
        if (ec) break;
        const std::string name = it->path().filename().string();
        if (name.rfind("obsidiandecryptexport", 0) == 0) {
            scrubAndRemoveExport(it->path().string());
            DEBUG_LOGF("[decrypt] swept stale plaintext key export: %s\n", name.c_str());
        }
    }
}

void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus) {
    if (isUsingEmbeddedDaemon()) {
        if (announceRestartStatus) {
            // Update connection_status_ so the loading overlay explains why
            // the daemon is restarting.
            connection_status_ = TR("restarting_after_encryption");
        }
        // Gate the main-loop reconnect while the daemon is down (so tryConnect can't hit the stopped
        // node or start a duplicate) and block a concurrent wallet switch/rescan, which check this flag.
        daemon_restarting_ = true;
        // Give daemon a moment to shut down, then restart
        // (do this off the main thread to avoid stalling the UI)
        async_tasks_.submit(taskName, [this](const util::AsyncTaskManager::Token& token) {
            // daemon_restarting_ MUST be cleared on every exit (incl. an early-out or a throw), else it
            // stays stuck true and wedges reconnect + all future switch/rescan/encryption operations.
            try {
                for (int i = 0; i < 20 && !token.cancelled() && !shutting_down_; ++i)
                    std::this_thread::sleep_for(std::chrono::milliseconds(100));
                if (!token.cancelled() && !shutting_down_) {
                    stopEmbeddedDaemon();
                    if (!token.cancelled() && !shutting_down_) startEmbeddedDaemon();
                }
            } catch (...) {}
            daemon_restarting_ = false;   // re-arm reconnect (tryConnect runs from the update loop)
        });
    } else {
        ui::Notifications::instance().warning(
            TR("sec_restart_daemon_for_encryption"));
    }
}

void App::encryptWalletWithPassphrase(const std::string& passphrase) {
    if (!rpc_ || !rpc_->isConnected()) return;
    encrypt_in_progress_ = true;
    encrypt_status_ = TR("sec_encrypting_wallet");

    if (worker_) {
        worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
            WalletSecurityRpcAdapter rpcAdapter(rpc_.get());
            auto result = wallet_security_.runDeferredEncryption(
                {std::move(passphrase), {}}, rpcAdapter, nullptr);
            if (result.encrypted) {
                return [this]() {
                    encrypt_in_progress_ = false;
                    encrypt_status_ = TR("sec_wallet_encrypted_restarting_daemon");
                    DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");

                    // Immediately update local encryption state so the
                    // settings page reflects that the wallet is now encrypted
                    // (the daemon is about to restart, so getwalletinfo won't
                    //  be available for a while).
                    state_.encrypted = true;
                    state_.locked    = true;
                    state_.unlocked_until = 0;
                    state_.encryption_state_known = true;

                    // Transition settings dialog to PIN setup phase
                    if (show_encrypt_dialog_ &&
                        encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
                        encrypt_dialog_phase_ = EncryptDialogPhase::PinSetup;
                    }

                    ui::Notifications::instance().info(
                        TR("sec_wallet_encrypted_successfully"), 5.0f);

                    // The daemon shuts itself down after encryptwallet.
                    // Update connection_status_ so the loading overlay
                    // explains why the daemon is restarting.
                    restartDaemonAfterEncryption("encrypt-daemon-restart",
                                                 /*announceRestartStatus=*/true);
                };
            } else {
                std::string err = result.error;
                return [this, err]() {
                    encrypt_in_progress_ = false;
                    encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
                    DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());

                    ui::Notifications::instance().error(
                        std::string(TR("sec_encryption_failed_prefix")) + err);

                    // Return to passphrase entry on failure
                    if (show_encrypt_dialog_ &&
                        encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
                        encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
                    }
                };
            }
        });
    }
}

// ---------------------------------------------------------------------------
// Deferred encryption — runs after wizard exits, encrypts wallet in background
// Called every frame from render() until the task completes.
// ---------------------------------------------------------------------------
void App::processDeferredEncryption() {
    if (!wallet_security_.hasDeferredEncryption()) return;

    // Phase 1: wait for daemon connection
    if (!state_.connected || !rpc_ || !rpc_->isConnected()) {
        if (wallet_security_.shouldAttemptDeferredConnect(ImGui::GetTime())) {
            if (!connection_in_progress_) {
                // Just try to connect — tryConnect is now async
                tryConnect();
                if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
                    startEmbeddedDaemon();
                }
            }
        }
        return; // try again next frame
    }

    // Phase 2: connected — launch encryption
    if (!encrypt_in_progress_) {
        auto deferredEncryption = wallet_security_.deferredEncryption();
        std::string passphrase = std::move(deferredEncryption.passphrase);
        std::string pin = std::move(deferredEncryption.pin);

        encrypt_in_progress_ = true;
        encrypt_status_ = TR("sec_encrypting_wallet");

        if (worker_) {
            worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
                WalletSecurityRpcAdapter rpcAdapter(rpc_.get());
                WalletSecurityVaultAdapter vaultAdapter(vault_.get());
                auto result = wallet_security_.runDeferredEncryption(
                    std::move(request), rpcAdapter, vault_ ? &vaultAdapter : nullptr);

                if (result.encrypted) {
                    return [this, result]() {
                        encrypt_in_progress_ = false;
                        encrypt_status_.clear();
                        DEBUG_LOGF("[App] Wallet encrypted (deferred)\n");

                        // Finalize PIN settings on main thread
                        if (result.pinProvided) {
                            if (result.pinStored) {
                                settings_->setPinEnabled(true);
                                settings_->save();
                                ui::Notifications::instance().info(TR("sec_wallet_encrypted_and_pin_set"), 5.0f);
                            } else {
                                ui::Notifications::instance().warning(
                                    TR("sec_wallet_encrypted_but_pin_vault_failed"));
                            }
                        } else {
                            ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
                        }

                        wallet_security_.clearDeferredEncryption();

                        // Restart daemon (it shuts itself down after encryptwallet)
                        restartDaemonAfterEncryption("deferred-encrypt-daemon-restart",
                                                     /*announceRestartStatus=*/false);
                    };
                } else {
                    std::string err = result.error;
                    return [this, err]() {
                        encrypt_in_progress_ = false;
                        encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
                        DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
                        ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
                        wallet_security_.clearDeferredEncryption();
                    };
                }
            });
        }
    }
}

// Shared main-thread success continuation for both the passphrase and PIN unlock paths.
// walletpassphrase has already succeeded on the worker thread, so set the unlocked state
// directly rather than issuing another RPC round-trip.
void App::applyUnlockSuccess(const std::string& passphrase, int timeout) {
    lock_unlock_in_progress_ = false;
    lock_error_msg_.clear();
    lock_attempts_ = 0;
    memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
    last_interaction_ = std::chrono::steady_clock::now();
    state_.encrypted = true;
    state_.locked = false;
    state_.unlocked_until = std::time(nullptr) + timeout;
    unlockTransactionHistoryCacheWithPassphrase(passphrase);
}

// Shared main-thread failure continuation: bump the attempt counter, surface the error,
// and apply the escalating lockout curve. Every failed unlock path routes through here so
// none can silently skip the lockout math (the PIN RPC-error path previously did).
void App::applyUnlockFailure(const std::string& errorMessage) {
    lock_unlock_in_progress_ = false;
    lock_attempts_++;
    lock_error_msg_ = errorMessage;
    lock_error_timer_ = 3.0f;
    memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
    const float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
    const int maxAttempts = (int)ui::schema::UI().drawElement("security", "max-attempts-before-lockout").sizeOr(5.0f);
    if (lock_attempts_ >= maxAttempts) {
        lock_lockout_timer_ = baseDelay * (float)(1 << std::min(lock_attempts_ - maxAttempts, 8));
    }
    DEBUG_LOGF("[App] Wallet unlock failed (attempt %d)\n", lock_attempts_);
}

void App::unlockWallet(const std::string& passphrase, int timeout) {
    if (!rpc_ || !rpc_->isConnected() || !worker_) return;
    lock_unlock_in_progress_ = true;

    // Use fast-lane worker to bypass head-of-line blocking behind refreshData.
    auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
    auto* r = (fast_rpc_ && fast_rpc_->isConnected())     ? fast_rpc_.get()    : rpc_.get();
    w->post([this, r, passphrase = std::string(passphrase), timeout]() mutable -> rpc::RPCWorker::MainCb {
        std::string err_msg;
        WalletSecurityRpcAdapter rpcAdapter(r);
        bool ok = rpcAdapter.unlockWallet(passphrase, timeout, err_msg);
        std::string cachePassphrase = passphrase;
        util::SecureVault::secureZero(passphrase.data(), passphrase.size());

        return [this, ok, err_msg, timeout, passphrase = std::move(cachePassphrase)]() mutable {
            if (ok) {
                applyUnlockSuccess(passphrase, timeout);
            } else {
                DEBUG_LOGF("[App] Passphrase unlock RPC error: %s\n", err_msg.c_str());
                applyUnlockFailure(TR("incorrect_passphrase"));
            }
            util::SecureVault::secureZero(passphrase.data(), passphrase.size());
        };
    });
}

void App::lockWallet() {
    if (!rpc_ || !rpc_->isConnected() || !worker_) return;
    if (lock_unlock_in_progress_) return;  // Prevent duplicate async calls
    lock_unlock_in_progress_ = true;

    // Use fast-lane worker to avoid blocking behind refreshData.
    auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
    auto* r = (fast_rpc_ && fast_rpc_->isConnected())     ? fast_rpc_.get()    : rpc_.get();
    w->post([this, r]() -> rpc::RPCWorker::MainCb {
        bool ok = false;
        try {
            r->call("walletlock");
            ok = true;
        } catch (...) {}

        return [this, ok]() {
            lock_unlock_in_progress_ = false;
            if (ok) {
                state_.locked = true;
                state_.unlocked_until = 0;
                resetTransactionHistoryCacheSession();
                lock_failure_warned_ = false;
                DEBUG_LOGF("[App] Wallet locked\n");
            } else {
                // The walletlock RPC failed — the wallet is still UNLOCKED. Surface it (once) rather
                // than silently leaving an auto-lock unfulfilled and the wallet exposed (W2-4).
                DEBUG_LOGF("[App] walletlock failed — wallet remains unlocked\n");
                if (!lock_failure_warned_) {
                    lock_failure_warned_ = true;
                    ui::Notifications::instance().warning(
                        TR("sec_couldnt_lock_wallet"), 12.0f);
                }
            }
        };
    });
}

void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
    if (!rpc_ || !rpc_->isConnected() || !worker_) return;
    encrypt_in_progress_ = true;
    encrypt_status_ = TR("sec_changing_passphrase");

    auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
    auto* r = (fast_rpc_ && fast_rpc_->isConnected())     ? fast_rpc_.get()    : rpc_.get();
    w->post([this,
             r,
             oldPass = std::string(oldPass),
             newPass = std::string(newPass)]() mutable -> rpc::RPCWorker::MainCb {
        bool ok = false;
        std::string err_msg;
        try {
            r->call("walletpassphrasechange", {oldPass, newPass});
            ok = true;
        } catch (const std::exception& e) {
            err_msg = e.what();
        }
        std::string cacheNewPass = newPass;
        util::SecureVault::secureZero(oldPass.data(), oldPass.size());
        util::SecureVault::secureZero(newPass.data(), newPass.size());

        return [this,
            ok,
            err_msg,
            newPass = std::move(cacheNewPass)]() mutable {
            encrypt_in_progress_ = false;
            if (ok) {
                encrypt_status_.clear();
                show_change_passphrase_ = false;
                memset(change_old_pass_buf_, 0, sizeof(change_old_pass_buf_));
                memset(change_new_pass_buf_, 0, sizeof(change_new_pass_buf_));
                memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
                unlockTransactionHistoryCacheWithPassphrase(newPass);
                storeTransactionHistoryCacheIfAvailable();
                ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
            } else {
                encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
            }
            util::SecureVault::secureZero(newPass.data(), newPass.size());
        };
    });
}

// ===========================================================================
// Refresh wallet encryption state (from getwalletinfo)
// ===========================================================================

void App::refreshWalletEncryptionState() {
    if (!rpc_ || !rpc_->isConnected() || !worker_) return;

    worker_->post([this]() -> rpc::RPCWorker::MainCb {
        json result;
        bool ok = false;
        try {
            rpc::RPCClient::TraceScope trace("Security / Wallet encryption state");
            result = rpc_->call("getwalletinfo");
            ok = true;
        } catch (...) {}

        if (!ok) return nullptr;

        return [this, result]() {
            try {
                if (result.contains("unlocked_until")) {
                    state_.encrypted = true;
                    int64_t until = result["unlocked_until"].get<int64_t>();
                    state_.unlocked_until = until;
                    state_.locked = (until == 0);
                    state_.encryption_state_known = true;
                    // Wallet is encrypted — any pending deferred-encryption request has now been
                    // satisfied (however it completed). Clear the persisted flag (W2-2).
                    if (settings_ && settings_->getEncryptionPending()) {
                        settings_->setEncryptionPending(false);
                        settings_->save();
                    }
                    if (state_.locked) {
                        resetTransactionHistoryCacheSession();
                    } else if (state_.transactions.empty()) {
                        loadTransactionHistoryCacheIfAvailable();
                    } else {
                        storeTransactionHistoryCacheIfAvailable();
                    }
                } else {
                    state_.encrypted = false;
                    state_.locked = false;
                    state_.unlocked_until = 0;
                    state_.encryption_state_known = true;
                    // W2-2: encryption was requested (persisted flag) but the wallet is NOT encrypted,
                    // and no deferred encryption is pending/in-flight — it was lost to a quit/crash or a
                    // failed connect before it applied. Warn (once/session) instead of silently leaving
                    // an unencrypted wallet the user believes is protected. The flag stays set until the
                    // wallet is actually encrypted, so the warning recurs each launch until resolved.
                    if (settings_ && settings_->getEncryptionPending() &&
                        !wallet_security_.hasDeferredEncryption() && !encrypt_in_progress_ &&
                        !encryption_incomplete_warned_) {
                        encryption_incomplete_warned_ = true;
                        ui::Notifications::instance().warning(
                            TR("sec_encryption_did_not_complete"), 30.0f);
                    }
                    if (state_.transactions.empty()) {
                        loadTransactionHistoryCacheIfAvailable();
                    } else {
                        storeTransactionHistoryCacheIfAvailable();
                    }

                    // Wallet is no longer encrypted — if a PIN vault exists,
                    // it's stale (passphrase it protects is gone). Reset PIN
                    // as if it were never set.
                    if (vault_ && vault_->hasVault()) {
                        DEBUG_LOGF("[App] Wallet unencrypted but PIN vault exists — removing stale vault\n");
                        vault_->removeVault();
                    }
                    if (settings_ && settings_->getPinEnabled()) {
                        settings_->setPinEnabled(false);
                        settings_->save();
                    }
                }
            } catch (...) {}
        };
    });
}

// ===========================================================================
// Auto-lock on idle
// ===========================================================================

void App::checkAutoLock() {
    if (capture_mode_) return;  // don't auto-lock while a UI sweep forces the encrypted/locked state
    if (!state_.isEncrypted() || state_.isLocked()) return;

    // Don't auto-lock while mining — mining is a long-running intentional
    // operation and locking the wallet on the daemon side stops solo mining.
    bool miningActive = state_.mining.generate
                        || (xmrig_manager_ && xmrig_manager_->isRunning());
    if (miningActive) {
        // Keep resetting the idle timer so we don't lock the instant mining stops
        last_interaction_ = std::chrono::steady_clock::now();
        return;
    }

    int timeout = settings_ ? settings_->getAutoLockTimeout() : 300;
    if (timeout <= 0) return; // disabled

    auto now = std::chrono::steady_clock::now();
    float elapsed = std::chrono::duration<float>(now - last_interaction_).count();

    if (elapsed >= (float)timeout) {
        // Lite has no daemon `walletlock` — App::lockWallet() early-returns without rpc_, so route
        // lite through lockLiteWallet() (locks the backend + tears down the chat session so no
        // decrypted store / unlocked DB key survives the idle lock). In full-node builds
        // lite_wallet_ is always null, so this branch is a no-op and behaviour is unchanged.
        if (lite_wallet_) lockLiteWallet();
        else              lockWallet();
        DEBUG_LOGF("[App] Auto-locked wallet after %d seconds idle\n", timeout);
    }
}

// ===========================================================================
// Mine when idle — auto-start/stop mining based on system idle state
// Supports two modes:
//   1. Start/Stop mode (default): start mining when idle, stop when active
//   2. Thread scaling mode: mining stays running, thread count changes
// ===========================================================================

void App::checkIdleMining() {
    if (!settings_ || !settings_->getMineWhenIdle()) {
        // Feature disabled — if we previously auto-started, stop now
        if (idle_mining_active_) {
            idle_mining_active_ = false;
            idle_scaled_to_idle_ = false;
            if (settings_ && (settings_->getPoolMode() || !supportsSoloMining())) {
                if (xmrig_manager_ && xmrig_manager_->isRunning())
                    stopPoolMining();
            } else {
                if (state_.mining.generate)
                    stopMining();
            }
        }
        // Reset scaling state when feature is off
        if (idle_scaled_to_idle_) idle_scaled_to_idle_ = false;
        return;
    }

    // Skip idle mining adjustments while thread benchmark is running
    if (ui::IsMiningBenchmarkActive()) return;

    int idleSec = util::Platform::getSystemIdleSeconds();
    int delay = settings_->getMineIdleDelay();
    bool isPool = settings_->getPoolMode() || !supportsSoloMining();
    if (isPool && !supportsPoolMining()) return;
    bool threadScaling = settings_->getIdleThreadScaling();
    int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());

    // GPU-aware idle detection: if enabled, treat GPU utilization >= 10%
    // as "user active" (e.g. watching a video).  Disabled = unrestricted
    // mode that only looks at keyboard/mouse input.
    bool gpuBusy = false;
    if (settings_->getIdleGpuAware()) {
        int gpuUtil = util::Platform::getGpuUtilization();
        gpuBusy = (gpuUtil >= 10);
    }
    bool systemIdle = (idleSec >= delay) && !gpuBusy;

    // Check if mining is already running (manually started by user)
    bool miningActive = isPool
        ? (xmrig_manager_ && xmrig_manager_->isRunning())
        : state_.mining.generate;

    if (threadScaling) {
        // --- Thread scaling mode ---
        // Mining must already be running (started by user). We just adjust threads.
        if (!miningActive || mining_toggle_in_progress_.load()) return;

        int activeThreads = settings_->getIdleThreadsActive();
        int idleThreads   = settings_->getIdleThreadsIdle();
        // Resolve auto values: active defaults to half, idle defaults to all
        if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
        if (idleThreads   <= 0) idleThreads   = maxThreads;
        // Clamp to [1, logical cores] before these reach setgenerate / startPoolMining — a settings field
        // could otherwise carry an arbitrary count straight past every bound. (M-06)
        activeThreads = std::clamp(activeThreads, 1, maxThreads);
        idleThreads   = std::clamp(idleThreads,   1, maxThreads);

        if (systemIdle) {
            // System is idle — scale up to idle thread count
            if (!idle_scaled_to_idle_) {
                idle_scaled_to_idle_ = true;
                if (isPool) {
                    stopPoolMining();
                    startPoolMining(idleThreads);
                } else {
                    startMining(idleThreads);
                }
                DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads);
            }
        } else {
            // User is active (or GPU busy) — scale down to active thread count
            if (idle_scaled_to_idle_) {
                idle_scaled_to_idle_ = false;
                if (isPool) {
                    stopPoolMining();
                    startPoolMining(activeThreads);
                } else {
                    startMining(activeThreads);
                }
                DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, activeThreads);
            } else {
                // Mining just started while user is active — ensure active
                // thread count is applied (grid selection may differ).
                int currentThreads = isPool
                    ? xmrig_manager_->getStats().threads_active
                    : state_.mining.genproclimit;
                if (currentThreads > 0 && currentThreads != activeThreads) {
                    if (isPool) {
                        stopPoolMining();
                        startPoolMining(activeThreads);
                    } else {
                        startMining(activeThreads);
                    }
                    DEBUG_LOGF("[App] Idle thread scaling: initial %d -> %d threads (active)\n", currentThreads, activeThreads);
                }
            }
        }
    } else {
        // --- Start/Stop mode (original behavior) ---
        if (systemIdle) {
            // System is idle — start mining if not already running
            if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
                // For solo mining, need daemon connected and synced
                if (!isPool && (!state_.connected || state_.sync.syncing)) return;

                int threads = settings_->getPoolThreads();
                if (threads <= 0) threads = std::max(1, maxThreads / 2);

                idle_mining_active_ = true;
                if (isPool)
                    startPoolMining(threads);
                else
                    startMining(threads);
                DEBUG_LOGF("[App] Idle mining started after %d seconds idle\n", idleSec);
            }
        } else {
            // User is active — stop mining if we auto-started it
            if (idle_mining_active_) {
                idle_mining_active_ = false;
                if (isPool) {
                    if (xmrig_manager_ && xmrig_manager_->isRunning())
                        stopPoolMining();
                } else {
                    if (state_.mining.generate)
                        stopMining();
                }
                DEBUG_LOGF("[App] Idle mining stopped — user returned\n");
            }
        }
    }
}

// ===========================================================================
// Restart Setup Wizard (from Settings)
// ===========================================================================



// ===========================================================================
// Lock Screen Rendering
// ===========================================================================

void App::renderLockScreen() {
    using namespace ui::material;

    // Full-window lock overlay: cover the ENTIRE viewport (sidebar included) so the whole screen is
    // obscured while locked, not just the tab-content area. A borderless, transparent, focused window
    // over the main viewport (same pattern as the modal overlays; opened from within ##ContentArea,
    // like the portfolio modal). Ended at the bottom of this function.
    ImGuiViewport* vp = ImGui::GetMainViewport();
    ImVec2 winPos = vp->Pos, winSize = vp->Size;
    ImGui::SetNextWindowPos(winPos);
    ImGui::SetNextWindowSize(winSize);
    ImGui::SetNextWindowFocus();
    ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));  // transparent — we draw the backdrop
    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
    ImGui::Begin("##LockOverlay", nullptr,
        ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
        ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
        ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings);
    ImDrawList* dl = ImGui::GetWindowDrawList();
    // Consume input to everything behind (sidebar included) while locked. AllowOverlap so the lock
    // card's widgets (drawn later, on top of this full-window button) still receive clicks/hover —
    // without it this blocker shadows them (dead clicks + a whole-screen hand cursor).
    ImGui::SetNextItemAllowOverlap();
    ImGui::SetCursorScreenPos(winPos);
    ImGui::InvisibleButton("##LockInputBlocker", winSize,
        ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle);

    // Live-blur backdrop over the wallet content while locked — matches the app's modal look AND
    // obscures the wallet for privacy (the old backdrop defaulted to 0 = fully visible). Capture-once
    // on lock-open + resize; the opaque base inside DrawFullWindowBlurBackdrop guarantees the wallet
    // is obscured even when the blur is unavailable (low-spec / acrylic-off / first frame / capture
    // failure), so this can't leak content. Purely visual — the auth logic below is untouched.
    ImVec2 winMax(winPos.x + winSize.x, winPos.y + winSize.y);
    {
        BlurCaptureState& cap = BlurCaptureStateFor("##LockScreen");
        int frame = ImGui::GetFrameCount();
        bool justLocked = (cap.lastSeenFrame < frame - 1);
        cap.lastSeenFrame = frame;
        if (justLocked || cap.w != winSize.x || cap.h != winSize.y) cap.captureFrames = 3;
        cap.w = winSize.x; cap.h = winSize.y;
        CapturingBlurBackdropRef() = (cap.captureFrames > 0);
        if (cap.captureFrames > 0) {
            if (ImDrawCallback liveCap = ui::effects::ImGuiAcrylic::GetLiveCaptureCallback()) {
                dl->AddCallback(liveCap, nullptr);
                dl->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
            }
            cap.captureFrames--;
        }
        DrawFullWindowBlurBackdrop(dl, winPos, winMax, /*allowBlur=*/!justLocked);
        MarkBlurOverlayDrawn(nullptr);  // suppress foreground theme-effect bleed + re-capture on unlock
    }

    // Card. The lock screen is fully hand-drawn (dl->AddText at manual y offsets), so — unlike
    // BeginOverlayDialog cards — nothing scales automatically. Multiply every geometry literal
    // (card size, logo, input/button widths, vertical gaps) by dpiScale() so the card grows with
    // OS DPI / font scale instead of stranding a native-size card in a large window. The font
    // metrics (LegacySize / CalcTextSize) are already DPI-scaled via the atlas, so leave those.
    const float dp = ui::Layout::dpiScale();
    const auto& S = ui::schema::UI();
    float cardW = S.drawElement("screens.lock-screen", "card").getFloat("width", 400.0f);
    float cardH = S.drawElement("screens.lock-screen", "card").height;
    if (cardW <= 0) cardW = 400.0f;
    if (cardH <= 0) cardH = 320.0f;
    cardW *= dp;
    cardH *= dp;

    float cardX = winPos.x + (winSize.x - cardW) * 0.5f;
    float cardY = winPos.y + (winSize.y - cardH) * 0.5f;
    ImVec2 cardMin(cardX, cardY);
    ImVec2 cardMax(cardX + cardW, cardY + cardH);

    // Match the dialog cards: the same glass panel BeginOverlayDialog draws (renders opaque-dark on
    // the blur via the sole-consumer fallback), instead of a flat light SurfaceVariant fill.
    GlassPanelSpec cardGlass;
    cardGlass.rounding = 16.0f;
    cardGlass.fillAlpha = 35;
    cardGlass.borderAlpha = 50;
    cardGlass.borderWidth = 1.0f;
    DrawGlassPanel(dl, cardMin, cardMax, cardGlass);

    float cy = cardY + 24.0f * dp;

    // Logo
    float logoSize = S.drawElement("screens.lock-screen", "logo").sizeOr(64.0f) * dp;
    if (logo_tex_ != 0) {
        float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
        float logoW = logoSize * aspect;
        float logoX = cardX + (cardW - logoW) * 0.5f;
        dl->AddImage(logo_tex_, ImVec2(logoX, cy), ImVec2(logoX + logoW, cy + logoSize));
    }
    cy += logoSize + 16.0f * dp;

    // Title
    ImFont* titleFont = S.resolveFont(S.label("screens.lock-screen", "title").font);
    if (!titleFont) titleFont = ui::material::Type().h5();
    ImFont* captionFont = S.resolveFont(S.label("screens.lock-screen", "error-text").font);
    if (!captionFont) captionFont = ui::material::Type().caption();
    ImU32 textCol = ui::material::OnSurface();

    {
        const char* title = TR("sec_wallet_locked_title");
        ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
        dl->AddText(titleFont, titleFont->LegacySize,
                    ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
        cy += ts.y + 20.0f * dp;
    }

    // Lockout timer
    if (lock_lockout_timer_ > 0.0f) {
        lock_lockout_timer_ -= ImGui::GetIO().DeltaTime;
        if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;

        char msg[128];
        snprintf(msg, sizeof(msg), TR("sec_too_many_attempts_wait"), lock_lockout_timer_);
        ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
        dl->AddText(captionFont, captionFont->LegacySize,
                    ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
        cy += captionFont->LegacySize + 12.0f * dp;
    }

    // Check if PIN vault is available (per-wallet vault presence; not the global getPinEnabled flag).
    bool hasPinVault = vault_ && vault_->hasVault();

    // Mode toggle (PIN / Passphrase) — only show if PIN vault exists
    if (hasPinVault) {
        const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
        const char* modeText = lock_use_pin_ ? " PIN" : TR("sec_mode_passphrase");
        const char* switchLabel = lock_use_pin_
            ? TR("sec_use_passphrase_instead")
            : TR("sec_use_pin_instead");

        // Current mode indicator — icon with icon font, text with caption font
        ImFont* iconFont = ui::material::Type().iconSmall();
        ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, modeIcon);
        ImVec2 textSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, modeText);
        float totalW = iconSize.x + textSize.x;
        float startX = cardX + (cardW - totalW) * 0.5f;
        float textY = cy + (iconSize.y - textSize.y) * 0.5f; // vertically align text to icon
        dl->AddText(iconFont, iconFont->LegacySize,
                    ImVec2(startX, cy), IM_COL32(255,255,255,120), modeIcon);
        dl->AddText(captionFont, captionFont->LegacySize,
                    ImVec2(startX + iconSize.x, textY), IM_COL32(255,255,255,120), modeText);
        cy += std::max(iconSize.y, textSize.y) + 8.0f * dp;

        // Switch link
        ImVec2 sls = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, switchLabel);
        float switchX = cardX + (cardW - sls.x) * 0.5f;
        ImGui::SetCursorScreenPos(ImVec2(switchX, cy));
        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
        if (ImGui::InvisibleButton("##switch_mode", sls)) {
            lock_use_pin_ = !lock_use_pin_;
            memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
            memset(lock_pin_buf_, 0, sizeof(lock_pin_buf_));
            lock_error_msg_.clear();
            lock_screen_was_visible_ = false; // re-trigger auto-focus for new input
        }
        dl->AddText(captionFont, captionFont->LegacySize,
                    ImVec2(switchX, cy), ui::material::Primary(), switchLabel);
        ImGui::PopStyleColor();
        cy += captionFont->LegacySize + 12.0f * dp;
    } else {
        // No PIN vault — don't show toggle, force passphrase mode
        lock_use_pin_ = false;
    }

    // Input field
    float inputW = S.drawElement("screens.lock-screen", "input").getFloat("width", 320.0f);
    if (inputW <= 0) inputW = 320.0f;
    inputW *= dp;
    float inputX = cardX + (cardW - inputW) * 0.5f;

    bool canSubmit = lock_lockout_timer_ <= 0.0f && !lock_unlock_in_progress_;
    bool submitted = false;

    if (lock_use_pin_ && hasPinVault) {
        // PIN input
        ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
        ImGui::PushItemWidth(inputW);
        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
        ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal;
        if (canSubmit) pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
        submitted = ImGui::InputText("##lock_pin", lock_pin_buf_,
                                     sizeof(lock_pin_buf_), pinFlags);
        ImGui::PopStyleVar();
        ImGui::PopItemWidth();
    } else {
        // Passphrase input (original)
        ImGui::SetCursorScreenPos(ImVec2(inputX, cy));
        ImGui::PushItemWidth(inputW);
        ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f * dp);
        ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_Password;
        if (canSubmit) inputFlags |= ImGuiInputTextFlags_EnterReturnsTrue;
        submitted = ImGui::InputText("##lock_pass", lock_passphrase_buf_,
                                     sizeof(lock_passphrase_buf_), inputFlags);
        ImGui::PopStyleVar();
        ImGui::PopItemWidth();
    }
    // Focus indicator: a clear accent ring around the input while it's focused/being edited — the
    // default frame is subtle on the glass card, so this shows the field is active and ready to type.
    if (ImGui::IsItemActive() || ImGui::IsItemFocused()) {
        dl->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
                    ui::material::Primary(), 6.0f * dp, 0, 2.0f * dp);
    }
    cy += (40.0f + 12.0f) * dp;

    // Focus the input when the lock screen first appears.
    // IsWindowAppearing() does not work here because the lock screen is
    // rendered inside ##ContentArea which has already been alive since the
    // first frame.  Instead we track the hidden→visible transition ourselves.
    if (!lock_screen_was_visible_) {
        ImGui::SetKeyboardFocusHere(-1);
        lock_screen_was_visible_ = true;
    }

    // Error message
    if (!lock_error_msg_.empty() && lock_error_timer_ > 0) {
        lock_error_timer_ -= ImGui::GetIO().DeltaTime;
        ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, lock_error_msg_.c_str());
        dl->AddText(captionFont, captionFont->LegacySize,
                    ImVec2(cardX + (cardW - es.x) * 0.5f, cy), ui::material::Error(),
                    lock_error_msg_.c_str());
        cy += captionFont->LegacySize + 8.0f * dp;
    }

    // "Unlocking..." feedback while worker thread is running
    // Always reserve the vertical space so the button doesn't shift.
    {
        float rowH = captionFont->LegacySize + 8.0f * dp;
        if (lock_unlock_in_progress_) {
            // Animated spinner dots
            char msg[64];
            snprintf(msg, sizeof(msg), TR("sec_unlocking_fmt"), ui::material::LoadingDots());
            ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
            dl->AddText(captionFont, captionFont->LegacySize,
                        ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
                        ui::material::Primary(), msg);
        }
        cy += rowH;
    }

    // Unlock button
    float unlockW = S.drawElement("screens.lock-screen", "unlock-button").getFloat("width", 320.0f);
    float unlockH = S.drawElement("screens.lock-screen", "unlock-button").height;
    if (unlockW <= 0) unlockW = 320.0f;
    if (unlockH <= 0) unlockH = 44.0f;
    unlockW *= dp;
    unlockH *= dp;
    float unlockX = cardX + (cardW - unlockW) * 0.5f;

    ImGui::SetCursorScreenPos(ImVec2(unlockX, cy));
    ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
    ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
    ImGui::BeginDisabled(!canSubmit);
    bool btnClicked = ui::material::TactileButton(TR("sec_unlock_button"), ImVec2(unlockW, unlockH));
    ImGui::EndDisabled();
    ImGui::PopStyleVar();
    ImGui::PopStyleColor(3);

    // Handle submit (Enter key or button click)
    if ((submitted || btnClicked) && canSubmit) {
        // Derive daemon unlock timeout from auto-lock setting:
        // 2x the idle timeout so the daemon stays unlocked longer than
        // the GUI auto-lock, acting as a safety net if the GUI crashes.
        // Floor of 600s (10 min) when auto-lock is off or very short.
        int autoLock = settings_ ? settings_->getAutoLockTimeout() : 300;
        int timeout = (autoLock > 0) ? std::max(600, autoLock * 2) : 86400;

        if (lock_use_pin_ && hasPinVault && strlen(lock_pin_buf_) > 0) {
            // PIN unlock — run Argon2id key derivation + RPC off the main
            // thread so the UI stays responsive instead of freezing.
            std::string pin(lock_pin_buf_);
            memset(lock_pin_buf_, 0, sizeof(lock_pin_buf_));
            lock_unlock_in_progress_ = true;

            // Use fast-lane worker for priority unlock.
            auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
            auto* r = (fast_rpc_ && fast_rpc_->isConnected())     ? fast_rpc_.get()    : rpc_.get();
            if (w) {
                w->post([this, r, pin, timeout]() -> rpc::RPCWorker::MainCb {
                    // Heavy Argon2id derivation runs here (worker thread)
                    std::string passphrase;
                    bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);

                    if (!vaultOk) {
                        bool noVault = !vault_ || !vault_->hasVault();
                        return [this, noVault]() {
                            if (noVault) {
                                // Vault file missing — switch to passphrase mode. Not a failed
                                // attempt, so no counter bump / lockout escalation.
                                lock_unlock_in_progress_ = false;
                                lock_error_msg_ = TR("pin_not_set");
                                lock_use_pin_ = false;
                                lock_error_timer_ = 3.0f;
                            } else {
                                applyUnlockFailure(TR("incorrect_pin"));
                            }
                        };
                    }

                    // Vault decrypted — now unlock wallet via RPC (also on worker thread)
                    bool rpcOk = false;
                    std::string rpcErr;
                    try {
                        if (r && r->isConnected()) {
                            r->call("walletpassphrase", {passphrase, timeout});
                            rpcOk = true;
                        } else {
                            rpcErr = TR("sec_not_connected_to_daemon");
                        }
                    } catch (const std::exception& e) {
                        rpcErr = e.what();
                    }

                    if (rpcOk) {
                        return [this, timeout, passphrase = std::move(passphrase)]() mutable {
                            applyUnlockSuccess(passphrase, timeout);
                            util::SecureVault::secureZero(passphrase.data(), passphrase.size());
                        };
                    } else {
                        // Vault decrypt succeeded but the RPC failed — still a failed unlock,
                        // so route through applyUnlockFailure so the lockout curve applies
                        // (this path previously bumped the counter but skipped the lockout math).
                        return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
                            applyUnlockFailure(std::string(TR("sec_unlock_failed_prefix")) + rpcErr);
                            util::SecureVault::secureZero(passphrase.data(), passphrase.size());
                        };
                    }
                });
            }
        } else if (strlen(lock_passphrase_buf_) > 0) {
            // Direct passphrase unlock
            unlockWallet(std::string(lock_passphrase_buf_), timeout);
        }
    }

    ImGui::End();  // ##LockOverlay
    ImGui::PopStyleVar(3);   // WindowRounding, WindowBorderSize, WindowPadding
    ImGui::PopStyleColor();  // WindowBg
}

// ===========================================================================
// Encrypt Wallet Dialog (post-first-run, from Settings)
// ===========================================================================

void App::renderEncryptWalletDialog() {
    if (!show_encrypt_dialog_ && !show_change_passphrase_) return;
    using namespace ui::material;

    // Encrypt wallet dialog — multi-phase: passphrase → encrypting → PIN setup
    if (show_encrypt_dialog_) {
        const char* dlgTitle = (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup)
            ? TR("wiz_pin_title") : TR("settings_encrypt_wallet");

        // Prevent closing via X button while encrypting
        bool canClose = (encrypt_dialog_phase_ != EncryptDialogPhase::Encrypting);
        bool* pOpen = canClose ? &show_encrypt_dialog_ : nullptr;

        OverlayDialogSpec ov;
        ov.title = dlgTitle; ov.p_open = pOpen;
        ov.style = OverlayStyle::BlurFloat;
        ov.cardWidth = 480.0f; ov.idSuffix = "encrypt";
        if (BeginOverlayDialog(ov)) {

            // ---- Phase 1: Passphrase entry ----
            if (encrypt_dialog_phase_ == EncryptDialogPhase::PassphraseEntry) {
                DialogWarningHeader(TR("wiz_encrypt_warning"));
                ImGui::Spacing();

                ImGui::TextWrapped("%s", TR("enc_desc"));
                ImGui::Spacing();

                ImGui::TextUnformatted(TR("wiz_passphrase"));
                ImGui::PushItemWidth(-1);
                ImGui::InputText("##enc_pass", encrypt_pass_buf_, sizeof(encrypt_pass_buf_),
                                 ImGuiInputTextFlags_Password);
                ImGui::PopItemWidth();

                ImGui::TextUnformatted(TR("enc_confirm"));
                ImGui::PushItemWidth(-1);
                ImGui::InputText("##enc_confirm", encrypt_confirm_buf_, sizeof(encrypt_confirm_buf_),
                                 ImGuiInputTextFlags_Password);
                ImGui::PopItemWidth();

                // Strength meter bar
                {
                    size_t len = strlen(encrypt_pass_buf_);
                    // Character-class diversity: an all-digit or single-class
                    // string shouldn't score as high as a mixed one.
                    bool hasDigit = false, hasLower = false, hasUpper = false, hasSymbol = false;
                    for (const char* c = encrypt_pass_buf_; *c; ++c) {
                        unsigned char uc = static_cast<unsigned char>(*c);
                        if (uc >= '0' && uc <= '9')      hasDigit = true;
                        else if (uc >= 'a' && uc <= 'z') hasLower = true;
                        else if (uc >= 'A' && uc <= 'Z') hasUpper = true;
                        else                             hasSymbol = true;
                    }
                    int classes = (int)hasDigit + (int)hasLower + (int)hasUpper + (int)hasSymbol;

                    const char* strengthLabel = TR("wiz_strength_weak");
                    ImVec4 strengthCol(0.9f, 0.2f, 0.2f, 1.0f);
                    float strengthPct = 0.25f;
                    int tier = 0; // 0=Weak, 1=Fair, 2=Good, 3=Strong
                    if (len >= 16)      tier = 3;
                    else if (len >= 12) tier = 2;
                    else if (len >= 8)  tier = 1;
                    // Downgrade one tier when only a single character class is used.
                    if (classes <= 1 && tier > 0) tier -= 1;
                    if (tier == 3)      { strengthLabel = TR("wiz_strength_strong"); strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 1.0f; }
                    else if (tier == 2) { strengthLabel = TR("wiz_strength_good");   strengthCol = ImVec4(0.3f,0.9f,0.5f,1); strengthPct = 0.75f; }
                    else if (tier == 1) { strengthLabel = TR("wiz_strength_fair");   strengthCol = ImVec4(1,0.7f,0.3f,1);    strengthPct = 0.5f; }

                    float barW = ImGui::GetContentRegionAvail().x;
                    float barH = 4.0f * ui::Layout::dpiScale();
                    ImVec2 p = ImGui::GetCursorScreenPos();
                    ImDrawList* dl = ImGui::GetWindowDrawList();
                    dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
                                      IM_COL32(255,255,255,30), 2.0f);
                    if (len > 0)
                        dl->AddRectFilled(p, ImVec2(p.x + barW * strengthPct, p.y + barH),
                                          ImGui::ColorConvertFloat4ToU32(strengthCol), 2.0f);
                    ImGui::Dummy(ImVec2(barW, barH));
                    ImGui::Text(TR("wiz_strength"), strengthLabel);
                }

                if (!encrypt_status_.empty()) {
                    ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", encrypt_status_.c_str());
                }

                ImGui::Spacing();
                bool valid = strlen(encrypt_pass_buf_) >= 8 &&
                             strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;

                float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
                ImGui::BeginDisabled(!valid || encrypt_in_progress_);
                if (ui::material::TactileButton(TR("settings_encrypt_wallet"), ImVec2(btnW, 40))) {
                    std::string pass(encrypt_pass_buf_);
                    enc_dlg_saved_passphrase_ = pass;
                    memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
                    memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
                    encrypt_dialog_phase_ = EncryptDialogPhase::Encrypting;
                    encryptWalletWithPassphrase(pass);
                    util::SecureVault::secureZero(&pass[0], pass.size());
                }
                ImGui::EndDisabled();

                ImGui::SameLine();
                if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
                    memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
                    memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
                    show_encrypt_dialog_ = false;
                }

            // ---- Phase 2: Encrypting in progress ----
            } else if (encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
                const char* statusTitle = encrypt_in_progress_
                    ? TR("enc_encrypting") : encrypt_status_.c_str();
                ImGui::Text("%s", statusTitle);
                ImGui::Spacing();

                // Indeterminate progress bar
                {
                    float barW = ImGui::GetContentRegionAvail().x;
                    float barH = 6.0f * ui::Layout::dpiScale();
                    ImVec2 p = ImGui::GetCursorScreenPos();
                    ImDrawList* dl = ImGui::GetWindowDrawList();
                    dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
                                      IM_COL32(255,255,255,25), 3.0f);
                    float t = (float)ImGui::GetTime();
                    float pulse = 0.5f + 0.5f * sinf(t * 2.0f);
                    float segW = barW * 0.3f;
                    float segX = p.x + (barW - segW) * pulse;
                    dl->AddRectFilled(ImVec2(segX, p.y), ImVec2(segX + segW, p.y + barH),
                                      ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]),
                                      3.0f);
                    ImGui::Dummy(ImVec2(barW, barH));
                }

                if (!encrypt_status_.empty()) {
                    ImGui::TextColored(ImVec4(1, 1, 1, 0.6f), "%s", encrypt_status_.c_str());
                }

                ImGui::Spacing();
                ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", TR("enc_wait"));

                // Transition to PIN phase when encryption finishes successfully
                if (!encrypt_in_progress_ && encrypt_dialog_phase_ == EncryptDialogPhase::Encrypting) {
                    // encryptWalletWithPassphrase callback handles the transition
                }

            // ---- Phase 3: PIN setup (after successful encryption) ----
            } else if (encrypt_dialog_phase_ == EncryptDialogPhase::PinSetup) {
                ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 0.9f, 0.5f, 1));
                ImGui::Text(ICON_MD_CHECK_CIRCLE " %s", TR("enc_success"));
                ImGui::PopStyleColor();
                ImGui::Spacing();

                ImGui::TextWrapped("%s", TR("enc_pin_desc"));
                ImGui::Spacing();

                ImGui::TextUnformatted(TR("wiz_pin_label"));
                ImGui::PushItemWidth(-1);
                ImGui::InputText("##enc_dlg_pin", enc_dlg_pin_buf_, sizeof(enc_dlg_pin_buf_),
                                 ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
                ImGui::PopItemWidth();

                ImGui::TextUnformatted(TR("wiz_pin_confirm"));
                ImGui::PushItemWidth(-1);
                ImGui::InputText("##enc_dlg_pin_confirm", enc_dlg_pin_confirm_buf_,
                                 sizeof(enc_dlg_pin_confirm_buf_),
                                 ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
                ImGui::PopItemWidth();

                if (!enc_dlg_pin_status_.empty()) {
                    ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", enc_dlg_pin_status_.c_str());
                }

                ImGui::Spacing();
                std::string pinStr(enc_dlg_pin_buf_);
                std::string pinConfirm(enc_dlg_pin_confirm_buf_);
                bool pinValid = util::SecureVault::isValidPin(pinStr) && pinStr == pinConfirm;
                bool hasPassphrase = !enc_dlg_saved_passphrase_.empty();

                float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;

                ImGui::BeginDisabled(!pinValid || !hasPassphrase || pin_in_progress_);
                if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
                    pin_in_progress_ = true;
                    enc_dlg_pin_status_.clear();
                    std::string savedPass = enc_dlg_saved_passphrase_;
                    if (worker_ && vault_) {
                        worker_->post([this, pinStr, savedPass]() mutable -> rpc::RPCWorker::MainCb {
                            // Argon2id runs here (worker thread)
                            bool ok = vault_->store(pinStr, savedPass);
                            // Scrub the captured PIN + passphrase copies (they live in the worker's task
                            // queue until this runs); the source member is scrubbed in the MainCb. (L-03)
                            if (!savedPass.empty()) util::SecureVault::secureZero(&savedPass[0], savedPass.size());
                            if (!pinStr.empty())    util::SecureVault::secureZero(&pinStr[0], pinStr.size());
                            return [this, ok]() {
                                if (ok) {
                                    settings_->setPinEnabled(true);
                                    settings_->save();
                                    pin_in_progress_ = false;
                                    ui::Notifications::instance().info(TR("enc_pin_set_ok"));
                                    // Clean up
                                    if (!enc_dlg_saved_passphrase_.empty()) {
                                        util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
                                                                     enc_dlg_saved_passphrase_.size());
                                        enc_dlg_saved_passphrase_.clear();
                                    }
                                    memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
                                    memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
                                    show_encrypt_dialog_ = false;
                                } else {
                                    enc_dlg_pin_status_ = TR("enc_pin_vault_fail");
                                    pin_in_progress_ = false;
                                }
                            };
                        });
                    } else {
                        enc_dlg_pin_status_ = TR("enc_pin_vault_fail");
                        pin_in_progress_ = false;
                    }
                }
                ImGui::EndDisabled();

                ImGui::SameLine();
                if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW, 40))) {
                    if (!enc_dlg_saved_passphrase_.empty()) {
                        util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
                                                     enc_dlg_saved_passphrase_.size());
                        enc_dlg_saved_passphrase_.clear();
                    }
                    memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
                    memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
                    show_encrypt_dialog_ = false;
                    ui::Notifications::instance().info(TR("enc_pin_skipped"));
                }
            }
            EndOverlayDialog();
        }

        // Clean up saved passphrase if dialog was closed via X button
        if (!show_encrypt_dialog_ && !enc_dlg_saved_passphrase_.empty()) {
            util::SecureVault::secureZero(&enc_dlg_saved_passphrase_[0],
                                         enc_dlg_saved_passphrase_.size());
            enc_dlg_saved_passphrase_.clear();
        }
    }

    // Change passphrase dialog
    if (show_change_passphrase_) {
        OverlayDialogSpec ov;
        ov.title = TR("change_pass_title"); ov.p_open = &show_change_passphrase_;
        ov.style = OverlayStyle::BlurFloat;
        ov.cardWidth = 460.0f; ov.idSuffix = "changepass";
        if (BeginOverlayDialog(ov)) {

            // Same fund-loss consequence as Encrypt/Remove Encryption if the new
            // passphrase is lost — reuse their warning string/header for consistency.
            DialogWarningHeader(TR("wiz_encrypt_warning"));
            ImGui::Spacing();

            ImGui::TextUnformatted(TR("change_pass_current"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##chg_old", change_old_pass_buf_, sizeof(change_old_pass_buf_),
                             ImGuiInputTextFlags_Password);
            ImGui::PopItemWidth();

            ImGui::TextUnformatted(TR("change_pass_new"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##chg_new", change_new_pass_buf_, sizeof(change_new_pass_buf_),
                             ImGuiInputTextFlags_Password);
            ImGui::PopItemWidth();

            ImGui::TextUnformatted(TR("change_pass_confirm"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##chg_confirm", change_confirm_buf_, sizeof(change_confirm_buf_),
                             ImGuiInputTextFlags_Password);
            ImGui::PopItemWidth();

            if (!encrypt_status_.empty()) {
                ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", encrypt_status_.c_str());
            }

            ImGui::Spacing();
            bool valid = strlen(change_old_pass_buf_) > 0 &&
                         strlen(change_new_pass_buf_) >= 8 &&
                         strcmp(change_new_pass_buf_, change_confirm_buf_) == 0;

            // Two-button footer (primary + Cancel) to match the encrypt/decrypt siblings.
            float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
            ImGui::BeginDisabled(!valid || encrypt_in_progress_);
            if (ui::material::TactileButton(TR("change_pass_title"), ImVec2(btnW, 40))) {
                changePassphrase(std::string(change_old_pass_buf_),
                                std::string(change_new_pass_buf_));
            }
            ImGui::EndDisabled();

            ImGui::SameLine();
            // Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
            // the !show_change_passphrase_ cleanup block below.
            if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
                show_change_passphrase_ = false;
            }
            EndOverlayDialog();
        }

        // Wipe the passphrase buffers if the dialog was dismissed (X / Esc /
        // outside-click) without submitting. The success path already zeroes
        // them in changePassphrase(); the failure path keeps them for retry.
        if (!show_change_passphrase_) {
            memset(change_old_pass_buf_, 0, sizeof(change_old_pass_buf_));
            memset(change_new_pass_buf_, 0, sizeof(change_new_pass_buf_));
            memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
        }
    }
}

// ===========================================================================
// Decrypt (Remove Encryption) Dialog
// ===========================================================================
// Flow:
//   Phase 0: Enter current passphrase
//   Phase 1: Working — unlock → export → stop → rename → restart → import
//   Phase 2: Success
//   Phase 3: Error
// ===========================================================================

void App::renderDecryptWalletDialog() {
    if (!show_decrypt_dialog_) return;
    using namespace ui::material;
    using DecryptPhase = services::WalletSecurityWorkflow::DecryptPhase;
    using DecryptStep = services::WalletSecurityWorkflow::DecryptStep;

    auto decryptState = wallet_security_workflow_.snapshot();

    bool canClose = wallet_security_workflow_.canClose();
    bool* pOpen = canClose ? &show_decrypt_dialog_ : nullptr;

    OverlayDialogSpec ov;
    ov.title = TR("decrypt_title"); ov.p_open = pOpen;
    ov.style = OverlayStyle::BlurFloat;
    ov.cardWidth = 480.0f; ov.idSuffix = "decrypt";
    if (BeginOverlayDialog(ov)) {

        // ---- Phase 0: Passphrase entry ----
        if (decryptState.phase == DecryptPhase::PassphraseEntry) {
            DialogWarningHeader(TR("decrypt_warning"));
            ImGui::Spacing();

            ImGui::TextWrapped("%s", TR("decrypt_desc"));
            ImGui::Spacing();

            ImGui::TextUnformatted(TR("change_pass_current"));
            ImGui::PushItemWidth(-1);
            bool enterPressed = ImGui::InputText("##decrypt_pass", decrypt_pass_buf_,
                sizeof(decrypt_pass_buf_), ImGuiInputTextFlags_Password |
                ImGuiInputTextFlags_EnterReturnsTrue);
            ImGui::PopItemWidth();

            if (!decryptState.status.empty()) {
                ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", decryptState.status.c_str());
            }

            ImGui::Spacing();
            bool valid = strlen(decrypt_pass_buf_) >= 1;

            float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
            ImGui::BeginDisabled(!valid || decryptState.inProgress);
            if (ui::material::TactileButton(TR("settings_remove_encryption"), ImVec2(btnW, 40)) || (enterPressed && valid)) {
                std::string passphrase(decrypt_pass_buf_);
                memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
                wallet_security_workflow_.start(std::chrono::steady_clock::now());

                // Run entire decrypt flow on worker thread
                if (worker_) {
                    worker_->post([this, passphrase = std::move(passphrase)]() mutable -> rpc::RPCWorker::MainCb {
                        WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
                            [this](rpc::RPCClient& client, const char* context) {
                                return sendStopCommandSafely(client, context);
                            });
                        auto unlock = services::WalletSecurityWorkflowExecutor::unlockWallet(passphrase, decryptRpc);
                        // Scrub the passphrase — unlock is its only use in this flow.
                        if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
                        if (!unlock.ok) {
                                return [this]() {
                                wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
                            };
                        }

                        // Update step on main thread
                        return [this]() {
                            wallet_security_workflow_.advanceTo(DecryptStep::ExportKeys,
                                services::WalletSecurityWorkflow::stepStatus(DecryptStep::ExportKeys),
                                std::chrono::steady_clock::now());
                            
                            // Continue with step 2
                            worker_->post([this]() -> rpc::RPCWorker::MainCb {
                                WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
                                    [this](rpc::RPCClient& client, const char* context) {
                                        return sendStopCommandSafely(client, context);
                                    });
                                WalletSecurityFileAdapter files;
                                auto exportOutcome = services::WalletSecurityWorkflowExecutor::exportWallet(
                                    decryptRpc, files, static_cast<std::uint64_t>(std::time(nullptr)));
                                if (!exportOutcome.ok) {
                                    std::string err = exportOutcome.error;
                                    return [this, err]() {
                                        wallet_security_workflow_.fail(err);
                                    };
                                }

                                auto filePlan = exportOutcome.filePlan;

                                return [this, filePlan]() {
                                    wallet_security_workflow_.advanceTo(DecryptStep::StopDaemon,
                                        services::WalletSecurityWorkflow::stepStatus(DecryptStep::StopDaemon),
                                        std::chrono::steady_clock::now());
                                    
                                    // Continue with step 3
                                    worker_->post([this, filePlan]() -> rpc::RPCWorker::MainCb {
                                        WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
                                            [this](rpc::RPCClient& client, const char* context) {
                                                return sendStopCommandSafely(client, context);
                                            });
                                        services::WalletSecurityWorkflowExecutor::stopDaemon(decryptRpc);

                                        // Wait for daemon to fully stop
                                        for (int i = 0; i < 30 && !shutting_down_; ++i)
                                            std::this_thread::sleep_for(std::chrono::milliseconds(100));

                                        return [this, filePlan]() {
                                            wallet_security_workflow_.advanceTo(DecryptStep::BackupWallet,
                                                services::WalletSecurityWorkflow::stepStatus(DecryptStep::BackupWallet),
                                                std::chrono::steady_clock::now());
                                            
                                            // Continue with step 4 (rename)
                                            worker_->post([this, filePlan]() -> rpc::RPCWorker::MainCb {
                                                WalletSecurityFileAdapter files;
                                                auto backup = services::WalletSecurityWorkflowExecutor::backupEncryptedWallet(files, filePlan);
                                                if (!backup.ok) {
                                                    std::string err = backup.error;
                                                    return [this, err]() {
                                                        wallet_security_workflow_.fail(err);
                                                    };
                                                }

                                                return [this, exportPath = filePlan.exportPath]() {
                                                    wallet_security_workflow_.advanceTo(DecryptStep::RestartDaemon,
                                                        services::WalletSecurityWorkflow::stepStatus(DecryptStep::RestartDaemon),
                                                        std::chrono::steady_clock::now());

                                                    auto restartAndImport = [this, exportPath](const util::AsyncTaskManager::Token& token) {
                                                        // Scrub + delete the plaintext key export (obsidiandecryptexport…) on EVERY exit path —
                                                        // success, a restart-failure early return, or an exception. A full cleartext dump of all
                                                        // private keys must never outlive this step. The startup sweep is a further net for a
                                                        // crash/kill mid-flight. (H-02)
                                                        struct ExportScrub { std::string p; ~ExportScrub() { App::scrubAndRemoveExport(p); } } exportScrub{exportPath};
                                                        WalletSecurityDaemonAdapter daemonAdapter(*this, token);
                                                        WalletSecurityDecryptRpcAdapter decryptRpc(rpc_.get(),
                                                            [this](rpc::RPCClient& client, const char* context) {
                                                                return sendStopCommandSafely(client, context);
                                                            });
                                                        auto restart = services::WalletSecurityWorkflowExecutor::restartDaemonAndWait(
                                                            daemonAdapter, decryptRpc);
                                                        if (!restart.ok) {
                                                            if (restart.error.empty()) return;
                                                            if (worker_) {
                                                                worker_->post([this, err = restart.error]() -> rpc::RPCWorker::MainCb {
                                                                    return [this, err]() {
                                                                        wallet_security_workflow_.fail(err);
                                                                    };
                                                                });
                                                            }
                                                            return;
                                                        }

                                                        // Update step on main thread — close dialog, import in background
                                                        if (worker_) {
                                                            worker_->post([this]() -> rpc::RPCWorker::MainCb {
                                                                return [this]() {
                                                                    // Close the decrypt dialog — user can use the wallet now
                                                                    wallet_security_workflow_.closeDialogForImport();
                                                                    show_decrypt_dialog_ = false;

                                                                    // Mark rescanning so status bar picks it up immediately
                                                                    state_.sync.rescanning = true;
                                                                    state_.sync.rescan_progress = 0.0f;

                                                                    // Clear encryption state early — vault/PIN removed now,
                                                                    // wallet file is already unencrypted
                                                                    services::WalletSecurityWorkflowExecutor::cleanupVaultAndPin([this]() {
                                                                        if (vault_ && vault_->hasVault()) {
                                                                            vault_->removeVault();
                                                                        }
                                                                        if (settings_ && settings_->getPinEnabled()) {
                                                                            settings_->setPinEnabled(false);
                                                                            settings_->save();
                                                                        }
                                                                    });

                                                                    ui::Notifications::instance().info(
                                                                        TR("sec_importing_keys_rescanning"),
                                                                        8.0f);
                                                                };
                                                            });
                                                        }

                                                        WalletSecurityImportRpcAdapter importAdapter(rpc_.get(), saved_config_);
                                                        auto importResult = services::WalletSecurityWorkflowExecutor::importWallet(
                                                            importAdapter, exportPath);
                                                        // (exportScrub scrubs + deletes the plaintext key export on scope exit — H-02)

                                                        if (!importResult.ok) {
                                                            std::string err = importResult.error;
                                                            if (worker_) {
                                                                worker_->post([this, err]() -> rpc::RPCWorker::MainCb {
                                                                    return [this, err]() {
                                                                        wallet_security_workflow_.finishImport();
                                                                        ui::Notifications::instance().error(
                                                                            err +
                                                                            TR("sec_encrypted_backup_suffix"),
                                                                            12.0f);
                                                                    };
                                                                });
                                                            }
                                                            return;
                                                        }

                                                        // Success — force full state refresh so peers,
                                                        // balances, and addresses are fetched immediately.
                                                        if (worker_) {
                                                            worker_->post([this]() -> rpc::RPCWorker::MainCb {
                                                                return [this]() {
                                                                    wallet_security_workflow_.finishImport();

                                                                    // Force address + peer refresh
                                                                    invalidateAddressValidationCache();
                                                                    addresses_dirty_ = true;
                                                                    transactions_dirty_ = true;
                                                                    last_tx_block_height_ = -1;

                                                                    refreshWalletEncryptionState();
                                                                    refreshData();
                                                                    refreshPeerInfo();

                                                                    ui::Notifications::instance().success(
                                                                        TR("sec_wallet_decrypted_all_keys_imported"),
                                                                        8.0f);
                                                                    DEBUG_LOGF("[App] Wallet decrypted successfully\n");
                                                                };
                                                            });
                                                        }
                                                    };

                                                    async_tasks_.submit("decrypt-restart-import", restartAndImport);
                                                };
                                            });
                                        };
                                    });
                                };
                            });
                        };
                    });
                }
            }
            ImGui::EndDisabled();

            ImGui::SameLine();
            if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
                memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
                show_decrypt_dialog_ = false;
            }

        // ---- Phase 1: Working ----
        } else if (decryptState.phase == DecryptPhase::Working) {
            // Step checklist
            const char* stepLabels[] = {
                TR("decrypt_step_unlock"),
                TR("decrypt_step_export"),
                TR("decrypt_step_stop"),
                TR("decrypt_step_backup"),
                TR("decrypt_step_restart")
            };
            const int numSteps = 5;
            
            // Compute elapsed times
            auto now = std::chrono::steady_clock::now();
            auto stepElapsed = std::chrono::duration_cast<std::chrono::seconds>(
                now - decryptState.stepStarted).count();
            auto totalElapsed = std::chrono::duration_cast<std::chrono::seconds>(
                now - decryptState.overallStarted).count();

            ImGui::Spacing();
            for (int i = 0; i < numSteps; i++) {
                ImGui::PushFont(Type().iconMed());
                if (services::WalletSecurityWorkflow::stepIsComplete(decryptState.step,
                    services::WalletSecurityWorkflow::stepFromIndex(i))) {
                    // Completed
                    ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.4f, 1.0f), ICON_MD_CHECK_CIRCLE);
                } else if (i == services::WalletSecurityWorkflow::stepIndex(decryptState.step)) {
                    // In progress - animate
                    float alpha = 0.5f + 0.5f * sinf((float)ImGui::GetTime() * 4.0f);
                    ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, alpha), ICON_MD_PENDING);
                } else {
                    // Not started
                    ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 0.5f), ICON_MD_RADIO_BUTTON_UNCHECKED);
                }
                ImGui::PopFont();
                ImGui::SameLine();
                
                if (i == services::WalletSecurityWorkflow::stepIndex(decryptState.step)) {
                    // Show step label with elapsed time
                    int mins = (int)(stepElapsed / 60);
                    int secs = (int)(stepElapsed % 60);
                    if (mins > 0) {
                        ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f),
                            "%s... (%dm %02ds)", stepLabels[i], mins, secs);
                    } else {
                        ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.6f, 1.0f),
                            "%s... (%ds)", stepLabels[i], secs);
                    }
                } else if (services::WalletSecurityWorkflow::stepIsComplete(decryptState.step,
                    services::WalletSecurityWorkflow::stepFromIndex(i))) {
                    ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "%s", stepLabels[i]);
                } else {
                    ImGui::TextDisabled("%s", stepLabels[i]);
                }
            }
            
            ImGui::Spacing();
            ImGui::Separator();
            ImGui::Spacing();

            // Indeterminate progress bar
            {
                float barW = ImGui::GetContentRegionAvail().x;
                float barH = 6.0f * ui::Layout::dpiScale();
                ImVec2 p = ImGui::GetCursorScreenPos();
                ImDrawList* dl = ImGui::GetWindowDrawList();
                dl->AddRectFilled(p, ImVec2(p.x + barW, p.y + barH),
                                  IM_COL32(255, 255, 255, 25), 3.0f);
                float t = (float)ImGui::GetTime();
                float segW = barW * 0.3f;
                float x0 = p.x + (barW + segW) * (0.5f + 0.5f * sinf(t * 2.0f)) - segW;
                float x1 = x0 + segW;
                x0 = std::max(x0, p.x);
                x1 = std::min(x1, p.x + barW);
                if (x1 > x0) {
                    dl->AddRectFilled(ImVec2(x0, p.y), ImVec2(x1, p.y + barH),
                                      IM_COL32(255, 218, 0, 200), 3.0f);
                }
                ImGui::Dummy(ImVec2(barW, barH));
            }

            ImGui::Spacing();

            // Step-specific hints
            if (decryptState.step == DecryptStep::RestartDaemon) {
                ImGui::TextWrapped("%s", TR("decrypt_wait_restart"));
            } else {
                ImGui::TextWrapped("%s", TR("decrypt_wait_general"));
            }

            // Total elapsed
            {
                int tMins = (int)(totalElapsed / 60);
                int tSecs = (int)(totalElapsed % 60);
                ImGui::Spacing();
                ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
            }

        // ---- Phase 2: Success ----
        } else if (decryptState.phase == DecryptPhase::Success) {
            ImGui::PushFont(Type().iconLarge());
            ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), ICON_MD_CHECK_CIRCLE);
            ImGui::PopFont();
            ImGui::SameLine();
            ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.5f, 1.0f), "%s", TR("decrypt_success_title"));

            ImGui::Spacing();
            ImGui::TextWrapped("%s", TR("decrypt_success_desc"));

            ImGui::Spacing();
            if (ui::material::TactileButton(TR("close"), ImVec2(-1, 40))) {
                show_decrypt_dialog_ = false;
            }

        // ---- Phase 3: Error ----
        } else if (decryptState.phase == DecryptPhase::Error) {
            ImGui::PushFont(Type().iconLarge());
            ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), ICON_MD_ERROR);
            ImGui::PopFont();
            ImGui::SameLine();
            ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", TR("decrypt_error_title"));

            ImGui::Spacing();
            ImGui::TextWrapped("%s", decryptState.status.c_str());

            ImGui::Spacing();
            float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
            if (ui::material::TactileButton(TR("try_again"), ImVec2(btnW, 40))) {
                wallet_security_workflow_.reset();
            }
            ImGui::SameLine();
            if (ui::material::TactileButton(TR("close"), ImVec2(btnW, 40))) {
                show_decrypt_dialog_ = false;
            }
        }
        EndOverlayDialog();
    }

    // Wipe the passphrase buffer if the dialog was dismissed (X / Esc / outside-
    // click) without submitting. The submit and Cancel paths already memset it;
    // this covers the BlurFloat dismiss paths. (canClose is false during Working,
    // so an in-flight decrypt cannot be dismissed here.)
    if (!show_decrypt_dialog_) {
        memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
    }
}

// ===========================================================================
// PIN Setup / Change / Remove Dialogs (from Settings page)
// ===========================================================================

void App::renderPinDialogs() {
    using namespace ui::material;

    // ---- Set PIN dialog ----
    if (show_pin_setup_) {
        OverlayDialogSpec ov;
        ov.title = TR("settings_set_pin"); ov.p_open = &show_pin_setup_;
        ov.style = OverlayStyle::BlurFloat;
        ov.cardWidth = 420.0f; ov.idSuffix = "pinsetup";
        if (BeginOverlayDialog(ov)) {

            ImGui::TextWrapped("%s", TR("pin_setup_desc"));
            ImGui::Spacing();

            ImGui::TextUnformatted(TR("pin_wallet_passphrase"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##pin_passphrase", pin_passphrase_buf_, sizeof(pin_passphrase_buf_),
                             ImGuiInputTextFlags_Password);
            ImGui::PopItemWidth();

            ImGui::TextUnformatted(TR("pin_new_label"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##pin_new", pin_buf_, sizeof(pin_buf_),
                             ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
            ImGui::PopItemWidth();

            ImGui::TextUnformatted(TR("wiz_pin_confirm"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##pin_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
                             ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
            ImGui::PopItemWidth();

            if (!pin_status_.empty()) {
                ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
            }

            ImGui::Spacing();
            std::string pinStr(pin_buf_);
            bool valid = strlen(pin_passphrase_buf_) > 0 &&
                         util::SecureVault::isValidPin(pinStr) &&
                         strcmp(pin_buf_, pin_confirm_buf_) == 0;

            // Two-button footer (primary + Cancel) to match the encrypt/decrypt siblings.
            float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
            ImGui::BeginDisabled(!valid || pin_in_progress_);
            if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
                pin_in_progress_ = true;
                pin_status_ = TR("sec_verifying_passphrase");

                // Verify passphrase + store vault on worker thread to avoid
                // blocking the UI with Argon2id key derivation.
                std::string passphrase(pin_passphrase_buf_);
                std::string pin(pin_buf_);
                memset(pin_passphrase_buf_, 0, sizeof(pin_passphrase_buf_));
                memset(pin_buf_, 0, sizeof(pin_buf_));
                memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));

                if (rpc_ && rpc_->isConnected() && worker_) {
                    worker_->post([this, passphrase, pin]() mutable -> rpc::RPCWorker::MainCb {
                        // Verify passphrase via RPC (worker thread)
                        try {
                            rpc::RPCClient::TraceScope trace("Security / PIN setup");
                            rpc_->call("walletpassphrase", {passphrase, 5});
                        } catch (const std::exception& e) {
                            if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
                            if (!pin.empty())        util::SecureVault::secureZero(&pin[0], pin.size());
                            return [this]() {
                                pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
                                pin_in_progress_ = false;
                            };
                        }

                        // Passphrase correct — store in vault (Argon2id, worker thread)
                        bool storeOk = vault_ && vault_->store(pin, passphrase);
                        // Captured passphrase + PIN are no longer needed — scrub the worker-queue copies. (M-01)
                        if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
                        if (!pin.empty())        util::SecureVault::secureZero(&pin[0], pin.size());

                        // Lock wallet back
                        try {
                            rpc::RPCClient::TraceScope trace("Security / PIN setup");
                            rpc_->call("walletlock");
                        } catch (...) {}

                        return [this, storeOk]() {
                            if (storeOk) {
                                settings_->setPinEnabled(true);
                                settings_->save();
                                pin_status_.clear();
                                pin_in_progress_ = false;
                                show_pin_setup_ = false;
                                ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
                            } else {
                                pin_status_ = TR("sec_failed_to_create_vault");
                                pin_in_progress_ = false;
                            }
                        };
                    });
                } else {
                    pin_status_ = TR("sec_not_connected_to_daemon_pin");
                    pin_in_progress_ = false;
                }
            }
            ImGui::EndDisabled();

            ImGui::SameLine();
            // Cancel does what Esc/close does — dismiss without applying. Buffers are wiped by
            // the !show_pin_setup_ cleanup block below.
            if (ui::material::TactileButton(TR("cancel"), ImVec2(btnW, 40))) {
                show_pin_setup_ = false;
            }
            EndOverlayDialog();
        }
        // Wipe the passphrase/PIN buffers if the dialog was dismissed (X / Esc /
        // outside-click) without submitting. The submit path already memsets them.
        if (!show_pin_setup_) {
            memset(pin_passphrase_buf_, 0, sizeof(pin_passphrase_buf_));
            memset(pin_buf_, 0, sizeof(pin_buf_));
            memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
        }
    }

    // ---- Change PIN dialog ----
    if (show_pin_change_) {
        OverlayDialogSpec ov;
        ov.title = TR("settings_change_pin"); ov.p_open = &show_pin_change_;
        ov.style = OverlayStyle::BlurFloat;
        ov.cardWidth = 420.0f; ov.idSuffix = "pinchange";
        if (BeginOverlayDialog(ov)) {

            ImGui::TextWrapped("%s", TR("pin_change_desc"));
            ImGui::Spacing();

            ImGui::TextUnformatted(TR("pin_current_label"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##pin_old", pin_old_buf_, sizeof(pin_old_buf_),
                             ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
            ImGui::PopItemWidth();

            ImGui::TextUnformatted(TR("pin_new_label"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##pin_change_new", pin_buf_, sizeof(pin_buf_),
                             ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
            ImGui::PopItemWidth();

            ImGui::TextUnformatted(TR("pin_confirm_new_label"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##pin_change_confirm", pin_confirm_buf_, sizeof(pin_confirm_buf_),
                             ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
            ImGui::PopItemWidth();

            if (!pin_status_.empty()) {
                ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
            }

            ImGui::Spacing();
            std::string newPin(pin_buf_);
            bool valid = strlen(pin_old_buf_) >= 4 &&
                         util::SecureVault::isValidPin(newPin) &&
                         strcmp(pin_buf_, pin_confirm_buf_) == 0;

            ImGui::BeginDisabled(!valid || pin_in_progress_);
            if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
                pin_in_progress_ = true;
                pin_status_ = TR("sec_changing_pin");
                std::string oldPin(pin_old_buf_);
                std::string newPinCopy = newPin;
                memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
                memset(pin_buf_, 0, sizeof(pin_buf_));
                memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));

                if (worker_ && vault_) {
                    worker_->post([this, oldPin, newPinCopy]() -> rpc::RPCWorker::MainCb {
                        // Argon2id runs here (worker thread)
                        bool ok = vault_->changePin(oldPin, newPinCopy);
                        return [this, ok]() {
                            if (ok) {
                                pin_status_.clear();
                                pin_in_progress_ = false;
                                show_pin_change_ = false;
                                ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
                            } else {
                                pin_status_ = TR("sec_incorrect_current_pin");
                                pin_in_progress_ = false;
                            }
                        };
                    });
                } else {
                    pin_status_ = TR("sec_internal_error_change_pin");
                    pin_in_progress_ = false;
                }
            }
            ImGui::EndDisabled();
            EndOverlayDialog();
        }
        // Wipe the PIN buffers if the dialog was dismissed without submitting.
        if (!show_pin_change_) {
            memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
            memset(pin_buf_, 0, sizeof(pin_buf_));
            memset(pin_confirm_buf_, 0, sizeof(pin_confirm_buf_));
        }
    }

    // ---- Remove PIN dialog ----
    if (show_pin_remove_) {
        OverlayDialogSpec ov;
        ov.title = TR("settings_remove_pin"); ov.p_open = &show_pin_remove_;
        ov.style = OverlayStyle::BlurFloat;
        ov.cardWidth = 400.0f; ov.idSuffix = "pinremove";
        if (BeginOverlayDialog(ov)) {

            ImGui::TextWrapped("%s", TR("pin_remove_desc"));
            ImGui::Spacing();

            ImGui::TextUnformatted(TR("pin_current_label"));
            ImGui::PushItemWidth(-1);
            ImGui::InputText("##pin_remove", pin_old_buf_, sizeof(pin_old_buf_),
                             ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsDecimal);
            ImGui::PopItemWidth();

            if (!pin_status_.empty()) {
                ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "%s", pin_status_.c_str());
            }

            ImGui::Spacing();
            bool valid = strlen(pin_old_buf_) >= 4;
            ImGui::BeginDisabled(!valid || pin_in_progress_);
            if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
                pin_in_progress_ = true;
                pin_status_ = TR("sec_verifying_pin");
                std::string oldPin(pin_old_buf_);
                memset(pin_old_buf_, 0, sizeof(pin_old_buf_));

                if (worker_ && vault_) {
                    worker_->post([this, oldPin]() -> rpc::RPCWorker::MainCb {
                        // Argon2id runs here (worker thread)
                        std::string passphrase;
                        bool ok = vault_->retrieve(oldPin, passphrase);
                        if (ok) {
                            util::SecureVault::secureZero(&passphrase[0], passphrase.size());
                        }
                        return [this, ok]() {
                            if (ok) {
                                vault_->removeVault();
                                settings_->setPinEnabled(false);
                                settings_->save();
                                pin_status_.clear();
                                pin_in_progress_ = false;
                                show_pin_remove_ = false;
                                ui::Notifications::instance().info(TR("sec_pin_removed"));
                            } else {
                                pin_status_ = TR("sec_incorrect_pin_remove");
                                pin_in_progress_ = false;
                            }
                        };
                    });
                } else {
                    pin_status_ = TR("sec_internal_error_remove_pin");
                    pin_in_progress_ = false;
                }
            }
            ImGui::EndDisabled();
            EndOverlayDialog();
        }
        // Wipe the PIN buffer if the dialog was dismissed without submitting.
        if (!show_pin_remove_) {
            memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
        }
    }
}

} // namespace dragonx
