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

#include "app.h"
#include "config/settings.h"
#include "config/version.h"
#include "rpc/rpc_worker.h"
#include "ui/schema/ui_schema.h"
#include "ui/effects/low_spec.h"
#include "ui/notifications.h"
#include "ui/windows/contacts_tab.h"
#include "ui/theme.h"
#include "ui/material/color_theme.h"
#include "ui/material/typography.h"
#include "ui/material/draw_helpers.h"
#include "ui/layout.h"
#include "util/single_instance.h"
#include "util/payment_uri.h"
#include "util/perf_log.h"

#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_impl_sdl3.h"
#include "ui/effects/imgui_acrylic.h"
#include "ui/effects/theme_effects.h"
#include "util/texture_loader.h"
#include "util/platform.h"
#include "daemon/seed_wallet_creator.h"
#include "resources/embedded_resources.h"
#include <sodium.h>

// Backend-specific headers
#ifdef DRAGONX_USE_DX11
  #include "imgui_impl_dx11.h"
  #include "platform/dx11_context.h"
#else
  #include "imgui_impl_opengl3.h"
  #include <SDL3/SDL_opengl.h>
  #ifdef DRAGONX_HAS_GLAD
    #include <glad/gl.h>
  #endif
#endif

#include <SDL3/SDL.h>
#include <map>

#ifdef _WIN32
#include "platform/windows_backdrop.h"
#include <windows.h>
#include <dwmapi.h>
#include <shlobj.h>
#include <propkey.h>
#include <propsys.h>
// SetCurrentProcessExplicitAppUserModelID lives behind NTDDI_WIN7 in
// MinGW's shobjidl.h.  Rather than forcing the version macro globally,
// declare just the one function we need (available on Windows 7+).
extern "C" HRESULT __stdcall SetCurrentProcessExplicitAppUserModelID(PCWSTR AppID);
// SHGetPropertyStoreForWindow is also behind NTDDI_WIN7 in MinGW headers.
extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid, void** ppv);
// Not defined in older MinGW SDK headers
#ifndef WS_EX_NOREDIRECTIONBITMAP
#define WS_EX_NOREDIRECTIONBITMAP 0x00200000L
#endif
// Needed by the borderless WndProc for WM_NCHITTEST
#ifndef GET_X_LPARAM
#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
#endif
#ifndef GET_Y_LPARAM
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))
#endif
#endif

#include <cstdio>
#include <cstdarg>
#include <cstdlib>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include <chrono>
#include <miniz.h>
#include "util/logger.h"

// Read the just-rendered framebuffer and write it to a PNG (used by the debug screenshot sweep).
// GL: read FBO 0 (RGBA, bottom-up -> miniz flip). DX11: copy the backbuffer to a staging texture,
// map it (BGRA, top-down -> channel-swap, no flip). Alpha forced opaque (the window is transparent).
namespace {
inline void writePng(const std::string& path, const std::vector<uint8_t>& rgba, int w, int h, bool flip) {
    size_t len = 0;
    void* png = tdefl_write_image_to_png_file_in_memory_ex(rgba.data(), w, h, 4, &len, 6, flip ? MZ_TRUE : MZ_FALSE);
    if (!png) return;
    std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
    std::ofstream out(path, std::ios::binary | std::ios::trunc);
    if (out) out.write(reinterpret_cast<const char*>(png), static_cast<std::streamsize>(len));
    mz_free(png);
}
#ifdef DRAGONX_USE_DX11
inline void captureFrameToPng(dragonx::platform::DX11Context& dx, const std::string& path) {
    IDXGISwapChain1* sc = dx.swapChain(); ID3D11Device* dev = dx.device(); ID3D11DeviceContext* ctx = dx.deviceContext();
    if (!sc || !dev || !ctx) return;
    ID3D11Texture2D* back = nullptr;
    if (FAILED(sc->GetBuffer(0, IID_PPV_ARGS(&back))) || !back) return;
    D3D11_TEXTURE2D_DESC bd; back->GetDesc(&bd);
    D3D11_TEXTURE2D_DESC sd = bd; sd.Usage = D3D11_USAGE_STAGING; sd.BindFlags = 0;
    sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; sd.MiscFlags = 0;
    ID3D11Texture2D* staging = nullptr;
    if (SUCCEEDED(dev->CreateTexture2D(&sd, nullptr, &staging)) && staging) {
        ctx->CopyResource(staging, back);
        D3D11_MAPPED_SUBRESOURCE m;
        if (SUCCEEDED(ctx->Map(staging, 0, D3D11_MAP_READ, 0, &m))) {
            int w = (int)bd.Width, h = (int)bd.Height;
            std::vector<uint8_t> px((size_t)w * h * 4);
            for (int y = 0; y < h; ++y) {
                const uint8_t* s = (const uint8_t*)m.pData + (size_t)y * m.RowPitch;
                uint8_t* d = px.data() + (size_t)y * w * 4;
                for (int x = 0; x < w; ++x) { d[x*4+0]=s[x*4+2]; d[x*4+1]=s[x*4+1]; d[x*4+2]=s[x*4+0]; d[x*4+3]=255; }
            }
            ctx->Unmap(staging, 0);
            writePng(path, px, w, h, /*flip*/false);   // DX11 textures are top-down
        }
        staging->Release();
    }
    back->Release();
}
#else
inline void captureFrameToPng(const std::string& path, int w, int h) {
    if (w <= 0 || h <= 0) return;
    std::vector<uint8_t> px((size_t)w * h * 4);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    glReadBuffer(GL_BACK);
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
    glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
    for (size_t i = 3; i < px.size(); i += 4) px[i] = 255;  // opaque
    writePng(path, px, w, h, /*flip*/true);    // GL default framebuffer is bottom-up
}
#endif
} // namespace

#ifdef _WIN32
// Windows crash handler — writes a minidump and log entry before exit
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
{
    // Write crash info to the debug log and a dedicated crash log
    const char* crashMsg = "\n=== CRASH DETECTED ===";
    DEBUG_LOGF("%s", crashMsg);
    DEBUG_LOGF("\nException code: 0x%08lX", ep->ExceptionRecord->ExceptionCode);
    DEBUG_LOGF("\nException address: %p", ep->ExceptionRecord->ExceptionAddress);
    if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION &&
        ep->ExceptionRecord->NumberParameters >= 2) {
        DEBUG_LOGF("\nAccess type: %s, target address: %p",
                ep->ExceptionRecord->ExceptionInformation[0] ? "WRITE" : "READ",
                (void*)ep->ExceptionRecord->ExceptionInformation[1]);
    }
    DEBUG_LOGF("\n=== END CRASH INFO ===\n");
    fflush(stdout);
    fflush(stderr);

    // Also write to a separate crash log file
    try {
        std::string crashPath = (std::filesystem::path(
            dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-crash.log").string();
        FILE* f = fopen(crashPath.c_str(), "a");
        if (f) {
            fprintf(f, "\n=== CRASH at PID %lu ===", (unsigned long)GetCurrentProcessId());
            fprintf(f, "\nException code: 0x%08lX", ep->ExceptionRecord->ExceptionCode);
            fprintf(f, "\nException address: %p", ep->ExceptionRecord->ExceptionAddress);
            if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION &&
                ep->ExceptionRecord->NumberParameters >= 2) {
                fprintf(f, "\nAccess type: %s, target address: %p",
                        ep->ExceptionRecord->ExceptionInformation[0] ? "WRITE" : "READ",
                        (void*)ep->ExceptionRecord->ExceptionInformation[1]);
            }
            fprintf(f, "\n=== END CRASH INFO ===\n");
            fclose(f);
        }
    } catch (...) {}
    return EXCEPTION_EXECUTE_HANDLER;
}

// Set the window's shell property store so Task Manager, taskbar, and shell
// always show "ObsidianDragon" regardless of any cached VERSIONINFO metadata.
static void SetWindowIdentity(HWND hwnd)
{
    IPropertyStore* pps = nullptr;
    HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_IPropertyStore, (void**)&pps);
    if (SUCCEEDED(hr) && pps) {
        // Set AppUserModel.ID on the window (overrides process-level ID for this window)
        PROPVARIANT pvId;
        PropVariantInit(&pvId);
        pvId.vt = VT_LPWSTR;
        pvId.pwszVal = const_cast<LPWSTR>(L"DragonX.ObsidianDragon.Wallet");
        pps->SetValue(PKEY_AppUserModel_ID, pvId);
        // Don't PropVariantClear — the string is a static literal

        // Set RelaunchDisplayNameResource so the shell shows our name
        PROPVARIANT pvName;
        PropVariantInit(&pvName);
        pvName.vt = VT_LPWSTR;
        pvName.pwszVal = const_cast<LPWSTR>(L"ObsidianDragon");
        pps->SetValue(PKEY_AppUserModel_RelaunchDisplayNameResource, pvName);

        // Set RelaunchCommand (required alongside RelaunchDisplayNameResource)
        PROPVARIANT pvCmd;
        PropVariantInit(&pvCmd);
        pvCmd.vt = VT_LPWSTR;
        wchar_t exePath[MAX_PATH] = {};
        GetModuleFileNameW(nullptr, exePath, MAX_PATH);
        pvCmd.pwszVal = exePath;
        pps->SetValue(PKEY_AppUserModel_RelaunchCommand, pvCmd);

        pps->Commit();
        pps->Release();
        DEBUG_LOGF("Window property store: identity set to ObsidianDragon\n");
    } else {
        DEBUG_LOGF("SHGetPropertyStoreForWindow failed: 0x%08lx\n", (unsigned long)hr);
    }
}
#endif

// ---------------------------------------------------------------
// Borderless window support (DX11 / Windows only)
// ---------------------------------------------------------------
#ifdef DRAGONX_USE_DX11

static WNDPROC g_sdlWndProc = nullptr;

// Caption-zone state: updated every frame by renderBorderlessControls()
// so that WM_NCHITTEST can return HTCAPTION for window dragging.
static float  g_captionHeight    = 20.0f;  // height of the title/menu bar (px)
static float  g_captionBtnStartX = 0.0f;   // left edge of the min/max/close buttons
static bool   g_captionImGuiHover = false;  // true when ImGui has a hovered/active item
static float  g_borderlessDpi    = 1.0f;   // DPI scale, updated from SDL each frame

// Custom WndProc that removes the native title bar by extending the
// client area to cover the entire window (WM_NCCALCSIZE) and provides
// resize borders + caption drag zone (WM_NCHITTEST).
static void winlog(const char* fmt, ...);   // [WINLOG] tracer (defined below)

static LRESULT CALLBACK BorderlessWndProc(HWND hwnd, UINT msg,
                                          WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
    case WM_NCCALCSIZE:
        if (wParam == TRUE) {
            // Returning 0 makes the entire window rect into client area,
            // which effectively removes the title bar.  When maximized,
            // constrain to the monitor work area so content isn't clipped
            // behind the taskbar.
            WINDOWPLACEMENT wp = {};
            wp.length = sizeof(wp);
            GetWindowPlacement(hwnd, &wp);
            if (wp.showCmd == SW_MAXIMIZE) {
                auto* p = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
                HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
                MONITORINFO mi = {};
                mi.cbSize = sizeof(mi);
                GetMonitorInfo(mon, &mi);
                p->rgrc[0] = mi.rcWork;
            }
            return 0;
        }
        break;

    case WM_NCHITTEST: {
        POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
        ScreenToClient(hwnd, &pt);
        RECT rc;
        GetClientRect(hwnd, &rc);

        // Resize borders (not when maximized).
        WINDOWPLACEMENT wp = {};
        wp.length = sizeof(wp);
        GetWindowPlacement(hwnd, &wp);
        if (wp.showCmd != SW_MAXIMIZE) {
            const int bdr = (int)(5.0f * g_borderlessDpi);
            bool t = pt.y <= bdr, b = pt.y >= rc.bottom - bdr;
            bool l = pt.x <= bdr, r = pt.x >= rc.right  - bdr;
            if (t && l) return HTTOPLEFT;
            if (t && r) return HTTOPRIGHT;
            if (b && l) return HTBOTTOMLEFT;
            if (b && r) return HTBOTTOMRIGHT;
            if (t) return HTTOP;
            if (b) return HTBOTTOM;
            if (l) return HTLEFT;
            if (r) return HTRIGHT;
        }

        // Caption zone: the top bar where the menu + window buttons live.
        // Return HTCAPTION so Windows handles drag/move natively — but
        // only when the cursor is NOT over the window-control buttons and
        // NOT over an ImGui interactive item (e.g. a menu-bar entry).
        if (pt.y < (int)g_captionHeight) {
            // Window-control buttons (min/max/close) on the right
            if (pt.x >= (int)g_captionBtnStartX) return HTCLIENT;
            // ImGui menu item hovered — let ImGui handle the click
            if (g_captionImGuiHover) return HTCLIENT;
            return HTCAPTION;
        }

        return HTCLIENT;
    }

    case WM_SETCURSOR:
        // Prevent DefWindowProc from resetting the cursor to the window-
        // class cursor (IDC_ARROW) on every mouse move.  Our render loop
        // calls SDL_SetCursor() each frame based on ImGui's cursor state;
        // without this, WM_SETCURSOR fires between frames and reverts the
        // hand/resize cursor back to the arrow.
        if (LOWORD(lParam) == HTCLIENT)
            return TRUE;   // "handled" — keep whatever cursor we last set
        break;

    case WM_GETMINMAXINFO: {
        auto* mmi = reinterpret_cast<MINMAXINFO*>(lParam);
        // Scale min window size by DPI so it matches the SDL-side minimum.
        mmi->ptMinTrackSize.x = (LONG)(1024 * g_borderlessDpi);
        mmi->ptMinTrackSize.y = (LONG)(720  * g_borderlessDpi);
        static float s_lastBd = -1.0f;   // [WINLOG] log only when g_borderlessDpi changes (this fires a lot)
        if (g_borderlessDpi != s_lastBd) {
            s_lastBd = g_borderlessDpi;
            winlog("WM_GETMINMAXINFO g_borderlessDpi=%.3f -> minTrack=%ldx%ld",
                   g_borderlessDpi, (long)mmi->ptMinTrackSize.x, (long)mmi->ptMinTrackSize.y);
        }
        return 0;
    }
    }
    return CallWindowProcW(g_sdlWndProc, hwnd, msg, wParam, lParam);
}

