// 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/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 "resources/embedded_resources.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 <cstdlib>
#include <cmath>
#include <filesystem>
#include <string>
#include <chrono>
#include "util/logger.h"

#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 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);
        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
static dragonx::util::SingleInstance g_single_instance("obsidiandragon");

// 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 "";
}

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::error_code ec;
        std::filesystem::create_directories(odDir, ec);
    }

#ifdef _WIN32
    // Redirect stdout/stderr to a log file so diagnostic output is visible
    // even when built as a GUI app (WIN32_EXECUTABLE hides the console).
    {
        std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.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);

    // Check for existing instance
    if (!g_single_instance.tryLock()) {
        fprintf(stderr, "Another instance of ObsidianDragon is already running.\n");
        DEBUG_LOGF("Please close the existing instance first.\n");
#ifdef _WIN32
        MessageBoxW(nullptr, L"Another instance of ObsidianDragon is already running.\nPlease close it first.",
                    L"ObsidianDragon", 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);
            }
        }
    }

    // ---------------------------------------------------------------
    // 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__
    if (currentDpiScale > 1.01f) {
        int curW = 0, curH = 0;
        SDL_GetWindowSize(window, &curW, &curH);
        int newW = (int)(curW * currentDpiScale);
        int newH = (int)(curH * currentDpiScale);

        // Clamp to the display's work area so the window fits on screen.
        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);
            }
        }

        SDL_SetWindowSize(window, newW, newH);
        SDL_SetWindowMinimumSize(window,
            (int)(1024 * currentDpiScale),
            (int)(720  * currentDpiScale));
        SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
        DEBUG_LOGF("HiDPI startup: window resized %dx%d -> %dx%d (scale %.2f)\n",
               curW, curH, newW, newH, currentDpiScale);
    }
#endif

    // 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);
            ImTextureID curGradTex2 = ctx->app->getGradientTexture();
            float winOp2 = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;

            // Scale only background layer alpha by window opacity so the
            // desktop shows through while UI elements stay fully opaque.
            auto scaleA2 = [&](ImU32 col) -> ImU32 {
                uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
                a = (uint8_t)(a * winOp2);
                return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
            };

            if (curGradTex2 != 0) {
                const auto& S1 = dragonx::ui::schema::UI();
                ImU32 baseTop1    = S1.resolveColor(S1.drawElement("backdrop", "base-color-top").color,    IM_COL32(18,28,65,200));
                ImU32 baseBottom1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
                int tintAlpha1    = (int)S1.drawElement("backdrop", "texture-tint-alpha").size;
                if (tintAlpha1 <= 0) tintAlpha1 = 140;
                bgDL->AddRectFilledMultiColor(p0, p1, scaleA2(baseTop1), scaleA2(baseTop1),
                                              scaleA2(baseBottom1), scaleA2(baseBottom1));
                int scaledTA1 = (int)(tintAlpha1 * winOp2);
                ImU32 tintCol1 = IM_COL32(255, 255, 255, scaledTA1);
                bgDL->AddImage(curGradTex2, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol1);
            } else if (ctx->backdrop_active) {
                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) * winOp2));
                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) * winOp2));
                bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
            }
            // Acrylic capture callback — must fire BEFORE the noise
            // overlay so the noise grain is not captured and blurred
            // into the glass cards.
            auto captureCb2 = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
            if (captureCb2) {
                bgDL->AddCallback(captureCb2, 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 are scaled so the desktop shows through while UI
            // elements (cards, text, buttons) remain fully readable.
            if (ctx->backdrop_active || curGradTex2 != 0) {
                const auto& S4 = dragonx::ui::schema::UI();
                float baseBgAlpha2 = S4.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
                ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha2;
            }

        }

        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;
    {
        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;
                SDL_SetWindowSize(window, dpiTargetW, dpiTargetH);
                DEBUG_LOGF("  Deferred resize retry %d: %dx%d -> %dx%d\n",
                       dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
            } else {
                // Size matches — done
                dpiResizeRetries = 0;
            }
            --dpiResizeRetries;
            needsRedraw = true;
        }

        // 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 = dpiResizePending ||
                                      std::abs(actualScale - storedScale) > 0.01f;
                    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.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
                    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 (idle): %.2f -> %.2f (ratio %.2f)\n",
                               oldScale, newScale, ratio);
                        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);
                        }

                        // 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;
                        } 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);
                        }

                        // 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);
                            }
                        }

                        dpiResizePending = true;
                        SDL_SetWindowSize(window, newW, newH);
                        lastKnownW = newW;
                        lastKnownH = newH;
                        // Arm deferred retry — the SetWindowSize above may
                        // not stick if the user is mid-drag (modal loop).
                        dpiTargetW = newW;
                        dpiTargetH = newH;
                        dpiResizeRetries = 30; // retry for ~30 frames
                        SDL_SetWindowMinimumSize(window,
                            (int)(1024 * newScale), (int)(720 * newScale));

                        typo.reload(io, newScale);
                        ImGui::GetStyle() = ImGuiStyle();
                        dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
                        if (newScale > 1.01f) {
                            ImGui::GetStyle().ScaleAllSizes(newScale);
                        }
                    }
                }
                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();
            }
#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 = dpiResizePending ||
                                   std::abs(actualScale - storedScale) > 0.01f;
                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.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
                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);
                    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);
                    }

                    // 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;
                    } 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);
                    }

                    // 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);
                        }
                    }

                    dpiResizePending = true;
                    SDL_SetWindowSize(window, newW, newH);
                    lastKnownW = newW;
                    lastKnownH = newH;
                    // Arm deferred retry — the SetWindowSize above may
                    // not stick if the user is mid-drag (modal loop).
                    dpiTargetW = newW;
                    dpiTargetH = newH;
                    dpiResizeRetries = 30; // retry for ~30 frames
                    SDL_SetWindowMinimumSize(window,
                        (int)(1024 * newScale),
                        (int)(720  * newScale));
                    DEBUG_LOGF("  Window resized: %dx%d (scale %.2f, pct %d)\n",
                           newW, newH, newScale, newPct);

                    // 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");
                }
            }
        }

        // Check if window is minimized
        if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
            // Still check shouldQuit while minimized to avoid hang
            if (app.shouldQuit()) {
                running = false;
            }
            SDL_Delay(10);
            continue;
        }

        // --- 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);

            // 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;

            bool lowSpec = dragonx::ui::effects::isLowSpecMode();

            // 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;
            }

        }

        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());

        // 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());

        // 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);
        }

        SDL_GL_SwapWindow(window);
#endif
        PERF_END("Present", _perfPresent);

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

        // 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()
                          || backdropNeedsFrames
                          || app.hasTransactionSendProgress()
                          || app.isTransactionRefreshInProgress()
                          || dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation()
                          || dragonx::ui::Notifications::instance().hasActive()
                          || dragonx::ui::material::SmoothScrollAnimating();
            // 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);
        if (logW >= 1024 && logH >= 720) {
            app.settings()->setWindowSize(logW, logH);
            app.settings()->save();
        }
    }

    // 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.
    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
