#include "WidgetManager.hpp"
#include "widgets/WidgetFactory.hpp"
#include "widgets/RoomCardWidgetBase.hpp"
#include "widgets/RoomCardTileWidget.hpp"
#include "HistoryStore.hpp"
#include "SdCard.hpp"
#include "Gui.hpp"
#include "esp_lv_adapter.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "cJSON.h"
#include <memory>
#include <new>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <sys/stat.h>
#include <cstdlib>

static const char* TAG = "WidgetMgr";
static constexpr uint8_t SCREEN_ID_NONE = 0xFF;

#if LV_USE_OBJ_NAME
static void dump_flex_objects(lv_obj_t* obj, uint8_t depth) {
    if (!obj) return;
    if (lv_obj_get_style_layout(obj, LV_PART_MAIN) == LV_LAYOUT_FLEX) {
        char name[64];
        lv_obj_get_name_resolved(obj, name, sizeof(name));
        ESP_LOGI("FlexMap", "flex obj=%p name=%s depth=%u", obj, name, depth);
    }
    uint32_t count = lv_obj_get_child_count(obj);
    for (uint32_t i = 0; i < count; ++i) {
        dump_flex_objects(lv_obj_get_child(obj, static_cast<int32_t>(i)), depth + 1);
    }
}
#endif

static bool is_valid_utf8(const char* text, size_t len) {
    size_t i = 0;
    while (i < len) {
        uint8_t c = static_cast<uint8_t>(text[i]);
        if (c < 0x80) {
            i++;
            continue;
        }
        if ((c & 0xE0) == 0xC0) {
            if (i + 1 >= len) return false;
            uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
            if ((c1 & 0xC0) != 0x80) return false;
            i += 2;
            continue;
        }
        if ((c & 0xF0) == 0xE0) {
            if (i + 2 >= len) return false;
            uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
            uint8_t c2 = static_cast<uint8_t>(text[i + 2]);
            if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80) return false;
            i += 3;
            continue;
        }
        if ((c & 0xF8) == 0xF0) {
            if (i + 3 >= len) return false;
            uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
            uint8_t c2 = static_cast<uint8_t>(text[i + 2]);
            uint8_t c3 = static_cast<uint8_t>(text[i + 3]);
            if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80) return false;
            i += 4;
            continue;
        }
        return false;
    }
    return true;
}

static void latin1_to_utf8(const char* src, size_t src_len, char* dst, size_t dst_size) {
    if (dst_size == 0) return;
    size_t di = 0;
    for (size_t si = 0; si < src_len; ++si) {
        uint8_t c = static_cast<uint8_t>(src[si]);
        if (c < 0x80) {
            if (di + 1 >= dst_size) break;
            dst[di++] = static_cast<char>(c);
        } else {
            if (di + 2 >= dst_size) break;
            dst[di++] = static_cast<char>(0xC0 | (c >> 6));
            dst[di++] = static_cast<char>(0x80 | (c & 0x3F));
        }
    }
    dst[di] = '\0';
}

static bool color_equals(const Color& a, const Color& b) {
    return a.r == b.r && a.g == b.g && a.b == b.b;
}

static bool is_auto_button_label(const WidgetConfig& button, const WidgetConfig& label) {
    if (label.type != WidgetType::LABEL) return false;
    if (label.parentId < 0 || label.parentId != static_cast<int8_t>(button.id)) return false;
    if (label.textSource != TextSource::STATIC) return false;
    bool textMatches = (strncmp(label.text, button.text, MAX_TEXT_LEN) == 0);
    if (!textMatches && button.text[0] == '\0') {
        if (strncmp(label.text, "Button", MAX_TEXT_LEN) == 0 || label.text[0] == '\0') {
            textMatches = true;
        }
    }
    if (!textMatches) return false;
    if (label.fontSize != button.fontSize) return false;
    if (label.textAlign != button.textAlign) return false;
    if (!color_equals(label.textColor, button.textColor)) return false;
    if (label.bgOpacity != 0) return false;
    if (label.borderRadius != 0) return false;
    if (label.shadow.enabled) return false;
    if (label.iconCodepoint != button.iconCodepoint) return false;
    if (label.iconPosition != button.iconPosition) return false;
    if (label.iconSize != button.iconSize) return false;
    if (label.iconGap != button.iconGap) return false;
    if (label.x != 0 || label.y != 0) return false;
    return true;
}

static void normalizeButtons(ScreenConfig& screen) {
    uint8_t removeIds[MAX_WIDGETS] = {};
    uint8_t removeCount = 0;

    // Remove legacy auto-generated label children for buttons
    for (uint8_t i = 0; i < screen.widgetCount; i++) {
        const WidgetConfig& w = screen.widgets[i];
        if (w.type != WidgetType::LABEL || w.parentId < 0) continue;

        WidgetConfig* parent = screen.findWidget(static_cast<uint8_t>(w.parentId));
        if (!parent || parent->type != WidgetType::BUTTON) continue;

        bool forceRemove = parent->text[0] != '\0';
        if (forceRemove || is_auto_button_label(*parent, w)) {
            if (removeCount < MAX_WIDGETS) {
                removeIds[removeCount++] = w.id;
            }
        }
    }

    for (uint8_t i = 0; i < removeCount; i++) {
        screen.removeWidget(removeIds[i]);
    }

    // Update container flag based on remaining children
    for (uint8_t i = 0; i < screen.widgetCount; i++) {
        WidgetConfig& w = screen.widgets[i];
        if (w.type != WidgetType::BUTTON) continue;

        bool hasChild = false;
        for (uint8_t j = 0; j < screen.widgetCount; j++) {
            const WidgetConfig& child = screen.widgets[j];
            if (child.parentId >= 0 && child.parentId == static_cast<int8_t>(w.id)) {
                hasChild = true;
                break;
            }
        }
        w.isContainer = hasChild;
    }
}

// WidgetManager implementation
WidgetManager& WidgetManager::instance() {
    static WidgetManager inst;
    return inst;
}

WidgetManager::WidgetManager() {
    // Allocate GuiConfig in PSRAM to save internal RAM
    config_ = static_cast<GuiConfig*>(heap_caps_malloc(sizeof(GuiConfig), MALLOC_CAP_SPIRAM));
    if (!config_) {
        ESP_LOGE(TAG, "Failed to allocate GuiConfig in PSRAM, trying internal RAM");
        config_ = new GuiConfig();
    } else {
        new (config_) GuiConfig();  // Placement new to call constructor
    }

    // widgets_ is default-initialized to nullptr
    portMUX_INITIALIZE(&knxCacheMux_);
    uiQueue_ = xQueueCreate(UI_EVENT_QUEUE_LEN, sizeof(UiEvent));
    if (!uiQueue_) {
        ESP_LOGE(TAG, "Failed to create UI event queue");
    }
    createDefaultConfig();
    activeScreenId_ = config_->startScreenId;
    lastActivityUs_ = esp_timer_get_time();
}

void WidgetManager::createDefaultConfig() {
    config_->clear();
    config_->screenCount = 1;
    ScreenConfig& screen = config_->screens[0];
    screen.clear(0, "Screen 1");

    // Default: Temperature label
    auto tempLabel = WidgetConfig::createKnxLabel(0, 50, 20,
        TextSource::KNX_DPT_TEMP, 1, "%.1f °C");
    tempLabel.fontSize = 3; // 28pt
    screen.addWidget(tempLabel);

    // Default: KNX Prog button
    auto progBtn = WidgetConfig::createButton(1, 50, 100, "KNX Prog", 0, true);
    progBtn.bgColor = {200, 50, 50}; // Red
    screen.addWidget(progBtn);

    normalizeButtons(screen);

    config_->startScreenId = screen.id;
    config_->standbyEnabled = false;
    config_->standbyScreenId = 0xFF;
    config_->standbyMinutes = 0;
    activeScreenId_ = screen.id;
}

void WidgetManager::init() {
    loadFromSdCard();
    HistoryStore::instance().configureFromConfig(*config_);
    HistoryStore::instance().loadFromSdCard();
    if (config_->findScreen(config_->startScreenId)) {
        activeScreenId_ = config_->startScreenId;
    } else if (config_->screenCount > 0) {
        activeScreenId_ = config_->screens[0].id;
    } else {
        activeScreenId_ = 0;
    }
    lastActivityUs_ = esp_timer_get_time();
    ESP_LOGI(TAG, "WidgetManager initialized with %d screens", config_->screenCount);
}

void WidgetManager::loadFromSdCard() {
    if (!SdCard::instance().isMounted()) {
        ESP_LOGI(TAG, "SD card not mounted, using defaults");
        return;
    }

    FILE* f = fopen(CONFIG_FILE, "r");
    if (!f) {
        ESP_LOGI(TAG, "No config file found, using defaults");
        return;
    }

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);

    if (size <= 0 || size > 32768) {
        ESP_LOGE(TAG, "Invalid config file size: %ld", size);
        fclose(f);
        return;
    }

    char* json = new char[size + 1];
    if (!json) {
        ESP_LOGE(TAG, "Failed to allocate memory for config");
        fclose(f);
        return;
    }

    size_t read = fread(json, 1, size, f);
    fclose(f);
    json[read] = '\0';

    bool success = updateConfigFromJson(json);
    delete[] json;

    if (success) {
        ESP_LOGI(TAG, "Loaded %d screens from SD card", config_->screenCount);
    } else {
        ESP_LOGE(TAG, "Failed to parse config file");
    }
}

void WidgetManager::saveToSdCard() {
    if (!SdCard::instance().isMounted()) {
        ESP_LOGE(TAG, "SD card not mounted, cannot save config");
        return;
    }

    char* json = new char[32768];
    if (!json) {
        ESP_LOGE(TAG, "Failed to allocate memory for JSON");
        return;
    }

    getConfigJson(json, 32768);

    FILE* f = fopen(CONFIG_FILE, "w");
    if (!f) {
        ESP_LOGE(TAG, "Failed to open config file for writing");
        delete[] json;
        return;
    }

    size_t written = fwrite(json, 1, strlen(json), f);
    fclose(f);
    delete[] json;

    if (written > 0) {
        ESP_LOGI(TAG, "Saved %d screens to SD card", config_->screenCount);
    } else {
        ESP_LOGE(TAG, "Failed to write config file");
    }
}

void WidgetManager::applyConfig() {
    HistoryStore::instance().configureFromConfig(*config_);
    if (!config_->findScreen(activeScreenId_)) {
        if (config_->findScreen(config_->startScreenId)) {
            activeScreenId_ = config_->startScreenId;
        } else if (config_->screenCount > 0) {
            activeScreenId_ = config_->screens[0].id;
        }
    }
    applyScreen(activeScreenId_);
}

void WidgetManager::saveAndApply() {
    saveToSdCard();
    applyConfig();
    ESP_LOGI(TAG, "Config saved and applied");
}

void WidgetManager::resetToDefaults() {
    closeModal();
    standbyActive_ = false;
    standbyWakePending_ = false;
    standbyReturnScreenId_ = SCREEN_ID_NONE;
    previousScreenId_ = SCREEN_ID_NONE;
    createDefaultConfig();
    saveAndApply();
    ESP_LOGI(TAG, "Reset to defaults");
}

ScreenConfig* WidgetManager::activeScreen() {
    if (modalContainer_ && modalScreenId_ != SCREEN_ID_NONE) {
        return config_->findScreen(modalScreenId_);
    }
    return config_->findScreen(activeScreenId_);
}

const ScreenConfig* WidgetManager::activeScreen() const {
    if (modalContainer_ && modalScreenId_ != SCREEN_ID_NONE) {
        return config_->findScreen(modalScreenId_);
    }
    return config_->findScreen(activeScreenId_);
}

const ScreenConfig* WidgetManager::currentScreen() const {
    return activeScreen();
}

void WidgetManager::applyScreen(uint8_t screenId) {
    if (esp_lv_adapter_lock(-1) != ESP_OK) {
        ESP_LOGE(TAG, "Failed to acquire LVGL lock!");
        return;
    }
    applyScreenLocked(screenId);
    esp_lv_adapter_unlock();
}

// Helper to convert ScreenAnimType to LVGL animation type (LVGL 9.x API)
static lv_screen_load_anim_t screenAnimToLvgl(ScreenAnimType type) {
    switch (type) {
        case ScreenAnimType::NONE:        return LV_SCREEN_LOAD_ANIM_NONE;
        case ScreenAnimType::FADE:        return LV_SCREEN_LOAD_ANIM_FADE_IN;
        case ScreenAnimType::SLIDE_LEFT:  return LV_SCREEN_LOAD_ANIM_MOVE_LEFT;
        case ScreenAnimType::SLIDE_RIGHT: return LV_SCREEN_LOAD_ANIM_MOVE_RIGHT;
        case ScreenAnimType::SLIDE_UP:    return LV_SCREEN_LOAD_ANIM_MOVE_TOP;
        case ScreenAnimType::SLIDE_DOWN:  return LV_SCREEN_LOAD_ANIM_MOVE_BOTTOM;
        case ScreenAnimType::OVER_LEFT:   return LV_SCREEN_LOAD_ANIM_OVER_LEFT;
        case ScreenAnimType::OVER_RIGHT:  return LV_SCREEN_LOAD_ANIM_OVER_RIGHT;
        case ScreenAnimType::OVER_UP:     return LV_SCREEN_LOAD_ANIM_OVER_TOP;
        case ScreenAnimType::OVER_DOWN:   return LV_SCREEN_LOAD_ANIM_OVER_BOTTOM;
        default:                          return LV_SCREEN_LOAD_ANIM_FADE_IN;
    }
}