// Draw custom window-control buttons (minimize / maximize-restore / close)
// on the foreground draw list, and handle title-bar dragging.
static void renderBorderlessControls(SDL_Window* window, HWND /*hwnd*/,
                                     dragonx::App& app)
{
    ImGuiIO& io    = ImGui::GetIO();
    // Use the main viewport's foreground draw list so buttons always render
    // on the primary window (not on a secondary viewport that has focus).
    ImGuiViewport* vp = ImGui::GetMainViewport();
    ImDrawList* fg = ImGui::GetForegroundDrawList(vp);
    float captionH = ImGui::GetFrameHeight();  // same height as menu bar
    float displayW = io.DisplaySize.x;

    // DPI scale factor for borderless controls
    float dp = dragonx::ui::Layout::dpiScale();

    // With multi-viewport, draw-list coordinates and io.MousePos are in
    // OS screen-space.  Offset all positions by the main viewport origin.
    const float ox = vp->Pos.x;
    const float oy = vp->Pos.y;

    const float btnW = 46.0f * dp;
    const float btnH = captionH;
    float closeX = ox + displayW - btnW;
    float maxX   = closeX  - btnW;
    float minX   = maxX    - btnW;
    ImVec2 mp    = io.MousePos;
    bool   click = ImGui::IsMouseClicked(ImGuiMouseButton_Left);

    const auto& themeS = dragonx::ui::schema::UI();
    ImU32 wcHover = themeS.resolveColor("var(--window-control-hover)", IM_COL32(255, 255, 255, 30));
    ImU32 wcIcon  = themeS.resolveColor("var(--window-control)", IM_COL32(255, 255, 255, 200));
    ImU32 wcClose = themeS.resolveColor("var(--window-close-hover)", IM_COL32(232, 17, 35, 200));

    // Icon glyph half-size and line thickness, scaled by DPI
    const float iconHalf = 5.0f * dp;
    const float lineThk  = 1.0f * dp;

    // --- Minimize button ---
    {
        ImVec2 p0(minX, oy), p1(minX + btnW, oy + btnH);
        bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y);
        if (hov) fg->AddRectFilled(p0, p1, wcHover);
        float cy = oy + btnH * 0.5f, lx = minX + btnW * 0.5f - iconHalf;
        fg->AddLine(ImVec2(lx, cy), ImVec2(lx + iconHalf * 2.0f, cy),
                    wcIcon, lineThk);
        if (hov && click) SDL_MinimizeWindow(window);
    }

    // --- Maximize / Restore button ---
    {
        ImVec2 p0(maxX, oy), p1(maxX + btnW, oy + btnH);
        bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y);
        if (hov) fg->AddRectFilled(p0, p1, wcHover);
        bool maximized = (SDL_GetWindowFlags(window) & SDL_WINDOW_MAXIMIZED) != 0;
        float cx = maxX + btnW * 0.5f, cy = oy + btnH * 0.5f;
        if (maximized) {
            float s1 = 4.0f * dp, s2 = 3.0f * dp;
            float s3 = 2.0f * dp, s4 = 5.0f * dp;
            fg->AddRect(ImVec2(cx - s1, cy - s2), ImVec2(cx + s2, cy + s1),
                        wcIcon, 0, 0, lineThk);
            fg->AddRect(ImVec2(cx - s3, cy - s4), ImVec2(cx + s4, cy + s3),
                        wcIcon, 0, 0, lineThk);
        } else {
            fg->AddRect(ImVec2(cx - iconHalf, cy - iconHalf + dp),
                        ImVec2(cx + iconHalf, cy + iconHalf - dp),
                        wcIcon, 0, 0, lineThk);
        }
        if (hov && click) {
            if (maximized) SDL_RestoreWindow(window);
            else           SDL_MaximizeWindow(window);
        }
    }

    // --- Close button ---
    {
        ImVec2 p0(closeX, oy), p1(closeX + btnW, oy + btnH);
        bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y);
        if (hov) fg->AddRectFilled(p0, p1, wcClose);
        float cx = closeX + btnW * 0.5f, cy = oy + btnH * 0.5f;
        fg->AddLine(ImVec2(cx - iconHalf, cy - iconHalf),
                    ImVec2(cx + iconHalf, cy + iconHalf),
                    wcIcon, lineThk);
        fg->AddLine(ImVec2(cx + iconHalf, cy - iconHalf),
                    ImVec2(cx - iconHalf, cy + iconHalf),
                    wcIcon, lineThk);
        if (hov && click) app.beginShutdown();
    }

    // --- Update caption-zone state for WM_NCHITTEST ---
    // WM_NCHITTEST uses client-local coordinates, so subtract viewport origin.
    g_captionHeight     = captionH;
    g_captionBtnStartX  = minX - ox;
    g_captionImGuiHover = ImGui::IsAnyItemHovered() || ImGui::IsAnyItemActive();
    g_borderlessDpi     = dp;
}

#endif // DRAGONX_USE_DX11

// Forward declarations
static bool InitSDL();
#ifdef DRAGONX_USE_DX11
static bool InitImGui(SDL_Window* window, dragonx::platform::DX11Context& dx);
static void Shutdown(SDL_Window* window, dragonx::platform::DX11Context& dx);
#else
static bool InitImGui(SDL_Window* window, SDL_GLContext gl_context);
static void Shutdown(SDL_Window* window, SDL_GLContext gl_context);
#endif

// Global single instance lock. Keyed PER VARIANT so the full node and Lite can run side by side —
// their config dirs are already separate (DRAGONX_APP_NAME), so only this lock kept them apart.
// NB: DRAGONX_LITE_BUILD is ALWAYS defined (0 for the full node, 1 for Lite) via $<BOOL:...>, so this
// must be #if (value), not #ifdef (existence) — #ifdef is true for both and made the full node grab
// the Lite lock.
#if DRAGONX_LITE_BUILD
static dragonx::util::SingleInstance g_single_instance("obsidiandragonlite");
#else
static dragonx::util::SingleInstance g_single_instance("obsidiandragon");
#endif

// Check for payment URI in command line args
static std::string findPaymentURI(int argc, char* argv[])
{
    for (int i = 1; i < argc; i++) {
        if (dragonx::util::isPaymentURI(argv[i])) {
            return argv[i];
        }
    }
    return "";
}

// ---------------------------------------------------------------
// Shared render-loop helpers (used by both the main loop and the
// resize event-watcher).  Extracted verbatim to de-duplicate the
// per-frame code — same calls, same order, same #ifdef branches.
// ---------------------------------------------------------------

// Draw the window backdrop (base gradient / texture tint / acrylic
// capture callback / WindowBg alpha).  The main loop passes lowSpec =
// isLowSpecMode() so low-spec skips texture sampling and the capture
// callback; the resize watcher passes lowSpec = false to reproduce its
// current always-sample-and-capture behavior.
static void drawWindowBackdrop(dragonx::App& app, ImDrawList* bgDL, ImVec2 p0, ImVec2 p1,
                               bool backdrop_active, bool lowSpec)
{
    // Read the current gradient texture from the app each frame
    // so hot-reloaded / skin-changed images are picked up immediately.
    ImTextureID curGradTex = app.getGradientTexture();

    // Window opacity: scale only background layer alpha so the
    // desktop shows through while UI stays fully opaque.
    float winOpacity = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;

    // Scale a single color's alpha channel by window opacity
    auto scaleAlpha = [&](ImU32 col) -> ImU32 {
        uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
        a = (uint8_t)(a * winOpacity);
        return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
    };

    if (curGradTex != 0) {
        // Base color gradient underneath the texture — read from ui.toml
        const auto& S2 = dragonx::ui::schema::UI();
        ImU32 baseTop    = S2.resolveColor(S2.drawElement("backdrop", "base-color-top").color,    IM_COL32(18,28,65,200));
        ImU32 baseBottom = S2.resolveColor(S2.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));

        if (lowSpec) {
            // Low-spec: skip texture sampling — just draw the base gradient.
            bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
                                          scaleAlpha(baseBottom), scaleAlpha(baseBottom));
        } else {
            int tintAlpha    = (int)S2.drawElement("backdrop", "texture-tint-alpha").size;
            if (tintAlpha <= 0) tintAlpha = 140;

            // Scale background gradient + texture tint alpha by
            // window opacity — only affects the backdrop, not UI.
            bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
                                          scaleAlpha(baseBottom), scaleAlpha(baseBottom));
            int scaledTintAlpha = (int)(tintAlpha * winOpacity);
            ImU32 tintCol = IM_COL32(255, 255, 255, scaledTintAlpha);
            bgDL->AddImage(curGradTex, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol);
        }
    } else if (backdrop_active) {
        // Programmatic gradient tint (fallback when texture unavailable)
        const auto& S = dragonx::ui::schema::UI();
        auto bde = [&](const char* key, float fb) {
            float v = S.drawElement("backdrop", key).size;
            return v >= 0 ? v : fb;
        };
        ImU32 colTop    = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12),
                                   (int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOpacity));
        ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8),
                                   (int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOpacity));
        bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
    }

    // Insert acrylic capture callback BEFORE the noise overlay
    // so the noise grain is not captured and blurred into the
    // glass cards (the noise should only be a visual overlay).
    // Skip in low-spec mode — acrylic is disabled so the FBO
    // capture/blur callback is unnecessary GPU work.
    if (!lowSpec) {
        auto captureCb = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
        if (captureCb) {
            bgDL->AddCallback(captureCb, nullptr);
            bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
        }
    }

    // WindowBg alpha stays at its base theme value — NOT scaled
    // by window opacity.  Only the background gradient/texture
    // layers fade so UI elements remain fully readable.
    if (backdrop_active || curGradTex != 0) {
        const auto& S3 = dragonx::ui::schema::UI();
        float baseBgAlpha = S3.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
        ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha;
    }
}

// ── Window-dimension diagnostic tracer ──────────────────────────────────────────────────────
// Always-on (writes to stderr → dragonx-debug.log on Windows) so it's visible in release builds,
// unlike DRAGONX_DEBUG-gated DEBUG_LOGF. Grep the log for "[WINLOG]" to trace how the window size
// changes when dragging between monitors of different DPI. Low volume — only fires on window
// resize / move / DPI-scale events. TODO: remove once the multi-monitor sizing bug is resolved.
static void winlog(const char* fmt, ...)
{
    char buf[1024];
    va_list a; va_start(a, fmt); vsnprintf(buf, sizeof(buf), fmt, a); va_end(a);
    fprintf(stderr, "[WINLOG] %s\n", buf);
    fflush(stderr);
}
// Dump the full current window state with a short tag.
static void winlogState(SDL_Window* w, const char* tag)
{
    int sw = 0, sh = 0, pw = 0, ph = 0, mw = 0, mh = 0;
    SDL_GetWindowSize(w, &sw, &sh);
    SDL_GetWindowSizeInPixels(w, &pw, &ph);
    SDL_GetWindowMinimumSize(w, &mw, &mh);
    float ds = SDL_GetWindowDisplayScale(w);
    float st = dragonx::ui::material::Typography::instance().getDpiScale();
    SDL_DisplayID did = SDL_GetDisplayForWindow(w);
    winlog("%-20s size=%dx%d px=%dx%d min=%dx%d dispScale=%.3f storedScale=%.3f display=%u",
           tag, sw, sh, pw, ph, mw, mh, ds, st, (unsigned)did);
#ifdef _WIN32
    HWND hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(w),
                                             SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
    if (hwnd) {
        RECT wr = {}, cr = {};
        GetWindowRect(hwnd, &wr);
        GetClientRect(hwnd, &cr);
        winlog("%-20s   native win=%ldx%ld client=%ldx%ld pos=%ld,%ld",
               tag, (long)(wr.right - wr.left), (long)(wr.bottom - wr.top),
               (long)(cr.right - cr.left), (long)(cr.bottom - cr.top), (long)wr.left, (long)wr.top);
    }
#endif
}

// Resize the window to an EXACT client size. On the DX11 borderless native HWND, SDL_SetWindowSize
// inflates the result by a phantom caption+border frame: SDL runs AdjustWindowRectEx to turn the
// requested client size into a window rect (adding the WS_CAPTION/WS_THICKFRAME frame), but our
// WM_NCCALCSIZE handler folds that entire window rect back into the client area — so the client ends
// up ~frame bigger than asked (e.g. a 720 min becomes 759, unreachable). Resize the HWND directly:
// for a borderless window the window rect equals the client, so SetWindowPos gives the exact size.
static void resizeWindowExact(SDL_Window* window, int w, int h)
{
#if defined(_WIN32) && defined(DRAGONX_USE_DX11)
    HWND hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(window),
                                             SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
    if (hwnd) {
        SetWindowPos(hwnd, nullptr, 0, 0, w, h, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
        return;
    }
#endif
    SDL_SetWindowSize(window, w, h);
}

// Handle SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: save the old-scale size,
// compute + clamp the new physical size, arm the deferred resize retry,
// and rebuild fonts / reset ImGui style at the new DPI scale.  newScale
// is pre-normalized by the caller (<=0 -> 1.0f, and macOS -> 1.0f).
static void handleDisplayScaleChange(SDL_Window* window, float newScale,
                                     std::map<int,std::pair<int,int>>& savedSizeForScale,
                                     int& lastKnownW, int& lastKnownH,
                                     int& dpiTargetW, int& dpiTargetH,
                                     int& dpiResizeRetries, bool& dpiResizePending,
                                     int& dpiSettleFrames)
{
    auto& typo = dragonx::ui::material::Typography::instance();
    float oldScale = typo.getDpiScale();
    if (std::abs(newScale - oldScale) > 0.01f) {
        float ratio = newScale / oldScale;
        DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n",
               oldScale, newScale, ratio);
        winlog("======== DPI CHANGE  storedScale %.3f -> newScale %.3f (ratio %.3f) ========",
               oldScale, newScale, ratio);
        winlogState(window, "  entry");
        ImGuiIO& io = ImGui::GetIO();

#ifdef DRAGONX_USE_DX11
        g_borderlessDpi = newScale;
#endif

        // Save the last known size (before auto-resize) under
        // the OLD scale.  We use lastKnownW/H instead of
        // SDL_GetWindowSize() because by now WM_DPICHANGED
        // may have already auto-resized the window.
        {
            int oldPct = (int)lroundf(oldScale * 100.0f);
            savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
            DEBUG_LOGF("  Saved %dx%d for scale %d%%\n",
                   lastKnownW, lastKnownH, oldPct);
            winlog("  saved lastKnown %dx%d under old scale %d%%", lastKnownW, lastKnownH, oldPct);
        }

        // Compute new physical size: if we've been to this
        // scale before, restore that exact size; otherwise
        // proportionally scale from the current size.
        int newPct = (int)lroundf(newScale * 100.0f);
        int newW, newH;
        auto it = savedSizeForScale.find(newPct);
        if (it != savedSizeForScale.end()) {
            newW = it->second.first;
            newH = it->second.second;
            winlog("  restore saved %dx%d for scale %d%%", newW, newH, newPct);
        } else {
            // Use lastKnownW/H (pre-auto-resize) instead of
            // SDL_GetWindowSize() which already reflects the
            // WM_DPICHANGED auto-resize.
            newW = (int)lroundf((float)lastKnownW * newScale / oldScale);
            newH = (int)lroundf((float)lastKnownH * newScale / oldScale);
            winlog("  proportional %dx%d = lastKnown %dx%d * %.3f/%.3f",
                   newW, newH, lastKnownW, lastKnownH, newScale, oldScale);
        }

        // Never restore/scale below the new scale's minimum — guarantees the window lands exactly
        // on the min in both directions even if the per-scale map was ever seeded oddly.
        const int minW = (int)(1024 * newScale), minH = (int)(720 * newScale);
        newW = std::max(newW, minW);
        newH = std::max(newH, minH);
        winlog("  after min-clamp (min %dx%d): %dx%d", minW, minH, newW, newH);

        // Clamp to the target display's work area so the
        // window doesn't overflow a smaller/higher-density monitor.
        SDL_DisplayID did = SDL_GetDisplayForWindow(window);
        if (did) {
            SDL_Rect usable;
            if (SDL_GetDisplayUsableBounds(did, &usable)) {
                newW = std::min(newW, usable.w);
                newH = std::min(newH, usable.h);
                winlog("  after display-clamp (usable %dx%d): %dx%d", usable.w, usable.h, newW, newH);
            }
        }

        // Set the new minimum BEFORE resizing. SDL_SetWindowSize clamps the request UP to the
        // current minimum, so when moving to a LOWER scale the shrink (e.g. 1080 -> 720) issued
        // while the min is still the old 1080 would be clamped back to 1080 and stick there
        // (SDL_SetWindowMinimumSize only ever grows a window, never shrinks it). Min-then-size
        // makes the shrink land on the new min immediately, without relying on the deferred retry.
        SDL_SetWindowMinimumSize(window, minW, minH);
        dpiResizePending = true;
        resizeWindowExact(window, newW, newH);
        lastKnownW = newW;
        lastKnownH = newH;
        // Arm deferred retry (SetWindowSize may not stick mid-drag) and suppress resize-recording
        // for the whole settle burst: a transient WINDOW_RESIZED (ours, the WM_DPICHANGED settle,
        // or a retry frame) that arrives after dpiResizePending was consumed and with scales already
        // matching would otherwise be misclassified as a user resize and corrupt savedSizeForScale.
        dpiTargetW = newW;
        dpiTargetH = newH;
        dpiResizeRetries = 30; // retry for ~30 frames
        dpiSettleFrames  = 40; // ignore resize-recording until the transition settles
        DEBUG_LOGF("  Window resized: %dx%d (scale %.2f, pct %d)\n",
               newW, newH, newScale, newPct);
        winlog("  SetWindowMinimumSize(%dx%d) then SetWindowSize(%dx%d); armed retries=30 settle=40",
               minW, minH, newW, newH);
        winlogState(window, "  after set");

        // 2) Rebuild font atlas at the new DPI scale
        typo.reload(io, newScale);

        // 3) Reset style → reapply base theme → scale by new DPI
        ImGui::GetStyle() = ImGuiStyle();
        dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
        if (newScale > 1.01f) {
            ImGui::GetStyle().ScaleAllSizes(newScale);
        }

        DEBUG_LOGF("  DPI transition complete\n");
    }
}

#if !defined(_WIN32)
#include <csignal>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#if defined(__has_include)
#  if __has_include(<execinfo.h>)
#    include <execinfo.h>
#    define DRAGONX_HAVE_BACKTRACE 1
#  endif
#endif

// Absolute path to the crash log, filled at install time so the async-signal handler needs no
// allocation. (POSIX counterpart of the Windows SEH CrashHandler above — W7-3.)
static char g_crashLogPath[1024] = {0};

// Async-signal-safe crash handler: only open()/write()/backtrace_symbols_fd()/raise() are used —
// no stdio, std::filesystem or malloc (all unsafe inside a signal handler).
static void PosixCrashHandler(int sig)
{
    int fd = g_crashLogPath[0] ? open(g_crashLogPath, O_WRONLY | O_CREAT | O_APPEND, 0600) : -1;
    if (fd >= 0) {
        auto put = [fd](const char* s) { ssize_t n = write(fd, s, std::strlen(s)); (void)n; };
        put("\n=== CRASH: signal ");
        char num[16]; int i = 0, v = sig;                        // signal number -> decimal, no stdio
        if (v == 0) { num[i++] = '0'; }
        else { char tmp[16]; int t = 0; while (v > 0) { tmp[t++] = char('0' + v % 10); v /= 10; }
               while (t > 0) num[i++] = tmp[--t]; }
        num[i] = '\n';
        ssize_t nn = write(fd, num, i + 1); (void)nn;
#ifdef DRAGONX_HAVE_BACKTRACE
        void* frames[64];
        int nframes = backtrace(frames, 64);
        backtrace_symbols_fd(frames, nframes, fd);               // async-signal-safe
#endif
        put("=== END CRASH ===\n");
        close(fd);
    }
    // Restore the default disposition and re-raise so we still get a core dump / normal termination.
    signal(sig, SIG_DFL);
    raise(sig);
}

static void installPosixCrashHandler(const std::string& crashLogPath)
{
    std::snprintf(g_crashLogPath, sizeof(g_crashLogPath), "%s", crashLogPath.c_str());
    struct sigaction sa;
    std::memset(&sa, 0, sizeof(sa));
    sa.sa_handler = PosixCrashHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    for (int sig : {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL}) {
        sigaction(sig, &sa, nullptr);
    }
}
#endif  // !_WIN32