void WidgetManager::applyScreenLocked(uint8_t screenId) {
    ESP_LOGI(TAG, "applyScreen(%d) - start", screenId);

    ScreenConfig* screen = config_->findScreen(screenId);
    if (!screen) {
        ESP_LOGW(TAG, "Screen %d not found", screenId);
        return;
    }

    normalizeButtons(*screen);

    if (modalContainer_) {
        ESP_LOGI(TAG, "Closing modal first");
        closeModalLocked();
    }

    // SAFE DESTRUCTION of C++ widgets:
    // 1. Mark all C++ widgets as "LVGL object already gone"
    for (auto& widget : widgets_) {
        if (widget) widget->clearLvglObject();
    }
    // 2. Clean layer_top (sub-buttons etc)
    lv_obj_clean(lv_layer_top());
    // 3. Destroy C++ objects (their destructors won't call lv_obj_delete)
    destroyAllWidgets();

    // Create a NEW screen object for the transition
    lv_obj_t* newScreen = lv_obj_create(NULL);
    lv_obj_remove_style_all(newScreen);
    lv_obj_set_size(newScreen, LV_PCT(100), LV_PCT(100));
    lv_obj_clear_flag(newScreen, LV_OBJ_FLAG_SCROLLABLE);

    ESP_LOGI(TAG, "Creating new widgets for screen '%s' (%d widgets)...",
             screen->name, screen->widgetCount);
    createAllWidgets(*screen, newScreen);
    ESP_LOGI(TAG, "Widgets created");
    applyCachedValuesToWidgets();

    // Get animation settings
    ESP_LOGI(TAG, "Animation config: type=%d, duration=%u",
             (int)config_->screenAnimType, config_->screenAnimDuration);
    lv_screen_load_anim_t animType = screenAnimToLvgl(config_->screenAnimType);
    uint32_t animDuration = config_->screenAnimDuration > 0 ? config_->screenAnimDuration : 300;

    ESP_LOGI(TAG, "Loading screen with animation: lvgl_type=%d, duration=%lums",
             (int)animType, (unsigned long)animDuration);

    // Load the new screen with animation (old screen auto-deleted after animation)
    // Parameters: new_scr, anim_type, time_ms, delay_ms, auto_del_old_scr
    lv_screen_load_anim(newScreen, animType, animDuration, 0, true);

    ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId);
}

void WidgetManager::showModalScreen(const ScreenConfig& screen) {
    if (esp_lv_adapter_lock(-1) != ESP_OK) return;
    showModalScreenLocked(screen);
    esp_lv_adapter_unlock();
}

void WidgetManager::showModalScreenLocked(const ScreenConfig& screen) {
    if (modalContainer_) {
        closeModalLocked();
    }

    // SAFE DESTRUCTION
    for (auto& widget : widgets_) {
        if (widget) widget->clearLvglObject();
    }
    lv_obj_clean(lv_scr_act()); // Should we clean screen when showing modal? 
    // Actually, usually modal is ON TOP of screen. 
    // But our current WidgetManager destroys screen widgets when showing modal!
    // That's why we clean.
    lv_obj_clean(lv_layer_top());

    destroyAllWidgets();

    lv_disp_t* disp = lv_disp_get_default();
    int32_t dispWidth = disp ? lv_disp_get_hor_res(disp) : 1280;
    int32_t dispHeight = disp ? lv_disp_get_ver_res(disp) : 800;

    // Create semi-transparent background overlay if dimming enabled
    lv_obj_t* dimmer = nullptr;
    if (screen.modalDimBackground) {
        dimmer = lv_obj_create(lv_layer_top());
        if (dimmer) {
            lv_obj_remove_style_all(dimmer);
            lv_obj_set_size(dimmer, dispWidth, dispHeight);
            lv_obj_set_style_bg_color(dimmer, lv_color_black(), 0);
            lv_obj_set_style_bg_opa(dimmer, LV_OPA_50, 0);
            lv_obj_clear_flag(dimmer, LV_OBJ_FLAG_SCROLLABLE);
        }
    }

    // Create modal container
    lv_obj_t* modal = lv_obj_create(lv_layer_top());
    lv_obj_clear_flag(modal, LV_OBJ_FLAG_SCROLLABLE);

    // Calculate modal size
    int32_t modalWidth = screen.modalWidth;
    int32_t modalHeight = screen.modalHeight;

    // Auto-size: calculate from widget bounds if not specified
    if (modalWidth <= 0 || modalHeight <= 0) {
        int32_t maxX = 0, maxY = 0;
        for (uint8_t i = 0; i < screen.widgetCount; i++) {
            const WidgetConfig& w = screen.widgets[i];
            if (w.visible) {
                int32_t right = w.x + w.width;
                int32_t bottom = w.y + w.height;
                if (right > maxX) maxX = right;
                if (bottom > maxY) maxY = bottom;
            }
        }
        if (modalWidth <= 0) modalWidth = maxX + 40;  // Add padding
        if (modalHeight <= 0) modalHeight = maxY + 40;
    }

    lv_obj_set_size(modal, modalWidth, modalHeight);

    // Position modal (0 = centered)
    if (screen.modalX == 0 && screen.modalY == 0) {
        lv_obj_center(modal);
    } else {
        lv_obj_set_pos(modal, screen.modalX, screen.modalY);
    }

    // Style modal
    lv_obj_set_style_bg_color(modal, lv_color_make(
        screen.backgroundColor.r,
        screen.backgroundColor.g,
        screen.backgroundColor.b), 0);
    lv_obj_set_style_bg_opa(modal, LV_OPA_COVER, 0);
    lv_obj_set_style_radius(modal, screen.modalBorderRadius, 0);
    lv_obj_set_style_border_width(modal, 0, 0);
    lv_obj_set_style_pad_all(modal, 0, 0);

    // Add shadow for modal
    lv_obj_set_style_shadow_color(modal, lv_color_black(), 0);
    lv_obj_set_style_shadow_opa(modal, LV_OPA_30, 0);
    lv_obj_set_style_shadow_width(modal, 20, 0);
    lv_obj_set_style_shadow_spread(modal, 5, 0);

    modalContainer_ = modal;
    modalDimmer_ = dimmer;
    modalScreenId_ = screen.id;

    // Create widgets inside modal (not on full screen)
    screen_ = modal;
    for (uint8_t i = 0; i < screen.widgetCount; i++) {
        const WidgetConfig& cfg = screen.widgets[i];
        auto widget = WidgetFactory::create(cfg);
        if (widget && cfg.id < 256) {
            widget->create(modal);
            widget->applyStyle();
            widgets_[cfg.id] = std::move(widget);
        }
    }

    applyCachedValuesToWidgets();

    ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight);
}

void WidgetManager::closeModal() {
    if (esp_lv_adapter_lock(-1) != ESP_OK) return;
    closeModalLocked();
    esp_lv_adapter_unlock();
}

void WidgetManager::closeModalLocked() {
    printf("WM: closeModal Start. Container=%p\n", (void*)modalContainer_);
    fflush(stdout);
    if (!modalContainer_) {
        return;
    }

    // SAFE DESTRUCTION
    for (auto& widget : widgets_) {
        if (widget) widget->clearLvglObject();
    }
    lv_obj_clean(lv_layer_top());

    // Destroy widgets first
    printf("WM: closeModal destroying widgets...\n");
    fflush(stdout);
    destroyAllWidgets();

    modalDimmer_ = nullptr;
    modalContainer_ = nullptr;
    modalScreenId_ = SCREEN_ID_NONE;
    
    printf("WM: closeModal Complete\n");
    fflush(stdout);
}

void WidgetManager::showScreenLocked(uint8_t screenId) {
    ESP_LOGI(TAG, "showScreen(%d) called", screenId);

    ScreenConfig* screen = config_->findScreen(screenId);
    if (!screen) {
        ESP_LOGW(TAG, "Screen %d not found", screenId);
        return;
    }

    ESP_LOGI(TAG, "Found screen '%s', mode=%d", screen->name,
             static_cast<int>(screen->mode));

    if (screen->mode == ScreenMode::MODAL) {
        showModalScreenLocked(*screen);
        return;
    }

    previousScreenId_ = activeScreenId_;
    activeScreenId_ = screen->id;
    standbyActive_ = false;
    applyScreenLocked(activeScreenId_);
}

void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) {
    printf("WM: handleButtonAction btn=%d act=%d type=%d\n", cfg.id, (int)cfg.action, (int)cfg.type);
    fflush(stdout);

    if (cfg.type != WidgetType::BUTTON && cfg.type != WidgetType::BUTTONMATRIX) {
        printf("WM: Not a clickable widget!\n");
        fflush(stdout);
        return;
    }

    onUserActivity();

    switch (cfg.action) {
        case ButtonAction::JUMP:
            printf("WM: Action JUMP to %d\n", cfg.targetScreen);
            fflush(stdout);
            navAction_ = ButtonAction::JUMP;
            navTargetScreen_ = cfg.targetScreen;
            navPending_ = true;
            navRequestUs_ = esp_timer_get_time();
            break;
        case ButtonAction::BACK:
            printf("WM: Action BACK\n");
            fflush(stdout);
            navAction_ = ButtonAction::BACK;
            navTargetScreen_ = SCREEN_ID_NONE;
            navPending_ = true;
            navRequestUs_ = esp_timer_get_time();
            break;
        case ButtonAction::KNX:
        default: {
            if (cfg.knxAddressWrite > 0) {
                bool currentState = false;
                if (target) {
                    currentState = (lv_obj_get_state(target) & LV_STATE_CHECKED) != 0;
                }

                // For toggle buttons: send the opposite of current state
                // For non-toggle buttons: send ON (true)
                bool sendValue = cfg.isToggle ? !currentState : true;

                ESP_LOGI(TAG, "Button %d clicked, KNX write to %d, value=%d (toggle=%d, currentState=%d)",
                         cfg.id, cfg.knxAddressWrite, sendValue, cfg.isToggle, currentState);

                // Send KNX telegram
                if (Gui::knxWorker.sendSwitch(cfg.knxAddressWrite, sendValue)) {
                    // Update local UI state immediately for responsive feedback
                    if (cfg.isToggle && target) {
                        if (sendValue) {
                            lv_obj_add_state(target, LV_STATE_CHECKED);
                        } else {
                            lv_obj_clear_state(target, LV_STATE_CHECKED);
                        }
                    }
                    // Also update the cache
                    cacheKnxSwitch(cfg.knxAddressWrite, sendValue);
                }
            }
            break;
        }
    }
}

void WidgetManager::goBackLocked() {
    printf("WM: goBack called. Modal=%p Active=%d Prev=%d\n", 
           (void*)modalContainer_, activeScreenId_, previousScreenId_);
    fflush(stdout);

    if (modalContainer_) {
        printf("WM: Closing modal...\n");
        fflush(stdout);
        closeModalLocked();
        printf("WM: Modal closed. Restoring screen %d\n", activeScreenId_);
        fflush(stdout);
        // Restore the active screen (which was in background)
        if (config_->findScreen(activeScreenId_)) {
            applyScreenLocked(activeScreenId_);
        } else {
            ESP_LOGE(TAG, "Active screen %d not found after closing modal!", activeScreenId_);
            if (config_->findScreen(config_->startScreenId)) {
                activeScreenId_ = config_->startScreenId;
                applyScreenLocked(activeScreenId_);
            }
        }
        return;
    }

    if (previousScreenId_ != SCREEN_ID_NONE && previousScreenId_ != activeScreenId_) {
        printf("WM: Going back to screen %d\n", previousScreenId_);
        fflush(stdout);
        if (config_->findScreen(previousScreenId_)) {
            activeScreenId_ = previousScreenId_;
            previousScreenId_ = SCREEN_ID_NONE;
            applyScreenLocked(activeScreenId_);
        } else {
             ESP_LOGW(TAG, "Previous screen %d not found", previousScreenId_);
             previousScreenId_ = SCREEN_ID_NONE;
        }
    } else {
        printf("WM: No previous screen to go back to\n");
        fflush(stdout);
    }
}

void WidgetManager::goBack() {
    if (esp_lv_adapter_lock(-1) != ESP_OK) return;
    goBackLocked();
    esp_lv_adapter_unlock();
}

void WidgetManager::navigateToScreen(uint8_t screenId) {
    navAction_ = ButtonAction::JUMP;
    navTargetScreen_ = screenId;
    navPending_ = true;
    navRequestUs_ = esp_timer_get_time();
}

void WidgetManager::navigateBack() {
    navAction_ = ButtonAction::BACK;
    navTargetScreen_ = SCREEN_ID_NONE;
    navPending_ = true;
    navRequestUs_ = esp_timer_get_time();
}

void WidgetManager::sendKnxSwitch(uint16_t groupAddr, bool value) {
    ESP_LOGI(TAG, "sendKnxSwitch: GA=%d, value=%d", groupAddr, value);

    // Send actual KNX telegram via KnxWorker
    if (Gui::knxWorker.sendSwitch(groupAddr, value)) {
        // Update cache so UI reflects the change
        cacheKnxSwitch(groupAddr, value);
    }
}