int main(int argc, char* argv[])
{
    // Ensure ObsidianDragon config directory exists early (before any file I/O)
    {
        std::string odDir = dragonx::util::Platform::getObsidianDragonDir();
        std::string odErr;
        if (!dragonx::util::Platform::ensureDirectory(odDir, &odErr)) {
            // Pre-App-init: nothing (ini, logs, config) can persist if this fails, and the
            // Windows log redirect below isn't set up yet — report loudly before any setup.
            std::fprintf(stderr, "%s\n", odErr.c_str());
#ifdef _WIN32
            MessageBoxA(nullptr, odErr.c_str(), DRAGONX_APP_NAME, MB_OK | MB_ICONERROR);
#endif
            return 1;
        }
    }

    // W7-2: initialize the app-level Logger's file sink on ALL platforms so LOG/LOGF/VERBOSE_LOGF are
    // actually persisted to dragonx-debug.log. Previously init() was never called, so on Linux/macOS the
    // file never existed at all (the Windows-only stdout freopen below is a separate mechanism).
    {
        const std::string logPath =
            (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.log").string();
        dragonx::util::Logger::instance().init(logPath);
    }

#if !defined(_WIN32)
    // W7-3: install the POSIX crash handler (the Windows SEH filter is installed below). A segfault or
    // abort now leaves a backtrace in dragonx-crash.log instead of vanishing silently on Linux/macOS.
    {
        const std::string crashPath =
            (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-crash.log").string();
        installPosixCrashHandler(crashPath);
    }
#endif

#ifdef _WIN32
    // Redirect raw stdout/stderr (library / daemon-pipe writes) to a log file so it's visible even when
    // built as a GUI app (WIN32_EXECUTABLE hides the console). Separate file from the structured Logger
    // above so the two writers don't interleave/contend on one file.
    {
        std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-stdout.log").string();
        freopen(logPath.c_str(), "w", stdout);
        freopen(logPath.c_str(), "a", stderr);
    }
    // Install crash handler for diagnostics
    SetUnhandledExceptionFilter(CrashHandler);

    // Set the Application User Model ID so Windows Task Manager, taskbar,
    // and jump lists show "ObsidianDragon" instead of inheriting a
    // description from the MinGW runtime ("POSIX WinThreads for Windows").
    SetCurrentProcessExplicitAppUserModelID(L"DragonX.ObsidianDragon.Wallet");
#endif

    // Check for payment URI in command line
    std::string pendingURI = findPaymentURI(argc, argv);

    DEBUG_LOGF("%s v%s\n", DRAGONX_APP_NAME, DRAGONX_VERSION);
    DEBUG_LOGF("Built with Dear ImGui %s\n", IMGUI_VERSION);
    fflush(stdout);

    // Env-gated headless smoke for the isolated seed-wallet creator (migrate-to-seed Phase 1).
    // Exercises SeedWalletCreator::create() end-to-end without the GUI, then exits. Secret-safe:
    // it never prints the seed words — only ok/word-count/address/error. Run with an isolated HOME.
    if (const char* smk = std::getenv("DRAGONX_SEED_CREATE_SMOKE"); smk && std::string(smk) == "1") {
        sodium_init();
        printf("[seed-create-smoke] starting isolated create...\n"); fflush(stdout);
        auto r = dragonx::daemon::SeedWalletCreator::create(/*keepDatadir=*/false,
            [](const std::string& m) { printf("[seed-create-smoke] %s\n", m.c_str()); fflush(stdout); });
        int words = 0; bool inWord = false;
        for (char c : r.seedPhrase) { bool sp = (c == ' '); if (!sp && !inWord) { inWord = true; ++words; } else if (sp) inWord = false; }
        printf("[seed-create-smoke] ok=%d words=%d addr=%s err=%s\n",
               r.ok ? 1 : 0, words, r.destAddress.c_str(), r.error.c_str());
        if (!r.seedPhrase.empty()) sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
        return r.ok ? 0 : 1;
    }

    // Check for existing instance
    if (!g_single_instance.tryLock()) {
        fprintf(stderr, "Another instance of %s is already running.\n", DRAGONX_APP_NAME);
        DEBUG_LOGF("Please close the existing instance first.\n");
#ifdef _WIN32
        const std::string msg = std::string("Another instance of ") + DRAGONX_APP_NAME +
                                " is already running.\nPlease close it first.";
        MessageBoxA(nullptr, msg.c_str(), DRAGONX_APP_NAME, MB_OK | MB_ICONINFORMATION);
#endif
        return 1;
    }

    // Initialize SDL
    if (!InitSDL()) {
        fprintf(stderr, "Failed to initialize SDL!\n");
#ifdef _WIN32
        MessageBoxW(nullptr, L"Failed to initialize SDL. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
                    L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
#endif
        return 1;
    }

    // Load saved window size early (before window creation, before App exists).
    // Settings are loaded again later in app.init(), this is just for the size.
    int savedWinW = 1200, savedWinH = 720;
    {
        dragonx::config::Settings earlySettings;
        if (earlySettings.load()) {
            int sw = earlySettings.getWindowWidth();
            int sh = earlySettings.getWindowHeight();
            if (sw >= 1024 && sh >= 720) {
                savedWinW = sw;
                savedWinH = sh;
                DEBUG_LOGF("Restored window size from settings: %dx%d\n", sw, sh);
            } else if (sw > 0 || sh > 0) {
                DEBUG_LOGF("Ignored saved window size %dx%d (below 1024x720 minimum) — using default %dx%d\n",
                           sw, sh, savedWinW, savedWinH);
            }
        }
    }

    // ---------------------------------------------------------------
    // Platform-specific window creation and graphics context
    // ---------------------------------------------------------------
#ifdef DRAGONX_USE_DX11
    // DirectX 11 path (Windows)
    //
    // We create the native Win32 HWND ourselves so we can set
    // WS_EX_NOREDIRECTIONBITMAP at creation time.  This style MUST be
    // present when the window is first created -- setting it later with
    // SetWindowLongPtrW does NOT reliably release the opaque redirection
    // surface that DWM allocates.  Without it, the legacy redirection
    // surface paints over our transparent DirectComposition visual.
    // This is the same approach Windows Terminal uses.

    // Register a minimal window class
    WNDCLASSEXW wc = {};
    wc.cbSize        = sizeof(WNDCLASSEXW);
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = DefWindowProcW;
    wc.hInstance     = GetModuleHandleW(nullptr);
    wc.hCursor       = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); // IDC_ARROW
    // Load application icon from .exe resources (.rc embeds IDI_ICON1 = ordinal 1)
    wc.hIcon         = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(1));
    wc.hIconSm       = (HICON)LoadImageW(wc.hInstance, MAKEINTRESOURCEW(1),
                            IMAGE_ICON,
                            GetSystemMetrics(SM_CXSMICON),
                            GetSystemMetrics(SM_CYSMICON),
                            LR_DEFAULTCOLOR);
    wc.lpszClassName = L"ObsidianDragon";
    RegisterClassExW(&wc);

    // Compute centered position including frame
    // Use saved window size from settings if available, otherwise default
    int winW = savedWinW, winH = savedWinH;
    DWORD dwStyle   = WS_OVERLAPPEDWINDOW;
    DWORD dwExStyle = WS_EX_APPWINDOW | WS_EX_NOREDIRECTIONBITMAP;
    RECT wr = {0, 0, winW, winH};
    AdjustWindowRectEx(&wr, dwStyle, FALSE, dwExStyle);
    int adjW = wr.right  - wr.left;
    int adjH = wr.bottom - wr.top;
    int posX = (GetSystemMetrics(SM_CXSCREEN) - adjW) / 2;
    int posY = (GetSystemMetrics(SM_CYSCREEN) - adjH) / 2;

    HWND nativeHwnd = CreateWindowExW(
        dwExStyle, L"ObsidianDragon",
        L"ObsidianDragon",
        dwStyle,
        posX, posY, adjW, adjH,
        nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
    if (!nativeHwnd) {
        fprintf(stderr, "Failed to create native Win32 window (error %lu)\n", GetLastError());
        MessageBoxW(nullptr, L"Failed to create window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
                    L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
        SDL_Quit();
        return 1;
    }
    DEBUG_LOGF("Native HWND created with WS_EX_NOREDIRECTIONBITMAP\n");
    // Don't show yet — wait until borderless subclass is installed
    // so the window appears without a visible native title bar.

    // Explicitly set the window icon on the HWND for taskbar + Alt-Tab.
    // The WNDCLASSEXW already sets hIcon/hIconSm, but WM_SETICON ensures
    // the shell picks up the icon even for borderless windows.
    {
        HINSTANCE hInst = GetModuleHandleW(nullptr);
        HICON bigIcon = LoadIconW(hInst, MAKEINTRESOURCEW(1));
        HICON smIcon  = (HICON)LoadImageW(hInst, MAKEINTRESOURCEW(1),
                            IMAGE_ICON,
                            GetSystemMetrics(SM_CXSMICON),
                            GetSystemMetrics(SM_CYSMICON),
                            LR_DEFAULTCOLOR);
        if (bigIcon) SendMessageW(nativeHwnd, WM_SETICON, ICON_BIG,   (LPARAM)bigIcon);
        if (smIcon)  SendMessageW(nativeHwnd, WM_SETICON, ICON_SMALL, (LPARAM)smIcon);
        DEBUG_LOGF("Taskbar icon set from .exe resource (big=%p small=%p)\n",
               (void*)bigIcon, (void*)smIcon);
    }

    // Wrap the native HWND with SDL for event handling + ImGui integration.
    // SDL_WINDOW_TRANSPARENT triggers DwmEnableBlurBehindWindow internally,
    // which is another signal to DWM to respect per-pixel alpha.
    SDL_PropertiesID createProps = SDL_CreateProperties();
    SDL_SetPointerProperty(createProps, SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER, (void*)nativeHwnd);
    SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_TRANSPARENT_BOOLEAN, true);
    SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
    SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
    SDL_SetStringProperty(createProps, SDL_PROP_WINDOW_CREATE_TITLE_STRING, DRAGONX_APP_NAME);
    SDL_SetNumberProperty(createProps, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, winW);
    SDL_SetNumberProperty(createProps, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, winH);
    SDL_Window* window = SDL_CreateWindowWithProperties(createProps);
    SDL_DestroyProperties(createProps);
    if (window == nullptr) {
        fprintf(stderr, "Error: SDL_CreateWindowWithProperties(): %s\n", SDL_GetError());
        MessageBoxW(nullptr, L"Failed to create SDL window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
                    L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
        DestroyWindow(nativeHwnd);
        SDL_Quit();
        return 1;
    }
    DEBUG_LOGF("SDL window wrapping native HWND (external)\n");

    // Subclass for borderless window (removes native title bar).
    // SDL has already installed its own WndProc; we save it and chain to it.
    g_sdlWndProc = (WNDPROC)GetWindowLongPtrW(nativeHwnd, GWLP_WNDPROC);
    SetWindowLongPtrW(nativeHwnd, GWLP_WNDPROC, (LONG_PTR)BorderlessWndProc);
    // Resize to intended client dimensions (no frame/title-bar overhead)
    // and trigger a frame recalculation so WM_NCCALCSIZE fires.
    SetWindowPos(nativeHwnd, nullptr, 0, 0, winW, winH,
                 SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOZORDER);
    ShowWindow(nativeHwnd, SW_SHOW);
    UpdateWindow(nativeHwnd);
    DEBUG_LOGF("Borderless window: native title bar removed\n");

    // Set shell property store on the HWND so Task Manager and the taskbar
    // always show "ObsidianDragon" (overrides any cached metadata).
    SetWindowIdentity(nativeHwnd);

    // Initialize DirectX 11 context with DXGI alpha swap chain
    dragonx::platform::DX11Context dx;
    if (!dx.init(window)) {
        fprintf(stderr, "Error: Failed to initialize DirectX 11 context\n");
        MessageBoxW(nullptr, L"Failed to initialize DirectX 11.\nPlease ensure your graphics drivers are up to date.\n\nCheck the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
                    L"ObsidianDragon - Graphics Error", MB_OK | MB_ICONERROR);
        SDL_DestroyWindow(window);
        SDL_Quit();
        return 1;
    }
    DEBUG_LOGF("DirectX 11 rendering backend initialized\n");
    fflush(stdout);

    bool backdrop_active = false;

#ifdef _WIN32
    // Transparency pipeline:
    //   1) WS_EX_NOREDIRECTIONBITMAP  (set at HWND creation)
    //   2) DirectComposition swap chain with DXGI_ALPHA_MODE_PREMULTIPLIED
    //   3) DwmEnableBlurBehindWindow with full-window region for blur
    //   4) On Win11 22H2+: Acrylic (DWMSBT_TRANSIENTWINDOW) for modern blur
    //
    // NOTE: Mica/MicaAlt (DWMSBT_MAINWINDOW / DWMSBT_TABBEDWINDOW) are
    // opaque materials and must NOT be used — they fill the entire window
    // with a wallpaper-derived color.  Acrylic is the correct choice: it
    // provides gaussian blur of the desktop behind the window.
    if (dx.hasAlphaCompositing()) {
        backdrop_active = true;
        dragonx::ui::material::SetBackdropActive(true);

        HWND hwnd_blur = (HWND)SDL_GetPointerProperty(
            SDL_GetWindowProperties(window),
            SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
        if (hwnd_blur) {
            // Override SDL's zero-area blur region with a full-window blur.
            // This gives DWM the signal to blur everything behind our window.
            HRGN blurRgn = CreateRectRgn(0, 0, -1, -1);  // full window
            DWM_BLURBEHIND bb = {};
            bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
            bb.fEnable = TRUE;
            bb.hRgnBlur = blurRgn;
            HRESULT hr_blur = DwmEnableBlurBehindWindow(hwnd_blur, &bb);
            DeleteObject(blurRgn);
            DEBUG_LOGF("DwmEnableBlurBehindWindow (full region): 0x%08lx\n", hr_blur);

            // On Windows 11 22H2+, also enable Acrylic for nicer modern blur
            auto ver = dragonx::platform::getWindowsVersion();
            if (ver.build >= 22621) {
                // Signal to DWM that we handle our own frame rendering.
                // Use {0,0,1,0} (1 pixel top) instead of {-1,-1,-1,-1}
                // because full-window margins cause DWM to render its own
                // caption buttons (min/max/close) on top of ours.
                // WM_NCCALCSIZE returning 0 already extends the client area
                // to the full window, which satisfies the system backdrop
                // requirement.  The 1-pixel top margin is enough to keep
                // DWM's shadow and rounded corners on Win11.
                MARGINS margins = {0, 0, 1, 0};
                DwmExtendFrameIntoClientArea(hwnd_blur, &margins);

                // Acrylic = blurred, semi-transparent desktop view
                // (NOT Mica which is opaque)
                DWORD backdrop = 3;  // DWMSBT_TRANSIENTWINDOW = Acrylic
                HRESULT hr_acr = DwmSetWindowAttribute(hwnd_blur, 38,
                    &backdrop, sizeof(backdrop));
                DEBUG_LOGF("DWM Acrylic (DWMSBT_TRANSIENTWINDOW): 0x%08lx\n", hr_acr);

                BOOL useDarkMode = TRUE;
                DwmSetWindowAttribute(hwnd_blur, 20,
                    &useDarkMode, sizeof(useDarkMode));
            }
        }
        DEBUG_LOGF("DX11: Alpha compositing active with blur\n");
        fflush(stdout);
    } else {
        DEBUG_LOGF("DX11: No alpha compositing - window is opaque\n");
    }
#endif

    // Initialize ImGui with DX11 backend
    if (!InitImGui(window, dx)) {
        fprintf(stderr, "Failed to initialize ImGui!\n");
        MessageBoxW(nullptr, L"Failed to initialize ImGui. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
                    L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
        Shutdown(window, dx);
        return 1;
    }

    // Initialize acrylic effects system (DX11 backend)
    if (dragonx::ui::effects::ImGuiAcrylic::Init()) {
        DEBUG_LOGF("Acrylic effects initialized (DX11)\n");
    } else {
        DEBUG_LOGF("Acrylic effects not available on DX11 (will use fallback colors)\n");
    }

#else
    // OpenGL path (Linux / macOS)
#ifdef __APPLE__
    // macOS requires GL 3.2 Core Profile + GLSL 150
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
#else
    // GL 3.0 + GLSL 130
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#endif

    // Create window with graphics context
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);

    SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_TRANSPARENT);
    SDL_Window* window = SDL_CreateWindow(
        DRAGONX_APP_NAME,
        savedWinW, savedWinH,
        window_flags
    );

    if (window == nullptr) {
        // Retry without transparency if compositor doesn't support it
        window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
        window = SDL_CreateWindow(
            DRAGONX_APP_NAME,
            savedWinW, savedWinH,
            window_flags
        );
    }

    if (window == nullptr) {
        fprintf(stderr, "Error: SDL_CreateWindow(): %s\n", SDL_GetError());
        return 1;
    }

    SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
    SDL_SetWindowMinimumSize(window, 1024, 720);
    SDL_GLContext gl_context = SDL_GL_CreateContext(window);
    if (gl_context == nullptr) {
        fprintf(stderr, "Error: SDL_GL_CreateContext(): %s\n", SDL_GetError());
        return 1;
    }

    SDL_GL_MakeCurrent(window, gl_context);
    SDL_GL_SetSwapInterval(1); // Enable vsync

    bool backdrop_active = false;

    // Check if the compositor granted window transparency
    if (SDL_GetWindowFlags(window) & SDL_WINDOW_TRANSPARENT) {
        backdrop_active = true;
        dragonx::ui::material::SetBackdropActive(true);
        DEBUG_LOGF("Window transparency enabled by compositor\n");
    }

#ifdef DRAGONX_HAS_GLAD
    // Initialize GLAD - load OpenGL function pointers
    int glad_version = gladLoadGL((GLADloadfunc)SDL_GL_GetProcAddress);
    if (!glad_version) {
        fprintf(stderr, "Error: Failed to initialize GLAD (OpenGL loader)\n");
        return 1;
    }
    DEBUG_LOGF("GLAD initialized - OpenGL %d.%d loaded\n", GLAD_VERSION_MAJOR(glad_version), GLAD_VERSION_MINOR(glad_version));
    
    // Initialize acrylic effects system
    if (dragonx::ui::effects::ImGuiAcrylic::Init()) {
        DEBUG_LOGF("Acrylic effects initialized\n");
        // Enable backdrop so glass panels use acrylic blur
        if (!backdrop_active) {
            backdrop_active = true;
            dragonx::ui::material::SetBackdropActive(true);
            DEBUG_LOGF("Backdrop activated for acrylic effects\n");
        }
    } else {
        DEBUG_LOGF("Acrylic effects not available (will use fallback colors)\n");
    }

#endif

    // Initialize ImGui with OpenGL backend
    if (!InitImGui(window, gl_context)) {
        fprintf(stderr, "Failed to initialize ImGui!\n");
        Shutdown(window, gl_context);
        return 1;
    }
#endif // DRAGONX_USE_DX11

    // Set window icon from logo_ObsidianDragon.png
    {
        std::string iconPath = dragonx::util::getExecutableDirectory() + "/res/img/logos/logo_ObsidianDragon.png";
        int iconW = 0, iconH = 0;
        unsigned char* iconPixels = dragonx::util::LoadRawPixelsFromFile(iconPath.c_str(), &iconW, &iconH);
        if (!iconPixels) {
            // Try embedded resource fallback (Windows single-file distribution)
            const auto* iconRes = dragonx::resources::getEmbeddedResource("logo_ObsidianDragon.png");
            if (iconRes && iconRes->data && iconRes->size > 0) {
                iconPixels = dragonx::util::LoadRawPixelsFromMemory(iconRes->data, iconRes->size, &iconW, &iconH);
            }
        }
        if (iconPixels) {
            SDL_Surface* iconSurface = SDL_CreateSurfaceFrom(iconW, iconH,
                SDL_PIXELFORMAT_RGBA32, iconPixels, iconW * 4);
            if (iconSurface) {
                SDL_SetWindowIcon(window, iconSurface);
                SDL_DestroySurface(iconSurface);
                DEBUG_LOGF("Window icon set (%dx%d)\n", iconW, iconH);
            }
            dragonx::util::FreeRawPixels(iconPixels);
        } else {
            DEBUG_LOGF("Warning: Could not load window icon from %s or embedded\n", iconPath.c_str());
        }
    }

    // Apply DragonX Material Design theme
    dragonx::ui::SetDragonXMaterialTheme();

    // Scale ImGui style sizes (padding, rounding, scrollbar, etc.) by the
    // display DPI factor.  On Windows Per-Monitor DPI v2, SDL3 works in
    // physical pixels and DisplayFramebufferScale is 1.0, so we must
    // manually scale both fonts (done in Typography::load) and style.
    // The window is also resized by dpiScale below so everything fits.
    float currentDpiScale = dragonx::ui::material::Typography::instance().getDpiScale();
#ifdef DRAGONX_USE_DX11
    g_borderlessDpi = currentDpiScale;
#endif
    if (currentDpiScale > 1.01f) {
        ImGui::GetStyle().ScaleAllSizes(currentDpiScale);
        DEBUG_LOGF("Scaled ImGui style sizes by %.2f for DPI awareness\n", currentDpiScale);
    }

    // If we're starting on a HiDPI display, scale the window so the UI
    // appears the same physical size as on a 100% display.
    // On macOS with HIGH_PIXEL_DENSITY, window sizes are already in logical
    // (point) coordinates and the framebuffer handles pixel density, so
    // we must NOT multiply the window size by the content scale.
#ifndef __APPLE__
    {
        int curW = 0, curH = 0;
        SDL_GetWindowSize(window, &curW, &curH);
        int newW = curW, newH = curH;
        if (currentDpiScale > 1.01f) {
            // On a HiDPI display the saved size is logical px — scale it up to physical so the UI
            // appears the same physical size as on a 100% display, and scale the minimum too.
            newW = (int)(curW * currentDpiScale);
            newH = (int)(curH * currentDpiScale);
            SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale));
        }

        // DEV/TEST hook (dormant unless the env is set): DRAGONX_WIN_GEOM="WxH" forces an exact window
        // size, placing it on the largest display that can hold it and bypassing the primary-monitor
        // clamp. Lets a headless WSLg sweep render at sizes wider than the 1280 primary (e.g. 2560x1440
        // on the mirrored 4K/1440p outputs). Needs the x11 backend so absolute positioning takes effect.
        int wantW = 0, wantH = 0;
        if (const char* geom = std::getenv("DRAGONX_WIN_GEOM"))
            sscanf(geom, "%dx%d", &wantW, &wantH);
        if (wantW > 0 && wantH > 0) {
            int count = 0;
            SDL_DisplayID* disp = SDL_GetDisplays(&count);
            SDL_DisplayID best = 0; SDL_Rect bestUsable{0, 0, 0, 0};
            for (int i = 0; i < count; ++i) {
                SDL_Rect u;
                if (!SDL_GetDisplayUsableBounds(disp[i], &u)) continue;
                bool fits = (u.w >= wantW && u.h >= wantH);
                bool bestFits = (bestUsable.w >= wantW && bestUsable.h >= wantH);
                // Prefer a display that fits; among those, the smallest; else the largest available.
                if ((fits && !bestFits) ||
                    (fits && bestFits && (long)u.w * u.h < (long)bestUsable.w * bestUsable.h) ||
                    (!fits && !bestFits && (long)u.w * u.h > (long)bestUsable.w * bestUsable.h)) {
                    bestUsable = u; best = disp[i];
                }
            }
            if (disp) SDL_free(disp);
            if (best) {
                SDL_SetWindowMinimumSize(window, 320, 240);
                SDL_SetWindowPosition(window, bestUsable.x + 10, bestUsable.y + 10);
                SDL_SetWindowSize(window, wantW, wantH);
            }
        } else {
        // Clamp to the current display's work area — runs on EVERY startup (this clamp used to live
        // inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the
        // window off-screen or bigger than the screen on a same-DPI cold start).
        SDL_DisplayID did = SDL_GetDisplayForWindow(window);
        if (did) {
            SDL_Rect usable;
            if (SDL_GetDisplayUsableBounds(did, &usable)) {
                newW = std::min(newW, usable.w);
                newH = std::min(newH, usable.h);
            }
        }

        if (newW != curW || newH != curH) {
            SDL_SetWindowSize(window, newW, newH);
            SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
            DEBUG_LOGF("Startup: window fitted %dx%d -> %dx%d (scale %.2f)\n",
                   curW, curH, newW, newH, currentDpiScale);
        }
        }
    }
#endif
    winlog("STARTUP  savedSize=%dx%d  currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale);
    winlogState(window, "  startup");

    // Create application instance
    dragonx::App app;
    if (!app.init()) {
        fprintf(stderr, "Failed to initialize application!\n");
#ifdef _WIN32
        MessageBoxW(nullptr, L"Failed to initialize application. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
                    L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
#endif
#ifdef DRAGONX_USE_DX11
        Shutdown(window, dx);
#else
        Shutdown(window, gl_context);
#endif
        return 1;
    }

    // Activate backdrop transparency if the skin system loaded a background
    // image during app.init() → SkinManager::setActiveSkin().
    if (app.getGradientTexture() != 0 && !backdrop_active) {
        backdrop_active = true;
        dragonx::ui::material::SetBackdropActive(true);
        // Re-apply TOML-based colors with the new backdrop alpha values.
        // Must NOT call SetDragonXMaterialTheme() here — that would clobber
        // the active TOML palette with hardcoded C++ DragonX colors, causing
        // a visual shift when the user later cycles themes via Ctrl+Arrow.
        dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
    }

    // Initialize performance profiler — writes to <ObsidianDragonDir>/perf.log
    {
        std::string perfPath = (std::filesystem::path(
            dragonx::util::Platform::getObsidianDragonDir()) / "perf.log").string();
        dragonx::util::PerfLog::instance().init(perfPath);
    }

    // NOTE: No font-scale startup resize here.  The saved window size
    // already includes any font-scale inflation from the previous session
    // (save divides by dpiScale but not fontScale).  Multiplying by
    // fontScale again would compound the effect on every restart.
    // The per-frame font-scale block in the main loop handles resizing
    // when the user actually changes the slider.

    // Handle pending payment URI from command line
    if (!pendingURI.empty()) {
        DEBUG_LOGF("Processing payment URI: %s\n", pendingURI.c_str());
        app.handlePaymentURI(pendingURI);
    }

    // ================================================================
    // Live-resize support (Linux/X11)
    // On X11, the window manager runs a modal loop during drag-resize,
    // blocking SDL_PollEvent.  We install an event watcher that fires
    // immediately from the WM callback and performs a full render cycle
    // so the UI redraws in real time while the window is being resized.
    // ================================================================
    struct ResizeCtx {
        SDL_Window*     window;
        dragonx::App*   app;
        bool            backdrop_active;
#ifdef DRAGONX_USE_DX11
        dragonx::platform::DX11Context*    dx;
#else
        SDL_GLContext    gl_context;
#endif
    };
    ResizeCtx resizeCtx;
    resizeCtx.window          = window;
    resizeCtx.app             = &app;
    resizeCtx.backdrop_active = backdrop_active;
#ifdef DRAGONX_USE_DX11
    resizeCtx.dx = &dx;
#else
    resizeCtx.gl_context = gl_context;
#endif

    auto resizeWatcher = [](void* userdata, SDL_Event* event) -> bool {
        if (event->type != SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED &&
            event->type != SDL_EVENT_WINDOW_RESIZED)
            return true;

        auto* ctx = static_cast<ResizeCtx*>(userdata);
        if (event->window.windowID != SDL_GetWindowID(ctx->window))
            return true;

        // Avoid re-entrancy
        static bool in_resize_render = false;
        if (in_resize_render) return true;
        in_resize_render = true;

        // Pre-frame: font atlas rebuilds before NewFrame()
        ctx->app->preFrame();

#ifdef DRAGONX_USE_DX11
        ctx->dx->resize(event->window.data1, event->window.data2);
        ImGui_ImplDX11_NewFrame();
#else
        SDL_GL_MakeCurrent(ctx->window, ctx->gl_context);
        ImGui_ImplOpenGL3_NewFrame();
#endif
        ImGui_ImplSDL3_NewFrame();
        ImGui::NewFrame();

        // Background gradient
        {
            ImGuiViewport* vp = ImGui::GetMainViewport();
            ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
            ImVec2 p0 = vp->Pos;
            ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
            // Resize path historically always sampled the texture and always
            // added the capture callback — pass lowSpec = false to preserve
            // that behavior exactly.
            drawWindowBackdrop(*ctx->app, bgDL, p0, p1, ctx->backdrop_active, /*lowSpec=*/false);
        }

        ImGuiIO& io = ImGui::GetIO();
        dragonx::ui::effects::ImGuiAcrylic::BeginFrame(
            (int)io.DisplaySize.x, (int)io.DisplaySize.y);

        ctx->app->update();
        ctx->app->render();

        ImGui::Render();
#ifdef DRAGONX_USE_DX11
        ctx->dx->ensureSize();
        ID3D11RenderTargetView* rtv = ctx->dx->renderTargetView();
        ctx->dx->deviceContext()->OMSetRenderTargets(1, &rtv, nullptr);
        {
            float wo = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;
            bool opq = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f;
            bool eb = (ctx->backdrop_active || ctx->app->getGradientTexture() != 0) && !opq;
            ctx->dx->clear(0.0f, 0.0f, 0.0f, eb ? 0.0f : 1.0f);
        }
        ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
        ctx->dx->present(0); // immediate present during resize
#else
        glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
        {
            float wo = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;
            bool opq = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f;
            bool eb = (ctx->backdrop_active || ctx->app->getGradientTexture() != 0) && !opq;
            glClearColor(0.0f, 0.0f, 0.0f, eb ? 0.0f : 1.0f);
        }
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
        SDL_GL_SwapWindow(ctx->window);
#endif

        in_resize_render = false;
        return true;
    };

    SDL_AddEventWatch(resizeWatcher, &resizeCtx);

    // Main loop
    bool running = true;
    bool needsRedraw = true;  // P6: Start with a full redraw
    auto lastPeriodicFrame = std::chrono::steady_clock::now();

    // Per-scale saved window sizes for DPI round-trips.
    // Each entry maps scale-percentage (100, 125, 150, 200, …) to the
    // physical pixel size the user had at that scale.  When transitioning
    // to a scale we've visited before the stored size is restored, so
    // resizing on a 200% screen doesn't clobber the 100% size.
    std::map<int, std::pair<int,int>> savedSizeForScale;
    bool dpiResizePending = false;
    bool fontScaleResizePending = false;
    // Font-scale window reference: size at scale 1.0 (updated on user resize)
    float fontScaleRefFS = 0.0f;
    int   fontScaleRefW  = 0, fontScaleRefH = 0;
    // Track last known window size at the current DPI scale.
    // Updated on user-initiated resizes (not DPI auto-resizes).
    // When DISPLAY_SCALE_CHANGED fires, SDL_GetWindowSize() already
    // returns the auto-resized value, so we use these instead.
    int lastKnownW = 0, lastKnownH = 0;
    // Deferred resize: SDL_SetWindowSize may not stick when the user
    // is dragging the window between monitors (Windows modal drag loop
    // overrides SetWindowPos).  We retry for several frames.
    int dpiTargetW = 0, dpiTargetH = 0;
    int dpiResizeRetries = 0;
    // Frames to keep ignoring resize-recording after a DPI transition (settle window) so transient
    // WINDOW_RESIZED events during the transition can't corrupt savedSizeForScale / lastKnownW/H.
    int dpiSettleFrames = 0;
    SDL_DisplayID lastLoggedDisplay = 0;   // [WINLOG] throttle: log MOVED only when the display changes
    Uint64 minimizedLastTickMs = 0;        // real-clock tick for the minimized "keep syncing" update
    {
        float s = dragonx::ui::material::Typography::instance().getDpiScale();
        int w = 0, h = 0;
        SDL_GetWindowSize(window, &w, &h);
        int pct = (int)lroundf(s * 100.0f);
        savedSizeForScale[pct] = {w, h};
        lastKnownW = w;
        lastKnownH = h;
    }

    while (running) {
        // Deferred DPI resize: if a previous DPI transition set a target
        // size that SDL_SetWindowSize didn't achieve (e.g. because the
        // window was being dragged), retry until it sticks.
        if (dpiResizeRetries > 0) {
            int curW = 0, curH = 0;
            SDL_GetWindowSize(window, &curW, &curH);
            if (curW != dpiTargetW || curH != dpiTargetH) {
                dpiResizePending = true;
                resizeWindowExact(window, dpiTargetW, dpiTargetH);
                DEBUG_LOGF("  Deferred resize retry %d: %dx%d -> %dx%d\n",
                       dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
                winlog("RETRY %d: cur %dx%d != target %dx%d -> SetWindowSize(target)",
                       dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
            } else {
                // Size matches — done
                winlog("RETRY done: reached target %dx%d", dpiTargetW, dpiTargetH);
                dpiResizeRetries = 0;
            }
            if (dpiResizeRetries == 1) winlogState(window, "RETRY-last(stuck?)");
            --dpiResizeRetries;
            needsRedraw = true;
        }
        // Burn down the post-DPI-transition settle window (see handleDisplayScaleChange). The retry
        // above forces redraws while active, so this drains promptly even if the app is otherwise idle.
        if (dpiSettleFrames > 0) --dpiSettleFrames;

        // P6: Idle rendering — sleep if nothing needs redrawing
        if (!needsRedraw) {
            // Wait up to 200ms for user input or wake event
            SDL_Event waitEvent;
            if (SDL_WaitEventTimeout(&waitEvent, 200)) {
                ImGui_ImplSDL3_ProcessEvent(&waitEvent);
                if (waitEvent.type == SDL_EVENT_QUIT) {
                    app.beginShutdown();
                }
                if (waitEvent.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED &&
                    waitEvent.window.windowID == SDL_GetWindowID(window)) {
                    app.beginShutdown();
                }
#ifdef DRAGONX_USE_DX11
                if (waitEvent.type == SDL_EVENT_WINDOW_RESIZED &&
                    waitEvent.window.windowID == SDL_GetWindowID(window)) {
                    dx.resize(waitEvent.window.data1, waitEvent.window.data2);
                }
#endif
                // Update saved size for current scale on user-initiated resize.
                // DPI-related resizes (from WM_DPICHANGED or our own SDL_SetWindowSize)
                // are skipped so they don't overwrite the saved size.
                if (waitEvent.type == SDL_EVENT_WINDOW_RESIZED &&
                    waitEvent.window.windowID == SDL_GetWindowID(window)) {
                    float actualScale = SDL_GetWindowDisplayScale(window);
                    float storedScale = dragonx::ui::material::Typography::instance().getDpiScale();
#ifdef __APPLE__
                    actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
#endif
                    bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
                                      std::abs(actualScale - storedScale) > 0.01f;
                    winlog("RESIZED(wait) evt=%dx%d actualScale=%.3f storedScale=%.3f dpiPending=%d settle=%d retries=%d -> %s",
                           waitEvent.window.data1, waitEvent.window.data2, actualScale, storedScale,
                           (int)dpiResizePending, dpiSettleFrames, dpiResizeRetries,
                           isDpiResize ? "IGNORED(dpi)" : "RECORDED(user)");
                    if (dpiResizePending) dpiResizePending = false;
                    if (!isDpiResize) {
                        int pct = (int)lroundf(storedScale * 100.0f);
                        savedSizeForScale[pct] = {waitEvent.window.data1, waitEvent.window.data2};
                        lastKnownW = waitEvent.window.data1;
                        lastKnownH = waitEvent.window.data2;
                    }
                }
                // Window restored from minimized — trigger immediate data refresh
                if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED &&
                    waitEvent.window.windowID == SDL_GetWindowID(window)) {
                    app.skipDaemonOutputBacklog();   // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
                    app.refreshNow();
                }
                // Handle DPI change that arrived while idle (same logic as poll loop)
                if (waitEvent.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED &&
                    waitEvent.window.windowID == SDL_GetWindowID(window)) {
                    float newScale = SDL_GetWindowDisplayScale(window);
                    if (newScale <= 0.0f) newScale = 1.0f;
#ifdef __APPLE__
                    newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
#endif
                    winlog("SCALE_CHANGED(wait) reported newScale=%.3f", newScale);
                    handleDisplayScaleChange(window, newScale, savedSizeForScale,
                                             lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
                                             dpiResizeRetries, dpiResizePending, dpiSettleFrames);
                }
                needsRedraw = true;  // Got an event — redraw
            }
            // Timeout expired: only redraw if there's actual work
            if (!needsRedraw) {
                auto now = std::chrono::steady_clock::now();
                auto sinceLast = std::chrono::duration_cast<std::chrono::milliseconds>(
                                     now - lastPeriodicFrame).count();

                // Immediate triggers: async RPC results or visible notifications
                bool hasImmediateWork = app.hasPendingRPCResults()
                                     || app.hasTransactionSendProgress()
                                     || app.isTransactionRefreshInProgress()
                                     || dragonx::ui::Notifications::instance().hasActive();

                // Periodic maintenance: fire refresh timers in app.update()
                // Low-spec: every 5s; normal: every 2s
                int periodMs = dragonx::ui::effects::isLowSpecMode() ? 5000 : 2000;

                if (hasImmediateWork || sinceLast >= periodMs) {
                    needsRedraw = true;
                } else {
                    continue;  // Nothing to do — go back to sleep
                }
            }
        }

        // Poll remaining events
        SDL_Event event;
        // Deferred font-scale commit: smooth visual update during Alt+scroll,
        // atlas rebuild after scrolling stops (~200ms idle).
        static Uint64 fontScaleCommitTick = 0;  // 0 = nothing pending

        while (SDL_PollEvent(&event)) {
            // Alt + scroll wheel: smooth font scale adjustment
            if (event.type == SDL_EVENT_MOUSE_WHEEL) {
                SDL_Keymod mods = SDL_GetModState();
                if (mods & SDL_KMOD_ALT) {
                    float step = 0.05f * event.wheel.y;
                    float cur  = dragonx::ui::Layout::userFontScale();
                    float next = std::max(1.0f, std::min(1.5f, cur + step));
                    if (next != cur) {
                        // Smooth preview — no atlas rebuild yet
                        dragonx::ui::Layout::setUserFontScaleVisual(next);
                        // Schedule atlas rebuild after scrolling stops
                        fontScaleCommitTick = SDL_GetTicks() + 200;
                    }
                    // Don't pass Alt+scroll to ImGui (would scroll windows)
                    continue;
                }
            }
            ImGui_ImplSDL3_ProcessEvent(&event);
            
            if (event.type == SDL_EVENT_QUIT) {
                app.beginShutdown();
            }
            if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED &&
                event.window.windowID == SDL_GetWindowID(window)) {
                app.beginShutdown();
            }
            // [WINLOG] Log a window MOVE only when it crosses to a different display (avoids drag spam).
            if (event.type == SDL_EVENT_WINDOW_MOVED &&
                event.window.windowID == SDL_GetWindowID(window)) {
                SDL_DisplayID d = SDL_GetDisplayForWindow(window);
                if (d != lastLoggedDisplay) {
                    lastLoggedDisplay = d;
                    winlog("MOVED to display %u  pos=%d,%d  dispScale=%.3f",
                           (unsigned)d, event.window.data1, event.window.data2,
                           SDL_GetWindowDisplayScale(window));
                    winlogState(window, "  on-cross");
                }
            }
#ifdef DRAGONX_USE_DX11
            // Handle window resize for DX11 swap chain
            if (event.type == SDL_EVENT_WINDOW_RESIZED &&
                event.window.windowID == SDL_GetWindowID(window)) {
                dx.resize(event.window.data1, event.window.data2);
            }
#endif
            // Update saved size for current scale on user-initiated resize.
            // DPI-related resizes (from WM_DPICHANGED or our own SDL_SetWindowSize)
            // are skipped so they don't overwrite the saved size.
            if (event.type == SDL_EVENT_WINDOW_RESIZED &&
                event.window.windowID == SDL_GetWindowID(window)) {
                float actualScale = SDL_GetWindowDisplayScale(window);
                float storedScale = dragonx::ui::material::Typography::instance().getDpiScale();
#ifdef __APPLE__
                actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
#endif
                bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
                                   std::abs(actualScale - storedScale) > 0.01f;
                winlog("RESIZED(poll) evt=%dx%d actualScale=%.3f storedScale=%.3f dpiPending=%d settle=%d retries=%d fsPending=%d -> %s",
                       event.window.data1, event.window.data2, actualScale, storedScale,
                       (int)dpiResizePending, dpiSettleFrames, dpiResizeRetries, (int)fontScaleResizePending,
                       isDpiResize ? "IGNORED(dpi)" : "RECORDED(user)");
                if (dpiResizePending) dpiResizePending = false;
                bool isFSResize = fontScaleResizePending;
                if (fontScaleResizePending) fontScaleResizePending = false;
                if (!isDpiResize) {
                    int pct = (int)lroundf(storedScale * 100.0f);
                    savedSizeForScale[pct] = {event.window.data1, event.window.data2};
                    lastKnownW = event.window.data1;
                    lastKnownH = event.window.data2;
                    // User manually resized — update the font-scale reference
                    // so the next scale change is relative to this size.
                    if (!isFSResize) {
                        float fs = dragonx::ui::Layout::userFontScale();
                        fontScaleRefW = (int)lroundf((float)event.window.data1 / fs);
                        fontScaleRefH = (int)lroundf((float)event.window.data2 / fs);
                    }
                }
            }
            // Window restored from minimized — trigger immediate data refresh
            if (event.type == SDL_EVENT_WINDOW_RESTORED &&
                event.window.windowID == SDL_GetWindowID(window)) {
                app.skipDaemonOutputBacklog();   // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
                app.refreshNow();
            }
            // Handle DPI/display scale changes (e.g. window dragged to a
            // different-DPI monitor, or user changes Windows scaling)
            if (event.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED &&
                event.window.windowID == SDL_GetWindowID(window)) {
                float newScale = SDL_GetWindowDisplayScale(window);
                if (newScale <= 0.0f) newScale = 1.0f;
#ifdef __APPLE__
                newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
#endif
                winlog("SCALE_CHANGED(poll) reported newScale=%.3f", newScale);
                handleDisplayScaleChange(window, newScale, savedSizeForScale,
                                         lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
                                         dpiResizeRetries, dpiResizePending, dpiSettleFrames);
            }
        }

        // Check if window is minimized
        if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
            // Keep the wallet syncing while minimized: run the logic update (drains RPC results, ticks the
            // refresh scheduler, keeps the daemon connection/reconnect + sync status live) but skip the
            // ImGui frame + GPU present since nothing is visible. app.update() only reads
            // GetIO()/GetTime()/IsAnyItemActive() — all valid outside a frame — so it's safe without a
            // NewFrame; feed it a real-clock DeltaTime (NewFrame, which normally sets it, is skipped) and
            // let app.update() clamp it. Throttled to ~5 Hz so CPU stays near-idle (refresh cadences are
            // seconds-scale). shouldQuit is still checked so a quit request never hangs behind minimize.
            Uint64 nowMs = SDL_GetTicks();
            float minDelta = (minimizedLastTickMs == 0) ? 0.001f
                                                        : (float)(nowMs - minimizedLastTickMs) / 1000.0f;
            minimizedLastTickMs = nowMs;
            ImGui::GetIO().DeltaTime = (minDelta > 0.0f) ? minDelta : 0.001f;
            try {
                app.update();
            } catch (const std::exception& e) {
                DEBUG_LOGF("[Main] minimized app.update() threw: %s\n", e.what());
            } catch (...) {}
            if (app.shouldQuit()) running = false;
            SDL_Delay(200);
            continue;
        }
        minimizedLastTickMs = 0;   // visible again — reset the minimized clock

        // --- PerfLog: begin frame ---
        dragonx::util::PerfLog::instance().beginFrame();

        // Commit deferred font-scale change once scrolling has stopped.
        if (fontScaleCommitTick != 0 && SDL_GetTicks() >= fontScaleCommitTick) {
            fontScaleCommitTick = 0;
            float fs = dragonx::ui::Layout::userFontScale();
            dragonx::ui::Layout::setUserFontScale(fs);
            app.settings()->setFontScale(fs);
            app.settings()->save();
        }

        // Pre-frame: font atlas rebuilds and schema hot-reload must
        // happen BEFORE NewFrame() because NewFrame() caches font ptrs.
        app.preFrame();

        // Smooth font-scale: compensate visual font size via FontScaleMain
        // when the atlas hasn't been rebuilt yet (during slider drag).
        // After atlas rebuild, atlasScale == userFontScale so this is 1.0.
        {
            float userFS  = dragonx::ui::Layout::userFontScale();
            float atlasFS = dragonx::ui::Layout::fontAtlasScale();
            if (atlasFS > 0.001f)
                ImGui::GetStyle().FontScaleMain = userFS / atlasFS;
        }

        // If font scale changed, resize window proportionally.
        // Reference size (at scale 1.0) is updated on user-initiated resizes
        // so the next scale change is relative to the current window, not a
        // stale snapshot.  target = ref * curFS — no rounding drift.
        {
            float curFS = dragonx::ui::Layout::userFontScale();

            if (fontScaleRefFS < 0.001f) {
                // First frame — record reference, no resize.
                fontScaleRefFS = curFS;
                int w, h;
                SDL_GetWindowSize(window, &w, &h);
                fontScaleRefW = (int)lroundf((float)w / curFS);
                fontScaleRefH = (int)lroundf((float)h / curFS);
            }

            if (std::fabs(curFS - fontScaleRefFS) > 0.001f) {
                int newW = (int)lroundf((float)fontScaleRefW * curFS);
                int newH = (int)lroundf((float)fontScaleRefH * curFS);

                // Clamp to display work area
                SDL_DisplayID did = SDL_GetDisplayForWindow(window);
                if (did) {
                    SDL_Rect usable;
                    if (SDL_GetDisplayUsableBounds(did, &usable)) {
                        newW = std::min(newW, usable.w);
                        newH = std::min(newH, usable.h);
                    }
                }

                float hwDpi = dragonx::ui::Layout::rawDpiScale();
                SDL_SetWindowMinimumSize(window,
                    (int)(1024 * hwDpi),
                    (int)(720  * hwDpi));
                fontScaleResizePending = true;
                SDL_SetWindowSize(window, newW, newH);
                lastKnownW = newW;
                lastKnownH = newH;

                fontScaleRefFS = curFS;
            }
        }

        // Start the Dear ImGui frame
        PERF_BEGIN(_perfNewFrame);
#ifdef DRAGONX_USE_DX11
        ImGui_ImplDX11_NewFrame();
#else
        ImGui_ImplOpenGL3_NewFrame();
#endif
        ImGui_ImplSDL3_NewFrame();
        ImGui::NewFrame();
        PERF_END("NewFrame", _perfNewFrame);

        PERF_BEGIN(_perfBackdrop);
        // Draw background image overlay across the entire viewport.
        // Uses theme-specified texture stretched to fill, with the
        // programmatic gradient as fallback when texture isn't available.
        {
            ImGuiViewport* vp = ImGui::GetMainViewport();
            ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
            ImVec2 p0 = vp->Pos;
            ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
            drawWindowBackdrop(app, bgDL, p0, p1, backdrop_active,
                               dragonx::ui::effects::isLowSpecMode());
        }

        PERF_END("Backdrop", _perfBackdrop);

        // Begin acrylic frame (update viewport dimensions)
        // Skip in low-spec mode — acrylic is disabled, no need to track FBO sizes
        ImGuiIO& io = ImGui::GetIO();
        if (!dragonx::ui::effects::isLowSpecMode()) {
            dragonx::ui::effects::ImGuiAcrylic::BeginFrame(
                (int)io.DisplaySize.x, (int)io.DisplaySize.y);
        }

        // Update theme visual effects timing
        dragonx::ui::effects::ThemeEffects::instance().beginFrame();
        // Propagate window opacity to theme effects so they fade with the
        // backdrop while UI elements (cards, text, buttons) stay opaque.
        {
            float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
            dragonx::ui::effects::ThemeEffects::instance().setBackgroundOpacity(wo);
        }

        // Reset smooth-scroll animation flag — ApplySmoothScroll() calls
        // during this frame will set it back to true if still interpolating.
        dragonx::ui::material::SmoothScrollAnimating() = false;

        // Always render normal UI
        try {
            PERF_BEGIN(_perfUpdate);
            app.update();
            PERF_END("AppUpdate", _perfUpdate);
        } catch (const std::exception& e) {
            DEBUG_LOGF("[Main] app.update() threw: %s\n", e.what());
        } catch (...) {
            DEBUG_LOGF("[Main] app.update() threw unknown exception\n");
        }

        // Background capture is now handled by the draw callback
        // inserted into the BackgroundDrawList above.  It fires
        // during RenderDrawData() at exactly the right moment.

        ImGuiErrorRecoveryState erState;
        ImGui::ErrorRecoveryStoreState(&erState);
        try {
            PERF_BEGIN(_perfRender);
            app.render();
            PERF_END("AppRender", _perfRender);
        } catch (const std::exception& e) {
            DEBUG_LOGF("[Main] app.render() threw: %s\n", e.what());
            ImGui::ErrorRecoveryTryToRecoverState(&erState);
        } catch (...) {
            DEBUG_LOGF("[Main] app.render() threw unknown exception\n");
            ImGui::ErrorRecoveryTryToRecoverState(&erState);
        }

        // Draw shutdown overlay on top of everything when shutting down
        if (app.isShuttingDown()) {
            app.renderShutdownScreen();
        }

#ifdef DRAGONX_USE_DX11
        // Borderless title bar: window controls + drag handling
        renderBorderlessControls(window, nativeHwnd, app);
#endif

        // Global cursor: show hand cursor when hovering any clickable widget
        // (ImGui buttons don't change the cursor by default)
        // Persist cursor for 2 extra frames to avoid flicker on Windows
        // where g.HoveredId may clear briefly between frames.
        {
            ImGuiContext& g = *GImGui;
            static ImGuiMouseCursor s_prevCursor = ImGuiMouseCursor_Arrow;
            static int s_holdFrames = 0;

            bool isInputField = (g.InputTextState.ID != 0 &&
                                 g.InputTextState.ID == g.HoveredId);
            if (g.MouseCursor == ImGuiMouseCursor_Arrow && g.HoveredId != 0
                && !(g.HoveredIdIsDisabled) && !isInputField) {
                ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
                s_prevCursor = ImGuiMouseCursor_Hand;
                s_holdFrames = 2;
            } else if (s_holdFrames > 0 && g.MouseCursor == ImGuiMouseCursor_Arrow) {
                // Hold previous cursor to avoid single-frame revert flicker
                ImGui::SetMouseCursor(s_prevCursor);
                s_holdFrames--;
            } else {
                s_prevCursor = g.MouseCursor;
                s_holdFrames = 0;
            }
        }

        // Rendering
        PERF_BEGIN(_perfImRender);
        ImGui::Render();
        PERF_END("ImGui::Render", _perfImRender);

        // Apply cursor immediately via SDL so it takes effect this frame.
        // ImGui_ImplSDL3_UpdateMouseCursor() runs at the START of the
        // next frame, creating a 1-frame lag.  By calling SDL_SetCursor()
        // here we eliminate that delay and any flicker.
        {
            static SDL_Cursor* sdlCursors[ImGuiMouseCursor_COUNT] = {};
            static bool sdlCursorsInit = false;
            if (!sdlCursorsInit) {
                sdlCursors[ImGuiMouseCursor_Arrow]      = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);
                sdlCursors[ImGuiMouseCursor_TextInput]   = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_TEXT);
                sdlCursors[ImGuiMouseCursor_ResizeAll]   = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_MOVE);
                sdlCursors[ImGuiMouseCursor_ResizeNS]    = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NS_RESIZE);
                sdlCursors[ImGuiMouseCursor_ResizeEW]    = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_EW_RESIZE);
                sdlCursors[ImGuiMouseCursor_ResizeNESW]  = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NESW_RESIZE);
                sdlCursors[ImGuiMouseCursor_ResizeNWSE]  = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NWSE_RESIZE);
                sdlCursors[ImGuiMouseCursor_Hand]        = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_POINTER);
                sdlCursors[ImGuiMouseCursor_NotAllowed]  = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NOT_ALLOWED);
                sdlCursorsInit = true;
            }
            ImGuiMouseCursor cur = ImGui::GetMouseCursor();
            if (cur >= 0 && cur < ImGuiMouseCursor_COUNT && sdlCursors[cur])
                SDL_SetCursor(sdlCursors[cur]);
        }
        PERF_BEGIN(_perfPresent);
#ifdef DRAGONX_USE_DX11
        // Ensure swap chain matches current window size.
        // SDL_EVENT_WINDOW_RESIZED may be delayed during live resize
        // (Windows runs a modal loop while the user drags the border).
        dx.ensureSize();

        // DX11: Set render target and clear with transparent black for DWM
        ID3D11RenderTargetView* rtv = dx.renderTargetView();
        dx.deviceContext()->OMSetRenderTargets(1, &rtv, nullptr);
        {
            // When the background is fully opaque (low-spec mode or
            // window_opacity >= 1.0), clear with alpha 1.0 so the
            // compositor/DWM skips expensive blur compositing.
            float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
            bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f;
            bool effectiveBackdrop = (backdrop_active || app.getGradientTexture() != 0)
                                    && !opaqueBackground;
            dx.clear(0.0f, 0.0f, 0.0f, effectiveBackdrop ? 0.0f : 1.0f);
        }
        ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());

        // Debug screenshot sweep — grab the finished frame before present.
        if (app.wantsScreenshotThisFrame()) {
            captureFrameToPng(dx, app.screenshotSweepPath());
            app.onScreenshotCaptured();
        }

        // Update and Render additional Platform Windows (multi-viewport)
        if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
        {
            ImGui::UpdatePlatformWindows();
            ImGui::RenderPlatformWindowsDefault();
        }

        dx.present(1);