void WidgetManager::enterStandby() {
    if (!config_->standbyEnabled || config_->standbyMinutes == 0) return;
    if (standbyActive_) return;
    if (config_->standbyScreenId == SCREEN_ID_NONE) return;

    ScreenConfig* standbyScreen = config_->findScreen(config_->standbyScreenId);
    if (!standbyScreen) return;

    standbyReturnScreenId_ = activeScreenId_;
    standbyActive_ = true;
    activeScreenId_ = standbyScreen->id;
    applyScreenLocked(activeScreenId_);
}

void WidgetManager::loop() {
    /*static uint32_t loopCount = 0;
    loopCount++;
    if (loopCount % 40 == 0) {
        ESP_LOGI(TAG, "Heap: %lu | Internal: %lu", 
            esp_get_free_heap_size(),
            heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
    }*/

    bool didUiNav = false;
    if (navPending_) {
        int64_t now = esp_timer_get_time();
        if (now - navRequestUs_ >= NAV_DELAY_US) {
            navPending_ = false;
            printf("WM: [TRACE] Nav start\n"); fflush(stdout);
            if (navAction_ == ButtonAction::JUMP) {
                showScreenLocked(navTargetScreen_);
            } else if (navAction_ == ButtonAction::BACK) {
                goBackLocked();
            }
            didUiNav = true;
            printf("WM: [TRACE] Nav end\n"); fflush(stdout);
        }
    }

    // printf("WM: [TRACE] Queue start\n"); fflush(stdout);
    processUiQueue();
    // printf("WM: [TRACE] Queue end\n"); fflush(stdout);

    // printf("WM: [TRACE] Tick start\n"); fflush(stdout);
    if (HistoryStore::instance().tick()) {
        refreshChartWidgets();
    }
    // printf("WM: [TRACE] Tick end\n"); fflush(stdout);

    // printf("WM: [TRACE] Time start\n"); fflush(stdout);
    updateSystemTimeWidgets();
    // printf("WM: [TRACE] Time end\n"); fflush(stdout);

    if (didUiNav) return;

    if (!config_->standbyEnabled || config_->standbyMinutes == 0) return;
    if (standbyActive_) return;
    if (config_->standbyScreenId == SCREEN_ID_NONE) return;

    int64_t now = esp_timer_get_time();
    int64_t idleUs = now - lastActivityUs_;
    int64_t timeoutUs = static_cast<int64_t>(config_->standbyMinutes) * 60 * 1000000LL;
    if (idleUs >= timeoutUs) {
        enterStandby();
    }
}

void WidgetManager::onUserActivity() {
    lastActivityUs_ = esp_timer_get_time();
    if (standbyActive_) {
        standbyActive_ = false;
        uint8_t returnId = standbyReturnScreenId_;
        if (returnId == SCREEN_ID_NONE) {
            returnId = config_->startScreenId;
        }
        standbyWakeTarget_ = returnId;
        standbyWakePending_ = true;
    }
}

void WidgetManager::destroyAllWidgets() {
    ESP_LOGI(TAG, "destroyAllWidgets: Start (%d widgets)", widgets_.size());
    // Destroy in reverse order (last created first)
    for (int i = static_cast<int>(widgets_.size()) - 1; i >= 0; i--) {
        if (widgets_[i]) {
            ESP_LOGD(TAG, "Destroying widget %d", i);
            widgets_[i].reset();
        }
    }
    ESP_LOGI(TAG, "destroyAllWidgets: Complete");
}

void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent) {
    screen_ = parent;

    lv_obj_set_style_bg_color(parent, lv_color_make(
        screen.backgroundColor.r,
        screen.backgroundColor.g,
        screen.backgroundColor.b), 0);
    lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0);

    // Background image (if set)
    // Note: Requires LV_USE_FS_POSIX=y in sdkconfig with LV_FS_POSIX_LETTER='S' (83)
    if (screen.bgImagePath[0] != '\0') {
        char fullPath[64];
        snprintf(fullPath, sizeof(fullPath), "S:/sdcard%s", screen.bgImagePath);

        // Check if file exists, try uppercase IMAGES as fallback
        struct stat st;
        char checkPath[64];
        snprintf(checkPath, sizeof(checkPath), "/sdcard%s", screen.bgImagePath);
        if (stat(checkPath, &st) != 0) {
            // Try uppercase /IMAGES/ instead of /images/
            if (strncmp(screen.bgImagePath, "/images/", 8) == 0) {
                snprintf(fullPath, sizeof(fullPath), "S:/sdcard/IMAGES%s", screen.bgImagePath + 7);
                ESP_LOGI(TAG, "Trying uppercase path: %s", fullPath);
            }
        }
        ESP_LOGI(TAG, "Loading background image: %s", fullPath);

        lv_obj_t* bgImg = lv_image_create(parent);
        lv_image_set_src(bgImg, fullPath);

        // Get image dimensions
        lv_image_header_t header;
        lv_result_t res = lv_image_decoder_get_info(fullPath, &header);

        if (res == LV_RESULT_OK && header.w > 0 && header.h > 0) {
            ESP_LOGI(TAG, "Image size: %dx%d", header.w, header.h);

            // Get display/canvas size
            int32_t dispW = lv_obj_get_width(parent);
            int32_t dispH = lv_obj_get_height(parent);
            if (dispW <= 0) dispW = LV_HOR_RES;
            if (dispH <= 0) dispH = LV_VER_RES;

            switch (screen.bgImageMode) {
                case BgImageMode::STRETCH: {
                    // Calculate scale to fill display (in 1/256 units for LVGL)
                    int32_t scaleX = (dispW * 256) / header.w;
                    int32_t scaleY = (dispH * 256) / header.h;
                    lv_image_set_scale_x(bgImg, scaleX);
                    lv_image_set_scale_y(bgImg, scaleY);
                    lv_obj_set_pos(bgImg, 0, 0);
                    ESP_LOGI(TAG, "Stretch scale: %ldx%ld", scaleX, scaleY);
                    break;
                }
                case BgImageMode::CENTER:
                    lv_obj_center(bgImg);
                    break;
                case BgImageMode::TILE:
                    lv_image_set_inner_align(bgImg, LV_IMAGE_ALIGN_TILE);
                    lv_obj_set_size(bgImg, lv_pct(100), lv_pct(100));
                    break;
                default:
                    break;
            }
        } else {
            ESP_LOGW(TAG, "Could not get image info, using default position");
            lv_obj_set_pos(bgImg, 0, 0);
        }

        // Send to background (behind all widgets)
        lv_obj_move_to_index(bgImg, 0);
    }

    // Pass 1: Create root widgets (parentId == -1)
    for (uint8_t i = 0; i < screen.widgetCount; i++) {
        const WidgetConfig& cfg = screen.widgets[i];
        if (cfg.parentId != -1) continue;

        auto widget = WidgetFactory::create(cfg);
        if (widget && cfg.id < 256) {
            widget->create(parent);
            widget->applyStyle();
            widgets_[cfg.id] = std::move(widget);
        }
    }

    // Pass 2: Create child widgets
    // Simple 1-level depth support for now. For deeper nesting, we'd need a topological sort or multiple passes.
    bool madeProgress = true;
    int remainingPasses = 10; // Prevent infinite loops
    
    while (madeProgress && remainingPasses > 0) {
        madeProgress = false;
        remainingPasses--;
        
        for (uint8_t i = 0; i < screen.widgetCount; i++) {
            const WidgetConfig& cfg = screen.widgets[i];
            
            // Skip if already created
            if (cfg.id < 256 && widgets_[cfg.id]) continue;
            
            // Skip if it's a root widget (should be created in Pass 1, but if failed/skipped, ignore)
            if (cfg.parentId == -1) continue;
            
            // Check if parent exists
            if (cfg.parentId >= 0 && cfg.parentId < 256 && widgets_[cfg.parentId]) {
                // Parent exists! Get its LVGL object
                lv_obj_t* parentObj = widgets_[cfg.parentId]->getObj();
                if (parentObj) {
                    auto widget = WidgetFactory::create(cfg);
                    if (widget && cfg.id < 256) {
                        widget->create(parentObj);
                        widget->applyStyle();
                        widgets_[cfg.id] = std::move(widget);
                        madeProgress = true;
                    }
                }
            }
        }
    }
}

void WidgetManager::onKnxValue(uint16_t groupAddr, float value, TextSource source) {
    UiEvent event = {};
    event.type = UiEventType::KNX_VALUE;
    event.groupAddr = groupAddr;
    event.textSource = source;
    event.value = value;
    cacheKnxValue(groupAddr, source, value);
    enqueueUiEvent(event);
}

void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
    onKnxValue(groupAddr, value, TextSource::KNX_DPT_TEMP);
}

void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
    UiEvent event = {};
    event.type = UiEventType::KNX_SWITCH;
    event.groupAddr = groupAddr;
    event.state = value;
    cacheKnxSwitch(groupAddr, value);
    enqueueUiEvent(event);
}

void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) {
    UiEvent event = {};
    event.type = UiEventType::KNX_TEXT;
    event.groupAddr = groupAddr;
    if (text) {
        const size_t max_len = sizeof(event.text) - 1;
        const size_t src_len = strnlen(text, max_len);
        if (is_valid_utf8(text, src_len)) {
            memcpy(event.text, text, src_len);
            event.text[src_len] = '\0';
        } else {
            latin1_to_utf8(text, src_len, event.text, sizeof(event.text));
        }
    } else {
        event.text[0] = '\0';
    }
    cacheKnxText(groupAddr, event.text);
    enqueueUiEvent(event);
}

void WidgetManager::onKnxTime(uint16_t groupAddr, const struct tm& value, KnxTimeType type) {
    UiEvent event = {};
    event.type = UiEventType::KNX_TIME;
    event.groupAddr = groupAddr;
    event.timeType = type;
    event.timeValue = value;
    cacheKnxTime(groupAddr, type, value);
    enqueueUiEvent(event);
}

bool WidgetManager::enqueueUiEvent(const UiEvent& event) {
    if (!uiQueue_) return false;
    return xQueueSend(uiQueue_, &event, 0) == pdTRUE;
}

void WidgetManager::processUiQueue() {
    if (!uiQueue_) return;
    if (uxQueueMessagesWaiting(uiQueue_) == 0) return;

    // Call from loop() which is ALREADY LOCKED by LVGL timer
    // DO NOT take esp_lv_adapter_lock() here.

    UiEvent event = {};
    static constexpr size_t kMaxEventsPerLoop = 8;
    size_t processed = 0;
    while (processed < kMaxEventsPerLoop &&
           xQueueReceive(uiQueue_, &event, 0) == pdTRUE) {
        switch (event.type) {
            case UiEventType::KNX_VALUE:
                applyKnxValue(event.groupAddr, event.value, event.textSource);
                break;
            case UiEventType::KNX_SWITCH:
                applyKnxSwitch(event.groupAddr, event.state);
                break;
            case UiEventType::KNX_TEXT:
                applyKnxText(event.groupAddr, event.text);
                break;
            case UiEventType::KNX_TIME:
                applyKnxTime(event.groupAddr, event.timeValue, event.timeType);
                break;
        }
        processed++;
    }

    if (chartRefreshPending_) {
        refreshChartWidgetsLocked();
        chartRefreshPending_ = false;
    }
}

void WidgetManager::applyCachedValuesToWidgets() {
    for (auto& widget : widgets_) {
        if (!widget) continue;

        // Primary address
        uint16_t addr = widget->getKnxAddress();
        TextSource source = widget->getTextSource();

        if (addr != 0 && source != TextSource::STATIC) {
            if (source == TextSource::KNX_DPT_TIME ||
                source == TextSource::KNX_DPT_DATE ||
                source == TextSource::KNX_DPT_DATETIME) {
                KnxTimeType type = KnxTimeType::TIME;
                if (source == TextSource::KNX_DPT_DATE) {
                    type = KnxTimeType::DATE;
                } else if (source == TextSource::KNX_DPT_DATETIME) {
                    type = KnxTimeType::DATETIME;
                }
                struct tm tmValue = {};
                if (getCachedKnxTime(addr, type, &tmValue)) {
                    widget->onKnxTime(tmValue, source);
                }
            } else if (source == TextSource::KNX_DPT_SWITCH) {
                bool state = false;
                if (getCachedKnxSwitch(addr, &state)) {
                    widget->onKnxSwitch(state);
                }
            } else if (source == TextSource::KNX_DPT_TEXT) {
                char text[MAX_TEXT_LEN] = {};
                if (getCachedKnxText(addr, text, sizeof(text))) {
                    widget->onKnxText(text);
                }
            } else if (isNumericTextSource(source)) {
                float value = 0.0f;
                if (getCachedKnxValue(addr, source, &value)) {
                    widget->onKnxValue(value);
                }
            }
        }

        if (widget->getType() == WidgetType::BUTTON) {
            bool state = false;
            if (addr != 0 && getCachedKnxSwitch(addr, &state)) {
                widget->onKnxSwitch(state);
            }
        }

        // Secondary address (left value)
        uint16_t addr2 = widget->getKnxAddress2();
        TextSource source2 = widget->getTextSource2();
        if (addr2 != 0 && source2 != TextSource::STATIC && isNumericTextSource(source2)) {
            float value = 0.0f;
            if (getCachedKnxValue(addr2, source2, &value)) {
                widget->onKnxValue2(value);
            }
        }

        // Tertiary address (right value)
        uint16_t addr3 = widget->getKnxAddress3();
        TextSource source3 = widget->getTextSource3();
        if (addr3 != 0 && source3 != TextSource::STATIC && isNumericTextSource(source3)) {
            float value = 0.0f;
            if (getCachedKnxValue(addr3, source3, &value)) {
                widget->onKnxValue3(value);
            }
        }

        // RoomCard text lines (only for Tile style)
        if (widget->getType() == WidgetType::ROOMCARD) {
            const WidgetConfig& cfg = widget->getConfig();
            if (cfg.cardStyle == 1 && cfg.textLineCount > 0) {
                for (uint8_t i = 0; i < cfg.textLineCount && i < MAX_TEXTLINES; ++i) {
                    const TextLineConfig& tl = cfg.textLines[i];
                    if (tl.enabled && tl.knxAddr != 0 && tl.textSrc != TextSource::STATIC && isNumericTextSource(tl.textSrc)) {
                        float value = 0.0f;
                        if (getCachedKnxValue(tl.knxAddr, tl.textSrc, &value)) {
                            static_cast<RoomCardTileWidget*>(widget.get())->onTextLineValue(i, value);
                        }
                    }
                }
            }
        }
    }
}