#else
        glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
        {
            // When the background is fully opaque (low-spec mode or
            // window_opacity >= 1.0), clear with alpha 1.0 so the
            // compositor skips alpha blending against the desktop.
            float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
            bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f;
            bool effectiveBackdrop = (backdrop_active || app.getGradientTexture() != 0)
                                    && !opaqueBackground;
            glClearColor(0.0f, 0.0f, 0.0f, effectiveBackdrop ? 0.0f : 1.0f);
        }
        glClear(GL_COLOR_BUFFER_BIT);
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        // Debug screenshot sweep — grab the finished frame before swap.
        if (app.wantsScreenshotThisFrame()) {
            captureFrameToPng(app.screenshotSweepPath(), (int)io.DisplaySize.x, (int)io.DisplaySize.y);
            app.onScreenshotCaptured();
        }

        // Update and Render additional Platform Windows (multi-viewport)
        if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
        {
            SDL_Window* backup_current_window = SDL_GL_GetCurrentWindow();
            SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext();
            ImGui::UpdatePlatformWindows();
            ImGui::RenderPlatformWindowsDefault();
            SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
        }

        // Uncap FPS during a screenshot sweep so it doesn't crawl at the compositor's (often
        // heavily throttled for an unfocused window) vsync rate. Restored when the sweep ends.
        {
            static bool sweepVsyncOff = false;
            const bool sweeping = app.isScreenshotSweeping();
            if (sweeping != sweepVsyncOff) {
                SDL_GL_SetSwapInterval(sweeping ? 0 : 1);
                sweepVsyncOff = sweeping;
            }
        }
        SDL_GL_SwapWindow(window);