void WidgetManager::refreshChartWidgetsLocked() {
    for (auto& widget : widgets_) {
        if (!widget) continue;
        if (widget->getType() == WidgetType::CHART) {
            widget->onHistoryUpdate();
        }
    }
}

void WidgetManager::refreshChartWidgets() {
    // Call from loop() which is ALREADY LOCKED by LVGL timer
    // DO NOT take esp_lv_adapter_lock() here.
    refreshChartWidgetsLocked();
}

void WidgetManager::applyNightMode(bool night) {
    // Update screen background
    const ScreenConfig* screen = activeScreen();
    if (screen && screen_) {
        Color bg = night ? screen->nightBackgroundColor : screen->backgroundColor;
        lv_obj_set_style_bg_color(screen_, lv_color_make(bg.r, bg.g, bg.b), 0);
    }

    // Re-apply style to all widgets (they check nightMode_ internally)
    for (auto& widget : widgets_) {
        if (widget) {
            widget->applyStyle();
        }
    }
}

void WidgetManager::updateSystemTimeWidgets() {
    int64_t nowUs = esp_timer_get_time();
    if (nowUs - lastSystemTimeUpdateUs_ < 500000) { // Update every 500ms
        return;
    }
    lastSystemTimeUpdateUs_ = nowUs;

    time_t now;
    time(&now);
    struct tm timeinfo;
    localtime_r(&now, &timeinfo);

    // Call from loop() which is ALREADY LOCKED by LVGL timer
    for (auto& widget : widgets_) {
        if (!widget) continue;
        TextSource src = widget->getTextSource();
        if (src == TextSource::SYSTEM_TIME || 
            src == TextSource::SYSTEM_DATE || 
            src == TextSource::SYSTEM_DATETIME) {
            widget->onKnxTime(timeinfo, src);
        }
    }
}

void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) {
    for (auto& widget : widgets_) {
        if (!widget) continue;

        // Primary address (bottom value)
        if (widget->getKnxAddress() == groupAddr &&
            widget->getTextSource() == source) {
            widget->onKnxValue(value);
        }

        // Secondary address (left value)
        if (widget->getKnxAddress2() == groupAddr &&
            widget->getTextSource2() == source) {
            widget->onKnxValue2(value);
        }

        // Tertiary address (right value)
        if (widget->getKnxAddress3() == groupAddr &&
            widget->getTextSource3() == source) {
            widget->onKnxValue3(value);
        }

        // RoomCard text line updates (only for Tile style)
        if (widget->getType() == WidgetType::ROOMCARD) {
            const WidgetConfig& cfg = widget->getConfig();
            if (cfg.cardStyle == 1 && cfg.textLineCount > 0) {
                for (uint8_t i = 0; i < cfg.textLineCount && i < MAX_TEXTLINES; ++i) {
                    const TextLineConfig& tl = cfg.textLines[i];
                    if (tl.enabled && tl.knxAddr == groupAddr && tl.textSrc == source) {
                        static_cast<RoomCardTileWidget*>(widget.get())->onTextLineValue(i, value);
                    }
                }
            }
        }
    }

    if (HistoryStore::instance().updateLatest(groupAddr, source, value)) {
        chartRefreshPending_ = true;
    }
}

void WidgetManager::applyKnxSwitch(uint16_t groupAddr, bool value) {
    for (auto& widget : widgets_) {
        if (!widget) continue;

        if (widget->getKnxAddress() == groupAddr) {
            widget->onKnxSwitch(value);
        }

        // RoomCard sub-button status updates
        if (widget->getType() == WidgetType::ROOMCARD) {
            const WidgetConfig& cfg = widget->getConfig();
            for (uint8_t i = 0; i < cfg.subButtonCount && i < MAX_SUBBUTTONS; ++i) {
                if (cfg.subButtons[i].enabled && cfg.subButtons[i].knxAddrRead == groupAddr) {
                    static_cast<RoomCardWidgetBase*>(widget.get())->onSubButtonStatus(i, value);
                }
            }
        }
    }

    if (config_->knxNightModeAddress != 0 && groupAddr == config_->knxNightModeAddress) {
        if (nightMode_ != value) {
            nightMode_ = value;
            applyNightMode(value);
        }
    }
}

void WidgetManager::applyKnxText(uint16_t groupAddr, const char* text) {
    for (auto& widget : widgets_) {
        if (widget && widget->getKnxAddress() == groupAddr) {
            widget->onKnxText(text);
        }
    }
}

void WidgetManager::applyKnxTime(uint16_t groupAddr, const struct tm& value, KnxTimeType type) {
    // Simplified system time synchronization
    bool isGlobalTime = false;
    if (type == KnxTimeType::TIME && config_->knxTimeAddress != 0 && groupAddr == config_->knxTimeAddress) isGlobalTime = true;
    if (type == KnxTimeType::DATE && config_->knxDateAddress != 0 && groupAddr == config_->knxDateAddress) isGlobalTime = true;
    if (type == KnxTimeType::DATETIME && config_->knxDateTimeAddress != 0 && groupAddr == config_->knxDateTimeAddress) isGlobalTime = true;

    if (isGlobalTime) {
        time_t now;
        time(&now);
        struct tm t_new;
        localtime_r(&now, &t_new);

        if (type == KnxTimeType::TIME) {
            t_new.tm_hour = value.tm_hour;
            t_new.tm_min = value.tm_min;
            t_new.tm_sec = value.tm_sec;
        } else if (type == KnxTimeType::DATE) {
            t_new.tm_year = value.tm_year;
            if (t_new.tm_year < 1900) t_new.tm_year += 1900;
            t_new.tm_mon = value.tm_mon;
            t_new.tm_mday = value.tm_mday;
        } else if (type == KnxTimeType::DATETIME) {
            t_new = value;
            if (t_new.tm_year < 1900) t_new.tm_year += 1900;
        }
        
        // Final normalization for mktime (expects year - 1900)
        if (t_new.tm_year >= 1900) t_new.tm_year -= 1900;

        time_t t = mktime(&t_new);
        if (t != -1) {
            struct timeval tv = { .tv_sec = t, .tv_usec = 0 };
            settimeofday(&tv, NULL);
        }
    }

    TextSource source = TextSource::STATIC;
    if (type == KnxTimeType::TIME) source = TextSource::KNX_DPT_TIME;
    else if (type == KnxTimeType::DATE) source = TextSource::KNX_DPT_DATE;
    else if (type == KnxTimeType::DATETIME) source = TextSource::KNX_DPT_DATETIME;

    if (source != TextSource::STATIC) {
        for (auto& widget : widgets_) {
            if (widget && widget->getKnxAddress() == groupAddr &&
                widget->getTextSource() == source) {
                widget->onKnxTime(value, source);
            }
        }
    }

    chartRefreshPending_ = true;
}

void WidgetManager::cacheKnxValue(uint16_t groupAddr, TextSource source, float value) {
    if (groupAddr == 0) return;
    portENTER_CRITICAL(&knxCacheMux_);

    size_t freeIndex = KNX_CACHE_SIZE;
    for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) {
        auto& entry = knxNumericCache_[i];
        if (entry.valid) {
            if (entry.groupAddr == groupAddr && entry.source == source) {
                entry.value = value;
                portEXIT_CRITICAL(&knxCacheMux_);
                return;
            }
        } else if (freeIndex == KNX_CACHE_SIZE) {
            freeIndex = i;
        }
    }

    size_t index = freeIndex;
    if (index == KNX_CACHE_SIZE) {
        index = knxNumericCacheNext_;
        knxNumericCacheNext_ = (knxNumericCacheNext_ + 1) % KNX_CACHE_SIZE;
    }
    auto& entry = knxNumericCache_[index];
    entry.groupAddr = groupAddr;
    entry.source = source;
    entry.value = value;
    entry.valid = true;
    portEXIT_CRITICAL(&knxCacheMux_);
}

void WidgetManager::cacheKnxSwitch(uint16_t groupAddr, bool value) {
    if (groupAddr == 0) return;
    portENTER_CRITICAL(&knxCacheMux_);

    size_t freeIndex = KNX_CACHE_SIZE;
    for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) {
        auto& entry = knxSwitchCache_[i];
        if (entry.valid) {
            if (entry.groupAddr == groupAddr) {
                entry.value = value;
                portEXIT_CRITICAL(&knxCacheMux_);
                return;
            }
        } else if (freeIndex == KNX_CACHE_SIZE) {
            freeIndex = i;
        }
    }

    size_t index = freeIndex;
    if (index == KNX_CACHE_SIZE) {
        index = knxSwitchCacheNext_;
        knxSwitchCacheNext_ = (knxSwitchCacheNext_ + 1) % KNX_CACHE_SIZE;
    }
    auto& entry = knxSwitchCache_[index];
    entry.groupAddr = groupAddr;
    entry.value = value;
    entry.valid = true;
    portEXIT_CRITICAL(&knxCacheMux_);
}

void WidgetManager::cacheKnxText(uint16_t groupAddr, const char* text) {
    if (groupAddr == 0) return;
    portENTER_CRITICAL(&knxCacheMux_);

    size_t freeIndex = KNX_CACHE_SIZE;
    for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) {
        auto& entry = knxTextCache_[i];
        if (entry.valid) {
            if (entry.groupAddr == groupAddr) {
                if (text) {
                    strncpy(entry.text, text, MAX_TEXT_LEN - 1);
                    entry.text[MAX_TEXT_LEN - 1] = '\0';
                } else {
                    entry.text[0] = '\0';
                }
                portEXIT_CRITICAL(&knxCacheMux_);
                return;
            }
        } else if (freeIndex == KNX_CACHE_SIZE) {
            freeIndex = i;
        }
    }

    size_t index = freeIndex;
    if (index == KNX_CACHE_SIZE) {
        index = knxTextCacheNext_;
        knxTextCacheNext_ = (knxTextCacheNext_ + 1) % KNX_CACHE_SIZE;
    }
    auto& entry = knxTextCache_[index];
    entry.groupAddr = groupAddr;
    if (text) {
        strncpy(entry.text, text, MAX_TEXT_LEN - 1);
        entry.text[MAX_TEXT_LEN - 1] = '\0';
    } else {
        entry.text[0] = '\0';
    }
    entry.valid = true;
    portEXIT_CRITICAL(&knxCacheMux_);
}

void WidgetManager::cacheKnxTime(uint16_t groupAddr, KnxTimeType type, const struct tm& value) {
    if (groupAddr == 0) return;
    portENTER_CRITICAL(&knxCacheMux_);

    size_t freeIndex = KNX_CACHE_SIZE;
    for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) {
        auto& entry = knxTimeCache_[i];
        if (entry.valid) {
            if (entry.groupAddr == groupAddr && entry.type == type) {
                entry.value = value;
                portEXIT_CRITICAL(&knxCacheMux_);
                return;
            }
        } else if (freeIndex == KNX_CACHE_SIZE) {
            freeIndex = i;
        }
    }

    size_t index = freeIndex;
    if (index == KNX_CACHE_SIZE) {
        index = knxTimeCacheNext_;
        knxTimeCacheNext_ = (knxTimeCacheNext_ + 1) % KNX_CACHE_SIZE;
    }
    auto& entry = knxTimeCache_[index];
    entry.groupAddr = groupAddr;
    entry.type = type;
    entry.value = value;
    entry.valid = true;
    portEXIT_CRITICAL(&knxCacheMux_);
}

bool WidgetManager::getCachedKnxValue(uint16_t groupAddr, TextSource source, float* out) const {
    if (groupAddr == 0 || out == nullptr) return false;
    bool found = false;
    portENTER_CRITICAL(&knxCacheMux_);
    for (const auto& entry : knxNumericCache_) {
        if (entry.valid && entry.groupAddr == groupAddr && entry.source == source) {
            *out = entry.value;
            found = true;
            break;
        }
    }
    portEXIT_CRITICAL(&knxCacheMux_);
    return found;
}