#endif
        PERF_END("Present", _perfPresent);

        // --- PerfLog: end frame ---
        dragonx::util::PerfLog::instance().endFrame();

        // Headless verification hook: DRAGONX_FULL_SWEEP=1 runs the full UI sweep once the app has
        // settled, then quits when it finishes. Used to run the sweep without clicking the button.
        {
            static const bool autoSweep = std::getenv("DRAGONX_FULL_SWEEP") != nullptr;
            static int autoFrames = 0;
            static bool autoStarted = false;
            if (autoSweep) {
                if (!autoStarted) {
                    if (++autoFrames >= 90) { app.startFullUiSweep(); autoStarted = true; }
                } else if (!app.isScreenshotSweeping()) {
                    running = false;  // sweep complete
                }
            }
        }

        // Exit when shutdown is complete
        if (app.shouldQuit()) {
            running = false;
        }

        // Periodic log flush so crash diagnostics are visible
        {
            static auto lastFlush = std::chrono::steady_clock::now();
            auto now = std::chrono::steady_clock::now();
            if (std::chrono::duration_cast<std::chrono::seconds>(now - lastFlush).count() >= 5) {
                fflush(stdout);
                fflush(stderr);
                lastFlush = now;
            }
        }

        // Track last render for idle periodic timer
        lastPeriodicFrame = std::chrono::steady_clock::now();

        // P6: Determine if next frame needs redraw or can idle
        {
            ImGuiContext& g = *GImGui;

            // Detect actual user interaction — NOT WantCaptureMouse/Keyboard
            // which are just "ImGui has windows that accept input" (always true).
            bool mouseMoving = g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f;
            bool mouseClicked = false;
            for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++)
                mouseClicked |= g.IO.MouseDown[i];
            bool scrolling = g.IO.MouseWheel != 0.0f || g.IO.MouseWheelH != 0.0f;
            bool typing = g.IO.InputQueueCharacters.Size > 0;
            bool widgetActive = g.ActiveId != 0 || g.MovingWindow != nullptr;

            bool uiActive = mouseMoving || mouseClicked || scrolling
                         || typing || widgetActive;

            // When the background is fully opaque, backdrop doesn't
            // need continuous frames — let the app idle and save power.
            float wo2 = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
            bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo2 >= 0.99f;
            bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0)
                                      && !opaqueBackground;
            bool animating = app.isShuttingDown()
                          || app.isWalletSwitchInProgress()
                          || backdropNeedsFrames
                          || app.hasTransactionSendProgress()
                          || app.isTransactionRefreshInProgress()
                          || dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation()
                          || dragonx::ui::Notifications::instance().hasActive()
                          || dragonx::ui::material::SmoothScrollAnimating()
                          || dragonx::ui::ConsumeContactsAvatarAnimation();
            // If nothing is happening, allow the next iteration to idle
            needsRedraw = uiActive || animating;
        }
    }

    // Remove resize event watcher
    SDL_RemoveEventWatch(resizeWatcher, &resizeCtx);

    // Save window size to settings before shutdown
    if (app.settings()) {
        int curW = 0, curH = 0;
        SDL_GetWindowSize(window, &curW, &curH);
        float s = dragonx::ui::material::Typography::instance().getDpiScale();
        int logW = (int)lroundf((float)curW / s);
        int logH = (int)lroundf((float)curH / s);
        winlog("SHUTDOWN save: size=%dx%d storedScale=%.3f -> logical=%dx%d", curW, curH, s, logW, logH);
        if (logW >= 1024 && logH >= 720) {
            app.settings()->setWindowSize(logW, logH);
            app.settings()->save();
        } else {
            DEBUG_LOGF("Not saving window size %dx%d (logical, below 1024x720 minimum)\n", logW, logH);
        }
    }

    // Hide the window immediately so the user perceives the app as closed
    // while background cleanup (thread joins, RPC disconnect) continues.
    SDL_HideWindow(window);

    // Watchdog: if cleanup takes too long the process lingers without a
    // window, showing up as a "Background Service" in Task Manager.
    // Force-exit after timeout — all critical state (settings, daemon
    // stop) was handled in beginShutdown().
    // Allow enough time for the daemon to shut down gracefully (up to
    // 10s in stop()) plus RPC disconnect overhead.
    std::thread([]() {
        std::this_thread::sleep_for(std::chrono::seconds(15));
        fflush(stdout);
        fflush(stderr);
        _Exit(0);
    }).detach();

    // Final cleanup (daemon already stopped by beginShutdown)
    app.shutdown();