bool WidgetManager::getCachedKnxSwitch(uint16_t groupAddr, bool* out) const {
    if (groupAddr == 0 || out == nullptr) return false;
    bool found = false;
    portENTER_CRITICAL(&knxCacheMux_);
    for (const auto& entry : knxSwitchCache_) {
        if (entry.valid && entry.groupAddr == groupAddr) {
            *out = entry.value;
            found = true;
            break;
        }
    }
    portEXIT_CRITICAL(&knxCacheMux_);
    return found;
}

bool WidgetManager::getCachedKnxText(uint16_t groupAddr, char* out, size_t outSize) const {
    if (groupAddr == 0 || out == nullptr || outSize == 0) return false;
    bool found = false;
    portENTER_CRITICAL(&knxCacheMux_);
    for (const auto& entry : knxTextCache_) {
        if (entry.valid && entry.groupAddr == groupAddr) {
            strncpy(out, entry.text, outSize - 1);
            out[outSize - 1] = '\0';
            found = true;
            break;
        }
    }
    portEXIT_CRITICAL(&knxCacheMux_);
    return found;
}

bool WidgetManager::getCachedKnxTime(uint16_t groupAddr, KnxTimeType type, struct tm* out) const {
    if (groupAddr == 0 || out == nullptr) return false;
    bool found = false;
    portENTER_CRITICAL(&knxCacheMux_);
    for (const auto& entry : knxTimeCache_) {
        if (entry.valid && entry.groupAddr == groupAddr && entry.type == type) {
            *out = entry.value;
            found = true;
            break;
        }
    }
    portEXIT_CRITICAL(&knxCacheMux_);
    return found;
}

bool WidgetManager::isNumericTextSource(TextSource source) {
    return source == TextSource::KNX_DPT_TEMP ||
           source == TextSource::KNX_DPT_PERCENT ||
           source == TextSource::KNX_DPT_POWER ||
           source == TextSource::KNX_DPT_ENERGY ||
           source == TextSource::KNX_DPT_DECIMALFACTOR;
}

// Helper function to parse hex color string
static uint32_t parseHexColor(const char* colorStr) {
    if (!colorStr || colorStr[0] != '#') return 0;
    return strtoul(colorStr + 1, nullptr, 16);
}

static Color defaultChartColor(size_t index) {
    static const Color kChartColors[CHART_MAX_SERIES] = {
        {239, 99, 81},
        {125, 211, 176},
        {94, 162, 239}
    };
    if (index >= CHART_MAX_SERIES) return kChartColors[0];
    return kChartColors[index];
}

void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
    cJSON* root = cJSON_CreateObject();
    if (!root) {
        snprintf(buf, bufSize, "{}");
        return;
    }

    cJSON_AddNumberToObject(root, "startScreen", config_->startScreenId);

    cJSON* standby = cJSON_AddObjectToObject(root, "standby");
    cJSON_AddBoolToObject(standby, "enabled", config_->standbyEnabled);
    cJSON_AddNumberToObject(standby, "screen", config_->standbyScreenId);
    cJSON_AddNumberToObject(standby, "minutes", config_->standbyMinutes);

    cJSON* knx = cJSON_AddObjectToObject(root, "knx");
    cJSON_AddNumberToObject(knx, "time", config_->knxTimeAddress);
    cJSON_AddNumberToObject(knx, "date", config_->knxDateAddress);
    cJSON_AddNumberToObject(knx, "dateTime", config_->knxDateTimeAddress);
    cJSON_AddNumberToObject(knx, "night", config_->knxNightModeAddress);

    // Screen transition animation
    cJSON* anim = cJSON_AddObjectToObject(root, "screenAnim");
    cJSON_AddNumberToObject(anim, "type", static_cast<int>(config_->screenAnimType));
    cJSON_AddNumberToObject(anim, "duration", config_->screenAnimDuration);

    cJSON* screens = cJSON_AddArrayToObject(root, "screens");

    for (uint8_t s = 0; s < config_->screenCount; s++) {
        const ScreenConfig& screen = config_->screens[s];
        cJSON* screenJson = cJSON_CreateObject();

        cJSON_AddNumberToObject(screenJson, "id", screen.id);
        cJSON_AddStringToObject(screenJson, "name", screen.name);
        cJSON_AddNumberToObject(screenJson, "mode", static_cast<int>(screen.mode));

        char bgColorStr[8];
        snprintf(bgColorStr, sizeof(bgColorStr), "#%02X%02X%02X",
                 screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b);
        cJSON_AddStringToObject(screenJson, "bgColor", bgColorStr);

        char nightBgColorStr[8];
        snprintf(nightBgColorStr, sizeof(nightBgColorStr), "#%02X%02X%02X",
                 screen.nightBackgroundColor.r, screen.nightBackgroundColor.g, screen.nightBackgroundColor.b);
        cJSON_AddStringToObject(screenJson, "nightBgColor", nightBgColorStr);

        // Background image
        if (screen.bgImagePath[0] != '\0') {
            cJSON_AddStringToObject(screenJson, "bgImage", screen.bgImagePath);
            cJSON_AddNumberToObject(screenJson, "bgImageMode", static_cast<int>(screen.bgImageMode));
        }

        // Modal-specific properties
        if (screen.mode == ScreenMode::MODAL) {
            cJSON* modal = cJSON_AddObjectToObject(screenJson, "modal");
            cJSON_AddNumberToObject(modal, "x", screen.modalX);
            cJSON_AddNumberToObject(modal, "y", screen.modalY);
            cJSON_AddNumberToObject(modal, "w", screen.modalWidth);
            cJSON_AddNumberToObject(modal, "h", screen.modalHeight);
            cJSON_AddNumberToObject(modal, "radius", screen.modalBorderRadius);
            cJSON_AddBoolToObject(modal, "dim", screen.modalDimBackground);
        }

        cJSON* widgets = cJSON_AddArrayToObject(screenJson, "widgets");
        for (uint8_t i = 0; i < screen.widgetCount; i++) {
            const WidgetConfig& w = screen.widgets[i];
            cJSON* widget = cJSON_CreateObject();

            cJSON_AddNumberToObject(widget, "id", w.id);
            cJSON_AddNumberToObject(widget, "type", static_cast<int>(w.type));
            cJSON_AddNumberToObject(widget, "x", w.x);
            cJSON_AddNumberToObject(widget, "y", w.y);
            cJSON_AddNumberToObject(widget, "w", w.width);
            cJSON_AddNumberToObject(widget, "h", w.height);
            cJSON_AddBoolToObject(widget, "visible", w.visible);
            cJSON_AddNumberToObject(widget, "textSrc", static_cast<int>(w.textSource));
            cJSON_AddStringToObject(widget, "text", w.text);
            cJSON_AddNumberToObject(widget, "knxAddr", w.knxAddress);
            cJSON_AddNumberToObject(widget, "fontSize", w.fontSize);
            cJSON_AddNumberToObject(widget, "textAlign", w.textAlign);
            cJSON_AddBoolToObject(widget, "isContainer", w.isContainer);

            char textColorStr[8];
            snprintf(textColorStr, sizeof(textColorStr), "#%02X%02X%02X",
                     w.textColor.r, w.textColor.g, w.textColor.b);
            cJSON_AddStringToObject(widget, "textColor", textColorStr);

            char widgetBgColorStr[8];
            snprintf(widgetBgColorStr, sizeof(widgetBgColorStr), "#%02X%02X%02X",
                     w.bgColor.r, w.bgColor.g, w.bgColor.b);
            cJSON_AddStringToObject(widget, "bgColor", widgetBgColorStr);

            cJSON_AddNumberToObject(widget, "bgOpacity", w.bgOpacity);
            cJSON_AddNumberToObject(widget, "radius", w.borderRadius);
            cJSON_AddNumberToObject(widget, "borderWidth", w.borderWidth);
            cJSON_AddNumberToObject(widget, "borderOpacity", w.borderOpacity);
            char borderColorStr[8];
            snprintf(borderColorStr, sizeof(borderColorStr), "#%02X%02X%02X",
                     w.borderColor.r, w.borderColor.g, w.borderColor.b);
            cJSON_AddStringToObject(widget, "borderColor", borderColorStr);

            cJSON* shadow = cJSON_AddObjectToObject(widget, "shadow");
            cJSON_AddBoolToObject(shadow, "enabled", w.shadow.enabled);
            cJSON_AddNumberToObject(shadow, "x", w.shadow.offsetX);
            cJSON_AddNumberToObject(shadow, "y", w.shadow.offsetY);
            cJSON_AddNumberToObject(shadow, "blur", w.shadow.blur);
            cJSON_AddNumberToObject(shadow, "spread", w.shadow.spread);
            char shadowColorStr[8];
            snprintf(shadowColorStr, sizeof(shadowColorStr), "#%02X%02X%02X",
                     w.shadow.color.r, w.shadow.color.g, w.shadow.color.b);
            cJSON_AddStringToObject(shadow, "color", shadowColorStr);

            cJSON_AddBoolToObject(widget, "isToggle", w.isToggle);
            cJSON_AddNumberToObject(widget, "knxAddrWrite", w.knxAddressWrite);
            cJSON_AddNumberToObject(widget, "action", static_cast<int>(w.action));
            cJSON_AddNumberToObject(widget, "targetScreen", w.targetScreen);

            // Icon properties
            cJSON_AddNumberToObject(widget, "iconCodepoint", w.iconCodepoint);
            cJSON_AddNumberToObject(widget, "iconPosition", w.iconPosition);
            cJSON_AddNumberToObject(widget, "iconSize", w.iconSize);
            cJSON_AddNumberToObject(widget, "iconGap", w.iconGap);
            cJSON_AddNumberToObject(widget, "iconPositionX", w.iconPositionX);
            cJSON_AddNumberToObject(widget, "iconPositionY", w.iconPositionY);
            cJSON_AddNumberToObject(widget, "arcMin", w.arcMin);
            cJSON_AddNumberToObject(widget, "arcMax", w.arcMax);
            cJSON_AddNumberToObject(widget, "arcUnit", w.arcUnit);
            cJSON_AddBoolToObject(widget, "arcShowValue", w.arcShowValue);
            cJSON_AddNumberToObject(widget, "arcScaleOffset", w.arcScaleOffset);
            char arcScaleColorStr[8];
            snprintf(arcScaleColorStr, sizeof(arcScaleColorStr), "#%02X%02X%02X",
                     w.arcScaleColor.r, w.arcScaleColor.g, w.arcScaleColor.b);
            cJSON_AddStringToObject(widget, "arcScaleColor", arcScaleColorStr);
            char arcValueColorStr[8];
            snprintf(arcValueColorStr, sizeof(arcValueColorStr), "#%02X%02X%02X",
                     w.arcValueColor.r, w.arcValueColor.g, w.arcValueColor.b);
            cJSON_AddStringToObject(widget, "arcValueColor", arcValueColorStr);
            cJSON_AddNumberToObject(widget, "arcValueFontSize", w.arcValueFontSize);

            cJSON_AddBoolToObject(widget, "themeFixed", w.themeFixed);

            cJSON_AddNumberToObject(widget, "parentId", w.parentId);

            // Secondary KNX address (left value)
            if (w.knxAddress2 > 0) {
                cJSON_AddNumberToObject(widget, "knxAddr2", w.knxAddress2);
                cJSON_AddNumberToObject(widget, "textSrc2", static_cast<int>(w.textSource2));
                cJSON_AddStringToObject(widget, "text2", w.text2);
            }

            // Tertiary KNX address (right value)
            if (w.knxAddress3 > 0) {
                cJSON_AddNumberToObject(widget, "knxAddr3", w.knxAddress3);
                cJSON_AddNumberToObject(widget, "textSrc3", static_cast<int>(w.textSource3));
                cJSON_AddStringToObject(widget, "text3", w.text3);
            }

            // Conditions
            if (w.conditionCount > 0) {
                cJSON* conditions = cJSON_AddArrayToObject(widget, "conditions");
                for (uint8_t ci = 0; ci < w.conditionCount && ci < MAX_CONDITIONS; ++ci) {
                    const StyleCondition& cond = w.conditions[ci];
                    if (!cond.enabled) continue;

                    cJSON* condJson = cJSON_CreateObject();

                    // Source
                    const char* sourceStr = "primary";
                    if (cond.source == ConditionSource::SECONDARY) sourceStr = "secondary";
                    else if (cond.source == ConditionSource::TERTIARY) sourceStr = "tertiary";
                    cJSON_AddStringToObject(condJson, "source", sourceStr);

                    cJSON_AddNumberToObject(condJson, "threshold", cond.threshold);

                    // Operator
                    const char* opStr = "lt";
                    switch (cond.op) {
                        case ConditionOp::LESS: opStr = "lt"; break;
                        case ConditionOp::LESS_EQUAL: opStr = "lte"; break;
                        case ConditionOp::EQUAL: opStr = "eq"; break;
                        case ConditionOp::GREATER_EQUAL: opStr = "gte"; break;
                        case ConditionOp::GREATER: opStr = "gt"; break;
                        case ConditionOp::NOT_EQUAL: opStr = "neq"; break;
                    }
                    cJSON_AddStringToObject(condJson, "op", opStr);

                    cJSON_AddNumberToObject(condJson, "priority", cond.priority);

                    // Style
                    if (cond.style.iconCodepoint != 0) {
                        cJSON_AddNumberToObject(condJson, "icon", cond.style.iconCodepoint);
                    }
                    if (cond.style.flags & ConditionStyle::FLAG_USE_TEXT_COLOR) {
                        char colorStr[8];
                        snprintf(colorStr, sizeof(colorStr), "#%02X%02X%02X",
                                 cond.style.textColor.r, cond.style.textColor.g, cond.style.textColor.b);
                        cJSON_AddStringToObject(condJson, "textColor", colorStr);
                    }
                    if (cond.style.flags & ConditionStyle::FLAG_USE_BG_COLOR) {
                        char colorStr[8];
                        snprintf(colorStr, sizeof(colorStr), "#%02X%02X%02X",
                                 cond.style.bgColor.r, cond.style.bgColor.g, cond.style.bgColor.b);
                        cJSON_AddStringToObject(condJson, "bgColor", colorStr);
                    }
                    if (cond.style.flags & ConditionStyle::FLAG_HIDE) {
                        cJSON_AddBoolToObject(condJson, "hide", true);
                    }

                    cJSON_AddItemToArray(conditions, condJson);
                }
            }

            if (w.type == WidgetType::CHART) {
                cJSON* chart = cJSON_AddObjectToObject(widget, "chart");
                cJSON_AddNumberToObject(chart, "period", w.chartPeriod);
                cJSON* series = cJSON_AddArrayToObject(chart, "series");
                uint8_t seriesCount = w.chartSeriesCount;
                if (seriesCount > CHART_MAX_SERIES) seriesCount = CHART_MAX_SERIES;
                for (uint8_t si = 0; si < seriesCount; ++si) {
                    cJSON* s = cJSON_CreateObject();
                    cJSON_AddNumberToObject(s, "knxAddr", w.chartKnxAddress[si]);
                    cJSON_AddNumberToObject(s, "textSrc", static_cast<int>(w.chartTextSource[si]));
                    char colorStr[8];
                    snprintf(colorStr, sizeof(colorStr), "#%02X%02X%02X",
                             w.chartSeriesColor[si].r, w.chartSeriesColor[si].g, w.chartSeriesColor[si].b);
                    cJSON_AddStringToObject(s, "color", colorStr);
                    cJSON_AddItemToArray(series, s);
                }
            }

            // RoomCard sub-buttons
            if (w.type == WidgetType::ROOMCARD) {
                cJSON_AddNumberToObject(widget, "subButtonSize", w.subButtonSize);
                cJSON_AddNumberToObject(widget, "subButtonDistance", w.subButtonDistance);
                cJSON_AddNumberToObject(widget, "subButtonOpacity", w.subButtonOpacity);
                cJSON_AddNumberToObject(widget, "cardStyle", w.cardStyle);
            }
            if (w.type == WidgetType::ROOMCARD && w.subButtonCount > 0) {
                cJSON* subButtons = cJSON_AddArrayToObject(widget, "subButtons");
                for (uint8_t si = 0; si < w.subButtonCount && si < MAX_SUBBUTTONS; ++si) {
                    const SubButtonConfig& sb = w.subButtons[si];
                    if (!sb.enabled) continue;

                    cJSON* sbJson = cJSON_CreateObject();
                    cJSON_AddNumberToObject(sbJson, "pos", static_cast<int>(sb.position));
                    cJSON_AddNumberToObject(sbJson, "iconOff", sb.iconCodepointOff);
                    cJSON_AddNumberToObject(sbJson, "iconOn", sb.iconCodepointOn);
                    cJSON_AddNumberToObject(sbJson, "knxRead", sb.knxAddrRead);
                    cJSON_AddNumberToObject(sbJson, "knxWrite", sb.knxAddrWrite);
                    cJSON_AddNumberToObject(sbJson, "action", static_cast<int>(sb.action));
                    cJSON_AddNumberToObject(sbJson, "target", sb.targetScreen);

                    char bgColorOnStr[8], bgColorOffStr[8], iconColorOnStr[8], iconColorOffStr[8];
                    snprintf(bgColorOnStr, sizeof(bgColorOnStr), "#%02X%02X%02X",
                             sb.bgColorOn.r, sb.bgColorOn.g, sb.bgColorOn.b);
                    snprintf(bgColorOffStr, sizeof(bgColorOffStr), "#%02X%02X%02X",
                             sb.bgColorOff.r, sb.bgColorOff.g, sb.bgColorOff.b);
                    snprintf(iconColorOnStr, sizeof(iconColorOnStr), "#%02X%02X%02X",
                             sb.iconColorOn.r, sb.iconColorOn.g, sb.iconColorOn.b);
                    snprintf(iconColorOffStr, sizeof(iconColorOffStr), "#%02X%02X%02X",
                             sb.iconColorOff.r, sb.iconColorOff.g, sb.iconColorOff.b);
                    cJSON_AddStringToObject(sbJson, "bgColorOn", bgColorOnStr);
                    cJSON_AddStringToObject(sbJson, "bgColorOff", bgColorOffStr);
                    cJSON_AddStringToObject(sbJson, "iconColorOn", iconColorOnStr);
                    cJSON_AddStringToObject(sbJson, "iconColorOff", iconColorOffStr);

                    cJSON_AddItemToArray(subButtons, sbJson);
                }
            }

            // RoomCard text lines
            if (w.type == WidgetType::ROOMCARD && w.textLineCount > 0) {
                cJSON* textLines = cJSON_AddArrayToObject(widget, "textLines");
                for (uint8_t ti = 0; ti < w.textLineCount && ti < MAX_TEXTLINES; ++ti) {
                    const TextLineConfig& tl = w.textLines[ti];
                    if (!tl.enabled) continue;

                    cJSON* tlJson = cJSON_CreateObject();
                    cJSON_AddNumberToObject(tlJson, "icon", tl.iconCodepoint);
                    cJSON_AddNumberToObject(tlJson, "textSrc", static_cast<int>(tl.textSrc));
                    cJSON_AddNumberToObject(tlJson, "knxAddr", tl.knxAddr);
                    cJSON_AddStringToObject(tlJson, "text", tl.text);
                    cJSON_AddNumberToObject(tlJson, "fontSize", tl.fontSize);

                    cJSON_AddItemToArray(textLines, tlJson);
                }
            }

            cJSON_AddItemToArray(widgets, widget);
        }

        cJSON_AddItemToArray(screens, screenJson);
    }

    char* jsonStr = cJSON_PrintUnformatted(root);
    if (jsonStr) {
        strncpy(buf, jsonStr, bufSize - 1);
        buf[bufSize - 1] = '\0';
        free(jsonStr);
    } else {
        snprintf(buf, bufSize, "{}");
    }

    cJSON_Delete(root);
}