#ifdef DRAGONX_USE_DX11
    Shutdown(window, dx);
#else
    Shutdown(window, gl_context);
#endif

    // Explicitly release the single-instance lock before exit so a new
    // instance can start immediately.
    g_single_instance.unlock();

    // Force-terminate the process.  All important cleanup (daemon stop,
    // settings save, RPC disconnect, SDL teardown) has completed above.
    // On Windows with mingw-w64 POSIX threads, normal CRT cleanup
    // deadlocks waiting for detached pthreads.  On Linux, static
    // destructors and atexit handlers can also block.  _Exit() bypasses
    // all of that.
    app.wipeSecrets();   // _Exit() below bypasses ~App(), so scrub secret buffers here (L-05)
    fflush(stdout);
    fflush(stderr);
    _Exit(0);

    return 0;
}

static bool InitSDL()
{
    if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) {
        fprintf(stderr, "Error: SDL_Init(): %s\n", SDL_GetError());
        return false;
    }
    return true;
}

// Persistent storage for IniFilename — must outlive ImGui context
static std::string s_iniPath;

#ifdef DRAGONX_USE_DX11

static bool InitImGui(SDL_Window* window, dragonx::platform::DX11Context& dx)
{
    // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();

    // Save imgui.ini to ObsidianDragon config directory
    s_iniPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "imgui.ini").string();
    io.IniFilename = s_iniPath.c_str();
    
    // Enable keyboard navigation
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    // Enable gamepad navigation
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
    // Enable multi-viewport (windows can float outside main window)
    io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;

    // Setup Platform/Renderer backends
    ImGui_ImplSDL3_InitForD3D(window);
    ImGui_ImplDX11_Init(dx.device(), dx.deviceContext());

    // Query display DPI scale (e.g. 1.5 for 150% Windows scaling)
    float dpiScale = SDL_GetWindowDisplayScale(window);
    if (dpiScale <= 0.0f) dpiScale = 1.0f;
    DEBUG_LOGF("Display scale: %.2f\n", dpiScale);

    // Load Material Design typography system with DPI awareness
    if (!dragonx::ui::material::Typography::instance().load(io, dpiScale)) {
        DEBUG_LOGF("Warning: Could not load typography system, using defaults\n");
        io.Fonts->AddFontDefault();
    }

    return true;
}

static void Shutdown(SDL_Window* window, dragonx::platform::DX11Context& dx)
{
    ImGui_ImplDX11_Shutdown();
    ImGui_ImplSDL3_Shutdown();
    ImGui::DestroyContext();

    dx.shutdown();

    // Save native HWND before SDL releases its reference.
    // SDL marks externally-provided HWNDs as SDL_WINDOW_EXTERNAL and
    // does NOT call DestroyWindow -- we must do it ourselves.
    HWND hwnd = nullptr;
    if (window) {
        hwnd = (HWND)SDL_GetPointerProperty(
            SDL_GetWindowProperties(window),
            SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
        SDL_DestroyWindow(window);
    }
    if (hwnd) {
        DestroyWindow(hwnd);
    }
    SDL_Quit();
}

#else // OpenGL path

static bool InitImGui(SDL_Window* window, SDL_GLContext gl_context)
{
    // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();

    // Save imgui.ini to ObsidianDragon config directory
    s_iniPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "imgui.ini").string();
    io.IniFilename = s_iniPath.c_str();
    
    // Enable keyboard navigation
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
    // Enable gamepad navigation
    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
    // Enable multi-viewport (windows can float outside main window)
    io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;

    // Setup Platform/Renderer backends
    ImGui_ImplSDL3_InitForOpenGL(window, gl_context);
#ifdef __APPLE__
    ImGui_ImplOpenGL3_Init("#version 150");
#else
    ImGui_ImplOpenGL3_Init("#version 130");
#endif

    // Query display DPI scale (e.g. 1.5 for 150% scaling)
    float dpiScale = SDL_GetWindowDisplayScale(window);
    if (dpiScale <= 0.0f) dpiScale = 1.0f;
#ifdef __APPLE__
    // On macOS with SDL_WINDOW_HIGH_PIXEL_DENSITY, ImGui handles Retina
    // resolution automatically via io.DisplayFramebufferScale.  Window
    // coordinates are already in logical points, so we must NOT also
    // scale fonts and style sizes by the content scale — that would
    // double everything.
    dpiScale = 1.0f;
#endif
    DEBUG_LOGF("Display scale: %.2f\n", dpiScale);

    // Load Material Design typography system with DPI awareness
    // This loads Ubuntu fonts in Light, Regular, and Medium weights
    // at all sizes needed for the type scale (H1-H6, Body1-2, etc.)
    if (!dragonx::ui::material::Typography::instance().load(io, dpiScale)) {
        DEBUG_LOGF("Warning: Could not load typography system, using defaults\n");
        io.Fonts->AddFontDefault();
    }

    return true;
}

static void Shutdown(SDL_Window* window, SDL_GLContext gl_context)
{
    // Shutdown acrylic effects before destroying GL context
    dragonx::ui::effects::ImGuiAcrylic::Shutdown();
    
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplSDL3_Shutdown();
    ImGui::DestroyContext();

    if (gl_context) {
        SDL_GL_DestroyContext(gl_context);
    }
    if (window) {
        SDL_DestroyWindow(window);
    }
    SDL_Quit();
}

#endif // DRAGONX_USE_DX11