bool WidgetManager::updateConfigFromJson(const char* json) {
    cJSON* root = cJSON_Parse(json);
    if (!root) {
        ESP_LOGE(TAG, "Failed to parse JSON");
        return false;
    }

    std::unique_ptr<GuiConfig> newConfig(new (std::nothrow) GuiConfig());
    if (!newConfig) {
        ESP_LOGE(TAG, "Out of memory for config");
        cJSON_Delete(root);
        return false;
    }
    newConfig->clear();

    auto parseWidgets = [&](cJSON* widgets, ScreenConfig& screen) -> bool {
        if (!cJSON_IsArray(widgets)) return false;

        screen.widgetCount = 0;
        cJSON* widget = nullptr;
        cJSON_ArrayForEach(widget, widgets) {
            if (screen.widgetCount >= MAX_WIDGETS) break;

            WidgetConfig& w = screen.widgets[screen.widgetCount];
            memset(&w, 0, sizeof(w));
            w.visible = true;
            w.action = ButtonAction::KNX;
            w.targetScreen = 0;
            w.textAlign = static_cast<uint8_t>(TextAlign::CENTER);
            w.isContainer = false;
            w.chartPeriod = static_cast<uint8_t>(ChartPeriod::HOUR_1);
            w.chartSeriesCount = 1;
            for (size_t i = 0; i < CHART_MAX_SERIES; ++i) {
                w.chartKnxAddress[i] = 0;
                w.chartTextSource[i] = TextSource::KNX_DPT_TEMP;
                w.chartSeriesColor[i] = defaultChartColor(i);
            }
            w.arcMin = 0;
            w.arcMax = 100;
            w.arcUnit = 0;
            w.arcShowValue = true;
            w.arcScaleOffset = 0;
            w.arcScaleColor = w.textColor;
            w.arcValueColor = w.textColor;
            w.arcValueFontSize = w.fontSize;

            cJSON* id = cJSON_GetObjectItem(widget, "id");
            if (cJSON_IsNumber(id)) w.id = id->valueint;

            cJSON* type = cJSON_GetObjectItem(widget, "type");
            if (cJSON_IsNumber(type)) w.type = static_cast<WidgetType>(type->valueint);

            cJSON* x = cJSON_GetObjectItem(widget, "x");
            if (cJSON_IsNumber(x)) w.x = x->valueint;

            cJSON* y = cJSON_GetObjectItem(widget, "y");
            if (cJSON_IsNumber(y)) w.y = y->valueint;

            cJSON* width = cJSON_GetObjectItem(widget, "w");
            if (cJSON_IsNumber(width)) w.width = width->valueint;

            cJSON* height = cJSON_GetObjectItem(widget, "h");
            if (cJSON_IsNumber(height)) w.height = height->valueint;

            cJSON* visible = cJSON_GetObjectItem(widget, "visible");
            if (cJSON_IsBool(visible)) w.visible = cJSON_IsTrue(visible);

            cJSON* textSrc = cJSON_GetObjectItem(widget, "textSrc");
            if (cJSON_IsNumber(textSrc)) w.textSource = static_cast<TextSource>(textSrc->valueint);

            cJSON* text = cJSON_GetObjectItem(widget, "text");
            if (cJSON_IsString(text)) {
                strncpy(w.text, text->valuestring, MAX_TEXT_LEN - 1);
                w.text[MAX_TEXT_LEN - 1] = '\0';
            }

            cJSON* knxAddr = cJSON_GetObjectItem(widget, "knxAddr");
            if (cJSON_IsNumber(knxAddr)) w.knxAddress = knxAddr->valueint;

            cJSON* fontSize = cJSON_GetObjectItem(widget, "fontSize");
            if (cJSON_IsNumber(fontSize)) w.fontSize = fontSize->valueint;

            cJSON* textAlign = cJSON_GetObjectItem(widget, "textAlign");
            if (cJSON_IsNumber(textAlign)) w.textAlign = textAlign->valueint;

            cJSON* isContainer = cJSON_GetObjectItem(widget, "isContainer");
            if (cJSON_IsBool(isContainer)) w.isContainer = cJSON_IsTrue(isContainer);

            cJSON* textColor = cJSON_GetObjectItem(widget, "textColor");
            if (cJSON_IsString(textColor)) {
                w.textColor = Color::fromHex(parseHexColor(textColor->valuestring));
            }

            cJSON* widgetBgColor = cJSON_GetObjectItem(widget, "bgColor");
            if (cJSON_IsString(widgetBgColor)) {
                w.bgColor = Color::fromHex(parseHexColor(widgetBgColor->valuestring));
            }

            cJSON* bgOpacity = cJSON_GetObjectItem(widget, "bgOpacity");
            if (cJSON_IsNumber(bgOpacity)) w.bgOpacity = bgOpacity->valueint;

            cJSON* radius = cJSON_GetObjectItem(widget, "radius");
            if (cJSON_IsNumber(radius)) w.borderRadius = radius->valueint;

            cJSON* borderWidth = cJSON_GetObjectItem(widget, "borderWidth");
            if (cJSON_IsNumber(borderWidth)) w.borderWidth = borderWidth->valueint;

            cJSON* borderOpacity = cJSON_GetObjectItem(widget, "borderOpacity");
            if (cJSON_IsNumber(borderOpacity)) w.borderOpacity = borderOpacity->valueint;

            cJSON* borderColor = cJSON_GetObjectItem(widget, "borderColor");
            if (cJSON_IsString(borderColor)) {
                w.borderColor = Color::fromHex(parseHexColor(borderColor->valuestring));
            }

            cJSON* shadow = cJSON_GetObjectItem(widget, "shadow");
            if (cJSON_IsObject(shadow)) {
                cJSON* enabled = cJSON_GetObjectItem(shadow, "enabled");
                if (cJSON_IsBool(enabled)) w.shadow.enabled = cJSON_IsTrue(enabled);

                cJSON* sx = cJSON_GetObjectItem(shadow, "x");
                if (cJSON_IsNumber(sx)) w.shadow.offsetX = sx->valueint;

                cJSON* sy = cJSON_GetObjectItem(shadow, "y");
                if (cJSON_IsNumber(sy)) w.shadow.offsetY = sy->valueint;

                cJSON* blur = cJSON_GetObjectItem(shadow, "blur");
                if (cJSON_IsNumber(blur)) w.shadow.blur = blur->valueint;

                cJSON* spread = cJSON_GetObjectItem(shadow, "spread");
                if (cJSON_IsNumber(spread)) w.shadow.spread = spread->valueint;

                cJSON* shadowColor = cJSON_GetObjectItem(shadow, "color");
                if (cJSON_IsString(shadowColor)) {
                    w.shadow.color = Color::fromHex(parseHexColor(shadowColor->valuestring));
                }
            }

            cJSON* isToggle = cJSON_GetObjectItem(widget, "isToggle");
            if (cJSON_IsBool(isToggle)) w.isToggle = cJSON_IsTrue(isToggle);

            cJSON* knxAddrWrite = cJSON_GetObjectItem(widget, "knxAddrWrite");
            if (cJSON_IsNumber(knxAddrWrite)) w.knxAddressWrite = knxAddrWrite->valueint;

            cJSON* action = cJSON_GetObjectItem(widget, "action");
            if (cJSON_IsNumber(action)) w.action = static_cast<ButtonAction>(action->valueint);

            cJSON* targetScreen = cJSON_GetObjectItem(widget, "targetScreen");
            if (cJSON_IsNumber(targetScreen)) w.targetScreen = targetScreen->valueint;

            // Icon properties
            cJSON* iconCodepoint = cJSON_GetObjectItem(widget, "iconCodepoint");
            if (cJSON_IsNumber(iconCodepoint)) w.iconCodepoint = static_cast<uint32_t>(iconCodepoint->valuedouble);

            cJSON* iconPosition = cJSON_GetObjectItem(widget, "iconPosition");
            if (cJSON_IsNumber(iconPosition)) w.iconPosition = iconPosition->valueint;

            cJSON* iconSize = cJSON_GetObjectItem(widget, "iconSize");
            if (cJSON_IsNumber(iconSize)) w.iconSize = iconSize->valueint;

            cJSON* iconGap = cJSON_GetObjectItem(widget, "iconGap");
            if (cJSON_IsNumber(iconGap)) w.iconGap = iconGap->valueint;

            cJSON* iconPositionX = cJSON_GetObjectItem(widget, "iconPositionX");
            if (cJSON_IsNumber(iconPositionX)) w.iconPositionX = iconPositionX->valueint;

            cJSON* iconPositionY = cJSON_GetObjectItem(widget, "iconPositionY");
            if (cJSON_IsNumber(iconPositionY)) w.iconPositionY = iconPositionY->valueint;

            cJSON* arcMin = cJSON_GetObjectItem(widget, "arcMin");
            if (cJSON_IsNumber(arcMin)) w.arcMin = arcMin->valueint;

            cJSON* arcMax = cJSON_GetObjectItem(widget, "arcMax");
            if (cJSON_IsNumber(arcMax)) w.arcMax = arcMax->valueint;

            cJSON* arcUnit = cJSON_GetObjectItem(widget, "arcUnit");
            if (cJSON_IsNumber(arcUnit)) w.arcUnit = arcUnit->valueint;

            cJSON* arcShowValue = cJSON_GetObjectItem(widget, "arcShowValue");
            if (cJSON_IsBool(arcShowValue)) {
                w.arcShowValue = cJSON_IsTrue(arcShowValue);
            } else if (cJSON_IsNumber(arcShowValue)) {
                w.arcShowValue = arcShowValue->valueint != 0;
            }

            cJSON* arcScaleOffset = cJSON_GetObjectItem(widget, "arcScaleOffset");
            if (cJSON_IsNumber(arcScaleOffset)) w.arcScaleOffset = static_cast<int8_t>(arcScaleOffset->valueint);

            cJSON* arcScaleColor = cJSON_GetObjectItem(widget, "arcScaleColor");
            if (cJSON_IsString(arcScaleColor)) {
                w.arcScaleColor = Color::fromHex(parseHexColor(arcScaleColor->valuestring));
            }

            cJSON* arcValueColor = cJSON_GetObjectItem(widget, "arcValueColor");
            if (cJSON_IsString(arcValueColor)) {
                w.arcValueColor = Color::fromHex(parseHexColor(arcValueColor->valuestring));
            }

            cJSON* arcValueFontSize = cJSON_GetObjectItem(widget, "arcValueFontSize");
            if (cJSON_IsNumber(arcValueFontSize)) w.arcValueFontSize = arcValueFontSize->valueint;

            cJSON* themeFixed = cJSON_GetObjectItem(widget, "themeFixed");
            if (cJSON_IsBool(themeFixed)) w.themeFixed = cJSON_IsTrue(themeFixed);

            cJSON* parentId = cJSON_GetObjectItem(widget, "parentId");
            if (cJSON_IsNumber(parentId)) {
                w.parentId = static_cast<int8_t>(parentId->valueint);
            } else {
                w.parentId = -1; // Default to root
            }

            // Secondary KNX address (left value)
            cJSON* knxAddr2 = cJSON_GetObjectItem(widget, "knxAddr2");
            if (cJSON_IsNumber(knxAddr2)) w.knxAddress2 = knxAddr2->valueint;

            cJSON* textSrc2 = cJSON_GetObjectItem(widget, "textSrc2");
            if (cJSON_IsNumber(textSrc2)) w.textSource2 = static_cast<TextSource>(textSrc2->valueint);

            cJSON* text2 = cJSON_GetObjectItem(widget, "text2");
            if (cJSON_IsString(text2)) {
                strncpy(w.text2, text2->valuestring, MAX_FORMAT_LEN - 1);
                w.text2[MAX_FORMAT_LEN - 1] = '\0';
            }

            // Tertiary KNX address (right value)
            cJSON* knxAddr3 = cJSON_GetObjectItem(widget, "knxAddr3");
            if (cJSON_IsNumber(knxAddr3)) w.knxAddress3 = knxAddr3->valueint;

            cJSON* textSrc3 = cJSON_GetObjectItem(widget, "textSrc3");
            if (cJSON_IsNumber(textSrc3)) w.textSource3 = static_cast<TextSource>(textSrc3->valueint);

            cJSON* text3 = cJSON_GetObjectItem(widget, "text3");
            if (cJSON_IsString(text3)) {
                strncpy(w.text3, text3->valuestring, MAX_FORMAT_LEN - 1);
                w.text3[MAX_FORMAT_LEN - 1] = '\0';
            }

            // Conditions
            cJSON* conditions = cJSON_GetObjectItem(widget, "conditions");
            if (cJSON_IsArray(conditions)) {
                uint8_t condIdx = 0;
                cJSON* condItem = nullptr;
                cJSON_ArrayForEach(condItem, conditions) {
                    if (condIdx >= MAX_CONDITIONS) break;

                    StyleCondition& cond = w.conditions[condIdx];
                    memset(&cond, 0, sizeof(cond));
                    cond.enabled = true;

                    // Source
                    cJSON* source = cJSON_GetObjectItem(condItem, "source");
                    if (cJSON_IsString(source)) {
                        if (strcmp(source->valuestring, "secondary") == 0) {
                            cond.source = ConditionSource::SECONDARY;
                        } else if (strcmp(source->valuestring, "tertiary") == 0) {
                            cond.source = ConditionSource::TERTIARY;
                        } else {
                            cond.source = ConditionSource::PRIMARY;
                        }
                    }

                    // Threshold
                    cJSON* threshold = cJSON_GetObjectItem(condItem, "threshold");
                    if (cJSON_IsNumber(threshold)) cond.threshold = static_cast<float>(threshold->valuedouble);

                    // Operator
                    cJSON* op = cJSON_GetObjectItem(condItem, "op");
                    if (cJSON_IsString(op)) {
                        if (strcmp(op->valuestring, "lt") == 0) cond.op = ConditionOp::LESS;
                        else if (strcmp(op->valuestring, "lte") == 0) cond.op = ConditionOp::LESS_EQUAL;
                        else if (strcmp(op->valuestring, "eq") == 0) cond.op = ConditionOp::EQUAL;
                        else if (strcmp(op->valuestring, "gte") == 0) cond.op = ConditionOp::GREATER_EQUAL;
                        else if (strcmp(op->valuestring, "gt") == 0) cond.op = ConditionOp::GREATER;
                        else if (strcmp(op->valuestring, "neq") == 0) cond.op = ConditionOp::NOT_EQUAL;
                    }

                    // Priority
                    cJSON* priority = cJSON_GetObjectItem(condItem, "priority");
                    if (cJSON_IsNumber(priority)) cond.priority = priority->valueint;

                    // Icon
                    cJSON* icon = cJSON_GetObjectItem(condItem, "icon");
                    if (cJSON_IsNumber(icon)) cond.style.iconCodepoint = static_cast<uint32_t>(icon->valuedouble);

                    // Text color
                    cJSON* textColor = cJSON_GetObjectItem(condItem, "textColor");
                    if (cJSON_IsString(textColor)) {
                        cond.style.textColor = Color::fromHex(parseHexColor(textColor->valuestring));
                        cond.style.flags |= ConditionStyle::FLAG_USE_TEXT_COLOR;
                    }

                    // Background color (only if not empty and starts with #)
                    cJSON* bgColor = cJSON_GetObjectItem(condItem, "bgColor");
                    if (cJSON_IsString(bgColor) && bgColor->valuestring &&
                        bgColor->valuestring[0] == '#' && strlen(bgColor->valuestring) >= 4) {
                        cond.style.bgColor = Color::fromHex(parseHexColor(bgColor->valuestring));
                        cond.style.flags |= ConditionStyle::FLAG_USE_BG_COLOR;
                    }

                    // Hide
                    cJSON* hide = cJSON_GetObjectItem(condItem, "hide");
                    if (cJSON_IsBool(hide) && cJSON_IsTrue(hide)) {
                        cond.style.flags |= ConditionStyle::FLAG_HIDE;
                    }

                    condIdx++;
                }
                w.conditionCount = condIdx;
            }

            cJSON* chart = cJSON_GetObjectItem(widget, "chart");
            if (cJSON_IsObject(chart)) {
                cJSON* period = cJSON_GetObjectItem(chart, "period");
                if (cJSON_IsNumber(period)) {
                    int periodVal = period->valueint;
                    if (periodVal < 0) periodVal = 0;
                    if (periodVal > static_cast<int>(ChartPeriod::MONTH_1)) {
                        periodVal = static_cast<int>(ChartPeriod::MONTH_1);
                    }
                    w.chartPeriod = static_cast<uint8_t>(periodVal);
                }

                cJSON* series = cJSON_GetObjectItem(chart, "series");
                if (cJSON_IsArray(series)) {
                    uint8_t idx = 0;
                    cJSON* item = nullptr;
                    cJSON_ArrayForEach(item, series) {
                        if (idx >= CHART_MAX_SERIES) break;
                        cJSON* sAddr = cJSON_GetObjectItem(item, "knxAddr");
                        if (cJSON_IsNumber(sAddr)) {
                            w.chartKnxAddress[idx] = sAddr->valueint;
                        }
                        cJSON* sSrc = cJSON_GetObjectItem(item, "textSrc");
                        if (cJSON_IsNumber(sSrc)) {
                            TextSource src = static_cast<TextSource>(sSrc->valueint);
                            if (isNumericTextSource(src)) {
                                w.chartTextSource[idx] = src;
                            }
                        }
                        cJSON* sColor = cJSON_GetObjectItem(item, "color");
                        if (cJSON_IsString(sColor)) {
                            w.chartSeriesColor[idx] = Color::fromHex(parseHexColor(sColor->valuestring));
                        }
                        idx++;
                    }
                    if (idx > 0) {
                        w.chartSeriesCount = idx;
                    }
                }
            }

            // RoomCard sub-button size and distance
            cJSON* subButtonSize = cJSON_GetObjectItem(widget, "subButtonSize");
            if (cJSON_IsNumber(subButtonSize)) {
                w.subButtonSize = subButtonSize->valueint;
            } else {
                w.subButtonSize = 40; // Default
            }
            cJSON* subButtonDistance = cJSON_GetObjectItem(widget, "subButtonDistance");
            if (cJSON_IsNumber(subButtonDistance)) {
                w.subButtonDistance = subButtonDistance->valueint;
            } else {
                w.subButtonDistance = 80; // Default 80px
            }
            cJSON* subButtonOpacity = cJSON_GetObjectItem(widget, "subButtonOpacity");
            if (cJSON_IsNumber(subButtonOpacity)) {
                w.subButtonOpacity = subButtonOpacity->valueint;
            } else {
                w.subButtonOpacity = 255; // Default fully opaque
            }
            cJSON* cardStyle = cJSON_GetObjectItem(widget, "cardStyle");
            if (cJSON_IsNumber(cardStyle)) {
                w.cardStyle = cardStyle->valueint;
                ESP_LOGI(TAG, "RoomCard cardStyle loaded: %d", w.cardStyle);
            } else {
                w.cardStyle = 0; // Default to Bubble style
                ESP_LOGI(TAG, "RoomCard cardStyle not found, using default 0");
            }

            // RoomCard sub-buttons
            cJSON* subButtons = cJSON_GetObjectItem(widget, "subButtons");
            if (cJSON_IsArray(subButtons)) {
                uint8_t sbIdx = 0;
                cJSON* sbItem = nullptr;
                cJSON_ArrayForEach(sbItem, subButtons) {
                    if (sbIdx >= MAX_SUBBUTTONS) break;

                    SubButtonConfig& sb = w.subButtons[sbIdx];
                    memset(&sb, 0, sizeof(sb));
                    sb.enabled = true;

                    cJSON* pos = cJSON_GetObjectItem(sbItem, "pos");
                    if (cJSON_IsNumber(pos)) {
                        sb.position = static_cast<SubButtonPosition>(pos->valueint);
                    }

                    cJSON* iconOff = cJSON_GetObjectItem(sbItem, "iconOff");
                    if (cJSON_IsNumber(iconOff)) {
                        sb.iconCodepointOff = static_cast<uint32_t>(iconOff->valuedouble);
                    } else {
                        // Backward compatibility: try old "icon" field
                        cJSON* icon = cJSON_GetObjectItem(sbItem, "icon");
                        if (cJSON_IsNumber(icon)) {
                            sb.iconCodepointOff = static_cast<uint32_t>(icon->valuedouble);
                        }
                    }

                    cJSON* iconOn = cJSON_GetObjectItem(sbItem, "iconOn");
                    if (cJSON_IsNumber(iconOn)) {
                        sb.iconCodepointOn = static_cast<uint32_t>(iconOn->valuedouble);
                    }

                    cJSON* knxRead = cJSON_GetObjectItem(sbItem, "knxRead");
                    if (cJSON_IsNumber(knxRead)) {
                        sb.knxAddrRead = knxRead->valueint;
                    }

                    cJSON* knxWrite = cJSON_GetObjectItem(sbItem, "knxWrite");
                    if (cJSON_IsNumber(knxWrite)) {
                        sb.knxAddrWrite = knxWrite->valueint;
                    }

                    cJSON* action = cJSON_GetObjectItem(sbItem, "action");
                    if (cJSON_IsNumber(action)) {
                        sb.action = static_cast<SubButtonAction>(action->valueint);
                    }

                    cJSON* target = cJSON_GetObjectItem(sbItem, "target");
                    if (cJSON_IsNumber(target)) {
                        sb.targetScreen = target->valueint;
                    }

                    // Background colors
                    cJSON* bgColorOn = cJSON_GetObjectItem(sbItem, "bgColorOn");
                    if (cJSON_IsString(bgColorOn)) {
                        sb.bgColorOn = Color::fromHex(parseHexColor(bgColorOn->valuestring));
                    } else {
                        // Backward compatibility: try old "colorOn" field
                        cJSON* colorOn = cJSON_GetObjectItem(sbItem, "colorOn");
                        if (cJSON_IsString(colorOn)) {
                            sb.bgColorOn = Color::fromHex(parseHexColor(colorOn->valuestring));
                        }
                    }

                    cJSON* bgColorOff = cJSON_GetObjectItem(sbItem, "bgColorOff");
                    if (cJSON_IsString(bgColorOff)) {
                        sb.bgColorOff = Color::fromHex(parseHexColor(bgColorOff->valuestring));
                    } else {
                        // Backward compatibility: try old "colorOff" field
                        cJSON* colorOff = cJSON_GetObjectItem(sbItem, "colorOff");
                        if (cJSON_IsString(colorOff)) {
                            sb.bgColorOff = Color::fromHex(parseHexColor(colorOff->valuestring));
                        }
                    }

                    // Icon colors (default to white if not specified)
                    cJSON* iconColorOn = cJSON_GetObjectItem(sbItem, "iconColorOn");
                    if (cJSON_IsString(iconColorOn)) {
                        sb.iconColorOn = Color::fromHex(parseHexColor(iconColorOn->valuestring));
                    } else {
                        sb.iconColorOn = {255, 255, 255}; // Default white
                    }

                    cJSON* iconColorOff = cJSON_GetObjectItem(sbItem, "iconColorOff");
                    if (cJSON_IsString(iconColorOff)) {
                        sb.iconColorOff = Color::fromHex(parseHexColor(iconColorOff->valuestring));
                    } else {
                        sb.iconColorOff = {255, 255, 255}; // Default white
                    }

                    sbIdx++;
                }
                w.subButtonCount = sbIdx;
            }

            // RoomCard text lines
            cJSON* textLines = cJSON_GetObjectItem(widget, "textLines");
            if (cJSON_IsArray(textLines)) {
                uint8_t tlIdx = 0;
                cJSON* tlItem = nullptr;
                cJSON_ArrayForEach(tlItem, textLines) {
                    if (tlIdx >= MAX_TEXTLINES) break;

                    TextLineConfig& tl = w.textLines[tlIdx];
                    memset(&tl, 0, sizeof(tl));
                    tl.enabled = true;

                    cJSON* icon = cJSON_GetObjectItem(tlItem, "icon");
                    if (cJSON_IsNumber(icon)) {
                        tl.iconCodepoint = static_cast<uint32_t>(icon->valuedouble);
                    }

                    cJSON* textSrc = cJSON_GetObjectItem(tlItem, "textSrc");
                    if (cJSON_IsNumber(textSrc)) {
                        tl.textSrc = static_cast<TextSource>(textSrc->valueint);
                    }

                    cJSON* knxAddr = cJSON_GetObjectItem(tlItem, "knxAddr");
                    if (cJSON_IsNumber(knxAddr)) {
                        tl.knxAddr = knxAddr->valueint;
                    }

                    cJSON* text = cJSON_GetObjectItem(tlItem, "text");
                    if (cJSON_IsString(text)) {
                        strncpy(tl.text, text->valuestring, MAX_FORMAT_LEN - 1);
                        tl.text[MAX_FORMAT_LEN - 1] = '\0';
                    }

                    cJSON* fontSize = cJSON_GetObjectItem(tlItem, "fontSize");
                    if (cJSON_IsNumber(fontSize)) {
                        tl.fontSize = fontSize->valueint;
                    } else {
                        tl.fontSize = 1;  // Default 18px
                    }

                    tlIdx++;
                }
                w.textLineCount = tlIdx;
            }

            screen.widgetCount++;
        }

        return true;
    };

    cJSON* screens = cJSON_GetObjectItem(root, "screens");
    if (cJSON_IsArray(screens)) {
        cJSON* screenJson = nullptr;
        cJSON_ArrayForEach(screenJson, screens) {
            if (newConfig->screenCount >= MAX_SCREENS) break;

            uint8_t screenId = newConfig->screenCount;
            const char* screenName = nullptr;

            cJSON* id = cJSON_GetObjectItem(screenJson, "id");
            if (cJSON_IsNumber(id)) {
                int idVal = id->valueint;
                if (idVal < 0) idVal = 0;
                screenId = static_cast<uint8_t>(idVal);
            }

            cJSON* name = cJSON_GetObjectItem(screenJson, "name");
            if (cJSON_IsString(name)) screenName = name->valuestring;

            ScreenConfig& screen = newConfig->screens[newConfig->screenCount];
            screen.clear(screenId, screenName);

            if (!screen.name[0]) {
                char fallback[16];
                snprintf(fallback, sizeof(fallback), "Screen %d", screenId);
                strncpy(screen.name, fallback, sizeof(screen.name) - 1);
            }

            cJSON* mode = cJSON_GetObjectItem(screenJson, "mode");
            if (cJSON_IsNumber(mode)) screen.mode = static_cast<ScreenMode>(mode->valueint);

            cJSON* bgColor = cJSON_GetObjectItem(screenJson, "bgColor");
            if (cJSON_IsString(bgColor)) {
                screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
            }

            cJSON* nightBgColor = cJSON_GetObjectItem(screenJson, "nightBgColor");
            if (cJSON_IsString(nightBgColor)) {
                screen.nightBackgroundColor = Color::fromHex(parseHexColor(nightBgColor->valuestring));
            }

            // Parse background image
            cJSON* bgImage = cJSON_GetObjectItem(screenJson, "bgImage");
            if (cJSON_IsString(bgImage)) {
                strncpy(screen.bgImagePath, bgImage->valuestring, MAX_BG_IMAGE_PATH_LEN - 1);
                screen.bgImagePath[MAX_BG_IMAGE_PATH_LEN - 1] = '\0';
            } else {
                screen.bgImagePath[0] = '\0';
            }

            cJSON* bgImageMode = cJSON_GetObjectItem(screenJson, "bgImageMode");
            if (cJSON_IsNumber(bgImageMode)) {
                screen.bgImageMode = static_cast<BgImageMode>(bgImageMode->valueint);
            } else {
                screen.bgImageMode = BgImageMode::STRETCH;
            }

            // Parse modal-specific properties
            cJSON* modal = cJSON_GetObjectItem(screenJson, "modal");
            if (cJSON_IsObject(modal)) {
                cJSON* mx = cJSON_GetObjectItem(modal, "x");
                if (cJSON_IsNumber(mx)) screen.modalX = mx->valueint;

                cJSON* my = cJSON_GetObjectItem(modal, "y");
                if (cJSON_IsNumber(my)) screen.modalY = my->valueint;

                cJSON* mw = cJSON_GetObjectItem(modal, "w");
                if (cJSON_IsNumber(mw)) screen.modalWidth = mw->valueint;

                cJSON* mh = cJSON_GetObjectItem(modal, "h");
                if (cJSON_IsNumber(mh)) screen.modalHeight = mh->valueint;

                cJSON* mr = cJSON_GetObjectItem(modal, "radius");
                if (cJSON_IsNumber(mr)) screen.modalBorderRadius = mr->valueint;

                cJSON* dim = cJSON_GetObjectItem(modal, "dim");
                if (cJSON_IsBool(dim)) screen.modalDimBackground = cJSON_IsTrue(dim);
            }

            cJSON* widgets = cJSON_GetObjectItem(screenJson, "widgets");
            if (!parseWidgets(widgets, screen)) {
                screen.widgetCount = 0;
            }
            normalizeButtons(screen);

            newConfig->screenCount++;
        }
    } else {
        newConfig->screenCount = 1;
        ScreenConfig& screen = newConfig->screens[0];
        screen.clear(0, "Screen 1");

        cJSON* bgColor = cJSON_GetObjectItem(root, "bgColor");
        if (cJSON_IsString(bgColor)) {
            screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
        }

        cJSON* widgets = cJSON_GetObjectItem(root, "widgets");
        if (!parseWidgets(widgets, screen)) {
            cJSON_Delete(root);
            return false;
        }
        normalizeButtons(screen);
    }

    cJSON* startScreen = cJSON_GetObjectItem(root, "startScreen");
    if (cJSON_IsNumber(startScreen)) {
        int val = startScreen->valueint;
        if (val < 0) val = 0;
        newConfig->startScreenId = static_cast<uint8_t>(val);
    }

    cJSON* standby = cJSON_GetObjectItem(root, "standby");
    if (cJSON_IsObject(standby)) {
        cJSON* enabled = cJSON_GetObjectItem(standby, "enabled");
        if (cJSON_IsBool(enabled)) newConfig->standbyEnabled = cJSON_IsTrue(enabled);

        cJSON* screen = cJSON_GetObjectItem(standby, "screen");
        if (cJSON_IsNumber(screen)) {
            int val = screen->valueint;
            if (val < 0) {
                newConfig->standbyScreenId = SCREEN_ID_NONE;
            } else {
                newConfig->standbyScreenId = static_cast<uint8_t>(val);
            }
        }

        cJSON* minutes = cJSON_GetObjectItem(standby, "minutes");
        if (cJSON_IsNumber(minutes)) {
            int val = minutes->valueint;
            if (val < 0) val = 0;
            newConfig->standbyMinutes = static_cast<uint16_t>(val);
        }
    }

    cJSON* knx = cJSON_GetObjectItem(root, "knx");
    if (cJSON_IsObject(knx)) {
        cJSON* timeAddr = cJSON_GetObjectItem(knx, "time");
        if (cJSON_IsNumber(timeAddr)) newConfig->knxTimeAddress = timeAddr->valueint;

        cJSON* dateAddr = cJSON_GetObjectItem(knx, "date");
        if (cJSON_IsNumber(dateAddr)) newConfig->knxDateAddress = dateAddr->valueint;

        cJSON* dateTimeAddr = cJSON_GetObjectItem(knx, "dateTime");
        if (cJSON_IsNumber(dateTimeAddr)) newConfig->knxDateTimeAddress = dateTimeAddr->valueint;

        cJSON* nightAddr = cJSON_GetObjectItem(knx, "night");
        if (cJSON_IsNumber(nightAddr)) newConfig->knxNightModeAddress = nightAddr->valueint;
    }

    // Screen transition animation
    cJSON* anim = cJSON_GetObjectItem(root, "screenAnim");
    if (cJSON_IsObject(anim)) {
        cJSON* animType = cJSON_GetObjectItem(anim, "type");
        if (cJSON_IsNumber(animType)) {
            newConfig->screenAnimType = static_cast<ScreenAnimType>(animType->valueint);
        }
        cJSON* animDuration = cJSON_GetObjectItem(anim, "duration");
        if (cJSON_IsNumber(animDuration)) {
            newConfig->screenAnimDuration = static_cast<uint16_t>(animDuration->valueint);
        }
    }

    if (newConfig->screenCount == 0) {
        cJSON_Delete(root);
        return false;
    }

    if (!newConfig->findScreen(newConfig->startScreenId)) {
        newConfig->startScreenId = newConfig->screens[0].id;
    }

    if (!newConfig->findScreen(newConfig->standbyScreenId)) {
        newConfig->standbyEnabled = false;
        newConfig->standbyScreenId = SCREEN_ID_NONE;
    }

    *config_ = *newConfig;
    cJSON_Delete(root);
    ESP_LOGI(TAG, "Parsed %d screens from JSON", config_->screenCount);
    return true;
}
