// D:\Dev\Mosis\MosisService\designer\main.cpp
// Mosis Designer - Desktop UI development tool with hot-reload

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <RmlUi/Core.h>
#include <RmlUi/Lua.h>
#include <RmlUi/Debugger.h>
#include "RmlUi_Renderer_GL3.h"
#include "platform.h"
#include "desktop_platform.h"
#include "desktop_file_interface.h"
#include "hot_reload.h"
#include "testing/action_recorder.h"
#include "testing/action_player.h"
#include "testing/ui_inspector.h"
#include "testing/visual_capture.h"
#include <RmlUi/Lua/Interpreter.h>
#include <RmlUi/Lua/LuaType.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <filesystem>
#include <memory>
#include <chrono>
#include <ctime>
#include "src/desktop_sandbox.h"
#include "src/app_discovery.h"

namespace fs = std::filesystem;

// Global state
static GLFWwindow* g_window = nullptr;
static Rml::Context* g_context = nullptr;
static RenderInterface_GL3* g_render_interface = nullptr;
static mosis::DesktopPlatform* g_platform = nullptr;
static mosis::HotReload* g_hot_reload = nullptr;
static std::string g_current_document_path;
static std::string g_current_screen_url;  // For hierarchy dump - tracks current screen
static bool g_needs_reload = false;
static std::unique_ptr<mosis::DesktopSandbox> g_sandbox;

// Resolution presets
static int g_width = 540;
static int g_height = 960;

// Test mode
enum class TestMode {
    Interactive,  // Normal interactive mode with hot-reload
    Record,       // Record user actions to JSON
    Playback,     // Playback actions from JSON
    Screenshot,   // Take a screenshot and exit
    DumpHierarchy // Dump UI hierarchy to JSON and exit
};

static TestMode g_test_mode = TestMode::Interactive;
static std::string g_test_input_path;   // Input file for playback
static std::string g_test_output_path;  // Output file for record/screenshot/hierarchy
static mosis::testing::ActionRecorder* g_action_recorder = nullptr;
static mosis::testing::ActionPlayer* g_action_player = nullptr;

// Touch simulation state for natural scrolling behavior
static bool g_touch_active = false;
static int g_touch_start_x = 0;
static int g_touch_start_y = 0;

// Logging and hierarchy dump for testing
static std::string g_log_file_path;       // Log file path (--log)
static std::string g_hierarchy_file_path; // Continuous hierarchy dump path (--hierarchy)
static std::ofstream g_log_file;

// Simulator mode
static bool g_simulator_mode = false;
static bool g_shell_mode = false;          // Use persistent shell instead of direct documents
static std::string g_test_apps_path;       // Path to test-apps directory
static std::string g_simulator_home_path;  // Path to simulator home.rml
static std::string g_shell_path;           // Path to shell.rml for shell mode
static std::string g_main_assets_path;     // Path to main assets (for goHome)
static std::vector<mosis::AppInfo> g_discovered_apps;
static std::string g_current_app_id;       // Currently running app (empty = home)

// Forward declarations
bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height);
void ShutdownRmlUi();
bool LoadDocument(const std::string& path);
void ReloadDocument();
void PopulateSimulatorApps();

// Helper to set the 'document' global in Lua to the current document
static void SetLuaDocumentGlobal(Rml::ElementDocument* doc) {
    if (!doc) return;
    lua_State* L = Rml::Lua::Interpreter::GetLuaState();
    if (!L) return;

    // Push the document using RmlUi's Lua type system
    Rml::Lua::LuaType<Rml::ElementDocument>::push(L, doc, false);
    lua_setglobal(L, "document");
    std::cout << "Set Lua 'document' global to: " << doc->GetTitle() << std::endl;
}

// GLFW callbacks
static void ErrorCallback(int error, const char* description) {
    std::cerr << "GLFW Error " << error << ": " << description << std::endl;
}

static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
    if (action != GLFW_PRESS) return;

    // F5 - Reload
    if (key == GLFW_KEY_F5) {
        g_needs_reload = true;
    }
    // F12 - Toggle debugger
    else if (key == GLFW_KEY_F12) {
        Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible());
    }
    // Escape - Back navigation
    else if (key == GLFW_KEY_ESCAPE) {
        // Record back button action if recording
        if (g_action_recorder && g_action_recorder->IsRecording()) {
            g_action_recorder->RecordButton("back");
        }

        // In simulator mode with an app running, go back to home
        if (g_simulator_mode && !g_current_app_id.empty()) {
            lua_State* L = Rml::Lua::Interpreter::GetLuaState();
            lua_getglobal(L, "simulator");
            if (lua_istable(L, -1)) {
                lua_getfield(L, -1, "goHome");
                if (lua_isfunction(L, -1)) {
                    if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
                        std::cerr << "Error calling simulator.goHome: " << lua_tostring(L, -1) << std::endl;
                        lua_pop(L, 1);
                    }
                    // Re-populate apps after returning home
                    PopulateSimulatorApps();
                } else {
                    lua_pop(L, 1);
                }
            }
            lua_pop(L, 1);
        } else if (g_context) {
            g_context->ProcessKeyDown(Rml::Input::KI_ESCAPE, 0);
            g_context->ProcessKeyUp(Rml::Input::KI_ESCAPE, 0);
        }
    }
    // R - Toggle recording (in interactive mode)
    else if (key == GLFW_KEY_R && g_test_mode == TestMode::Interactive) {
        if (!g_action_recorder) {
            g_action_recorder = new mosis::testing::ActionRecorder(g_width, g_height);
        }

        if (g_action_recorder->IsRecording()) {
            g_action_recorder->StopRecording();
            // Generate filename with timestamp
            auto now = std::chrono::system_clock::now();
            auto time_t = std::chrono::system_clock::to_time_t(now);
            std::tm tm_buf;
            localtime_s(&tm_buf, &time_t);
            char buffer[64];
            std::strftime(buffer, sizeof(buffer), "recording_%Y%m%d_%H%M%S.json", &tm_buf);
            std::string filename = buffer;
            if (g_action_recorder->SaveToFile(filename)) {
                std::cout << "Recording saved to: " << filename << std::endl;
            } else {
                std::cerr << "Failed to save recording" << std::endl;
            }
        } else {
            g_action_recorder->StartRecording();
            std::cout << "Recording started. Press R again to stop." << std::endl;
        }
    }
}

static void MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) {
    if (!g_context) return;

    double xpos, ypos;
    glfwGetCursorPos(window, &xpos, &ypos);

    // glfwGetCursorPos returns position in screen coordinates (same as window size)
    // which may differ from framebuffer size on high-DPI displays.
    // We need to scale to match the RmlUi context (which matches framebuffer).
    int winWidth, winHeight;
    glfwGetWindowSize(window, &winWidth, &winHeight);

    int fbWidth, fbHeight;
    glfwGetFramebufferSize(window, &fbWidth, &fbHeight);

    // Scale cursor position: screen coords -> framebuffer coords -> RmlUi context
    // On high DPI: winWidth=432, fbWidth=540, g_width=540
    // Cursor in screen space needs to scale to framebuffer/context space
    int mouseX = static_cast<int>(xpos * fbWidth / winWidth);
    int mouseY = static_cast<int>(ypos * fbHeight / winHeight);

    // Debug logging for click events
    std::cout << "MouseButton: " << (action == GLFW_PRESS ? "DOWN" : "UP")
              << " at raw(" << xpos << "," << ypos << ") -> scaled(" << mouseX << "," << mouseY << ")"
              << " win=" << winWidth << "x" << winHeight << " fb=" << fbWidth << "x" << fbHeight << std::endl;
    if (g_log_file.is_open()) {
        g_log_file << "[DEBUG] MouseButton: " << (action == GLFW_PRESS ? "DOWN" : "UP")
                   << " at raw(" << xpos << "," << ypos << ") -> scaled(" << mouseX << "," << mouseY << ")" << std::endl;
        g_log_file.flush();
    }

    int key_modifier = 0;
    if (mods & GLFW_MOD_CONTROL) key_modifier |= Rml::Input::KM_CTRL;
    if (mods & GLFW_MOD_SHIFT) key_modifier |= Rml::Input::KM_SHIFT;
    if (mods & GLFW_MOD_ALT) key_modifier |= Rml::Input::KM_ALT;

    // Update mouse position before processing button event
    g_context->ProcessMouseMove(mouseX, mouseY, key_modifier);

    if (button == GLFW_MOUSE_BUTTON_LEFT) {
        if (action == GLFW_PRESS) {
            if (g_action_recorder && g_action_recorder->IsRecording()) {
                g_action_recorder->RecordMouseDown(mouseX, mouseY);
            }
            // Use touch API for natural scrolling behavior
            g_touch_active = true;
            g_touch_start_x = mouseX;
            g_touch_start_y = mouseY;
            Rml::TouchList touches = {{0, Rml::Vector2f(static_cast<float>(mouseX), static_cast<float>(mouseY))}};
            g_context->ProcessTouchStart(touches, 0);
        } else if (action == GLFW_RELEASE) {
            if (g_action_recorder && g_action_recorder->IsRecording()) {
                g_action_recorder->RecordMouseUp(mouseX, mouseY);
            }
            // End touch for natural scrolling
            g_touch_active = false;
            Rml::TouchList touches = {{0, Rml::Vector2f(static_cast<float>(mouseX), static_cast<float>(mouseY))}};
            g_context->ProcessTouchEnd(touches, 0);
        }
    }
}

static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
    if (!g_context) return;

    // Scale from screen coordinates to framebuffer/RmlUi context coordinates
    int winWidth, winHeight;
    glfwGetWindowSize(window, &winWidth, &winHeight);

    int fbWidth, fbHeight;
    glfwGetFramebufferSize(window, &fbWidth, &fbHeight);

    int mouseX = static_cast<int>(xpos * fbWidth / winWidth);
    int mouseY = static_cast<int>(ypos * fbHeight / winHeight);

    // Always update mouse position for hover effects
    g_context->ProcessMouseMove(mouseX, mouseY, 0);

    // If touch is active (left button pressed), also send touch move for natural scrolling
    if (g_touch_active) {
        Rml::TouchList touches = {{0, Rml::Vector2f(static_cast<float>(mouseX), static_cast<float>(mouseY))}};
        g_context->ProcessTouchMove(touches, 0);
    }
}

static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) {
    if (!g_context) return;
    g_context->ProcessMouseWheel(static_cast<float>(-yoffset), 0);
}

static void FramebufferSizeCallback(GLFWwindow* window, int width, int height) {
    // Update render interface viewport when framebuffer size changes (DPI scaling)
    if (g_render_interface) {
        g_render_interface->SetViewport(width, height);
        std::cout << "Framebuffer resized to: " << width << "x" << height << std::endl;
    }
}

// System interface for RmlUi
class DesktopSystemInterface : public Rml::SystemInterface {
public:
    double GetElapsedTime() override {
        return glfwGetTime();
    }

    bool LogMessage(Rml::Log::Type type, const Rml::String& message) override {
        const char* type_str = "";
        switch (type) {
            case Rml::Log::LT_ERROR:   type_str = "[ERROR]"; break;
            case Rml::Log::LT_WARNING: type_str = "[WARN]"; break;
            case Rml::Log::LT_INFO:    type_str = "[INFO]"; break;
            default: type_str = "[DEBUG]"; break;
        }
        std::cout << type_str << " " << message << std::endl;

        // Also log to file if configured
        if (g_log_file.is_open()) {
            g_log_file << type_str << " " << message << std::endl;
            g_log_file.flush();
        }
        return true;
    }
};

static DesktopSystemInterface g_system_interface;


static void PrintUsage() {
    std::cout << "Usage: mosis-designer [options] [document.rml]\n"
              << "\nOptions:\n"
              << "  --resolution WxH    Set window resolution (default: 540x960)\n"
              << "  --assets PATH       Set assets directory (default: derived from document)\n"
              << "  --log FILE          Write all log messages to file\n"
              << "  --hierarchy FILE    Continuously dump UI hierarchy to JSON\n"
              << "\nSimulator mode:\n"
              << "  --simulator         Run in simulator mode (uses shell by default)\n"
              << "  --no-shell          Disable shell (use direct document loading)\n"
              << "  --test-apps PATH    Path to test-apps directory (default: ./test-apps)\n"
              << "\nTest modes:\n"
              << "  --record FILE       Record user actions to JSON file\n"
              << "  --playback FILE     Playback actions from JSON file\n"
              << "  --screenshot FILE   Take screenshot and exit\n"
              << "  --dump-hierarchy FILE  Dump UI hierarchy to JSON and exit\n"
              << "\nKeys:\n"
              << "  F5    Reload document\n"
              << "  F12   Toggle RmlUi debugger\n"
              << "  ESC   Back navigation\n"
              << "  R     Start/Stop recording (in interactive mode)\n"
              << std::endl;
}

int main(int argc, char* argv[]) {
    std::cout << "Mosis Designer v0.1.0" << std::endl;

    // Parse arguments
    std::string document_path;
    std::string assets_path;  // Will be derived from document path if not specified
    bool assets_path_specified = false;

    for (int i = 1; i < argc; ++i) {
        std::string arg = argv[i];
        if (arg == "--help" || arg == "-h") {
            PrintUsage();
            return 0;
        } else if (arg == "--resolution" && i + 1 < argc) {
            std::string res = argv[++i];
            size_t x = res.find('x');
            if (x != std::string::npos) {
                g_width = std::stoi(res.substr(0, x));
                g_height = std::stoi(res.substr(x + 1));
            }
        } else if (arg == "--assets" && i + 1 < argc) {
            assets_path = argv[++i];
            assets_path_specified = true;
        } else if (arg == "--log" && i + 1 < argc) {
            g_log_file_path = argv[++i];
        } else if (arg == "--hierarchy" && i + 1 < argc) {
            g_hierarchy_file_path = argv[++i];
        } else if (arg == "--record" && i + 1 < argc) {
            g_test_mode = TestMode::Record;
            g_test_output_path = argv[++i];
        } else if (arg == "--playback" && i + 1 < argc) {
            g_test_mode = TestMode::Playback;
            g_test_input_path = argv[++i];
        } else if (arg == "--screenshot-after" && i + 1 < argc) {
            // Capture screenshot after playback completes (used with --playback)
            g_test_output_path = argv[++i];
        } else if (arg == "--screenshot" && i + 1 < argc) {
            g_test_mode = TestMode::Screenshot;
            g_test_output_path = argv[++i];
        } else if (arg == "--dump-hierarchy" && i + 1 < argc) {
            g_test_mode = TestMode::DumpHierarchy;
            g_test_output_path = argv[++i];
        } else if (arg == "--simulator") {
            g_simulator_mode = true;
            g_shell_mode = true;  // Shell mode is implicit in simulator mode
        } else if (arg == "--no-shell") {
            g_shell_mode = false;  // Disable shell (use direct document loading)
        } else if (arg == "--test-apps" && i + 1 < argc) {
            g_test_apps_path = argv[++i];
        } else if (arg[0] != '-') {
            document_path = arg;
        }
    }

    // Simulator mode setup
    if (g_simulator_mode) {
        // Default test-apps path
        if (g_test_apps_path.empty()) {
            // Get executable directory for relative path resolution
            fs::path exe_path = fs::absolute(argv[0]).parent_path();

            // Try to find test-apps relative to executable or working directory
            std::vector<fs::path> search_paths = {
                "test-apps",
                "../test-apps",
                "../../test-apps",
                "../../../test-apps",
                exe_path / "test-apps",
                exe_path / "../test-apps",
                exe_path / "../../test-apps",
                exe_path / "../../../test-apps",     // From build/Release -> MosisService
            };
            for (const auto& path : search_paths) {
                if (fs::exists(path) && fs::is_directory(path)) {
                    g_test_apps_path = fs::absolute(path).string();
                    break;
                }
            }
        } else {
            g_test_apps_path = fs::absolute(g_test_apps_path).string();
        }

        // Find main assets for simulator mode
        // Look for src/main/assets/apps/shell/shell.rml relative to test-apps
        fs::path test_apps_fs = fs::path(g_test_apps_path);
        std::vector<fs::path> shell_search_paths = {
            test_apps_fs.parent_path() / "src" / "main" / "assets" / "apps" / "shell" / "shell.rml",
            fs::path("src/main/assets/apps/shell/shell.rml"),
            fs::absolute("src/main/assets/apps/shell/shell.rml"),
        };
        for (const auto& path : shell_search_paths) {
            if (fs::exists(path)) {
                g_simulator_home_path = fs::absolute(path).string();
                break;
            }
        }

        // Override document path to shell
        if (!g_simulator_home_path.empty()) {
            // Set the main assets path for proper resource loading
            // g_simulator_home_path is now shell.rml, go up 3 levels to get assets path
            g_main_assets_path = fs::path(g_simulator_home_path).parent_path().parent_path().parent_path().string();
            g_shell_path = g_simulator_home_path;
            document_path = g_shell_path;

            std::cout << "Shell mode enabled" << std::endl;
            std::cout << "Shell: " << g_shell_path << std::endl;
            std::cout << "Simulator mode enabled" << std::endl;
            std::cout << "Test apps path: " << g_test_apps_path << std::endl;
        } else {
            std::cerr << "Warning: Could not find shell.rml for simulator" << std::endl;
            g_simulator_mode = false;
        }
    }

    // Default document
    if (document_path.empty()) {
        document_path = "apps/shell/shell.rml";
    }

    // Derive assets path from document path if not specified
    // The document path should be something like .../assets/apps/home/home.rml
    // We want to find the "assets" directory in the path
    if (!assets_path_specified) {
        fs::path doc_path = fs::absolute(document_path);
        fs::path current = doc_path.parent_path();
        fs::path prev_path;

        // Walk up the directory tree looking for a folder that ends with "assets"
        // or contains typical asset folders like "apps", "ui", "fonts"
        while (!current.empty() && current != prev_path) {
            std::string folder_name = current.filename().string();
            if (folder_name == "assets") {
                assets_path = current.string();
                break;
            }
            // Check if this folder contains typical asset subfolders
            if (fs::exists(current / "apps") && fs::exists(current / "ui")) {
                assets_path = current.string();
                break;
            }
            prev_path = current;
            current = current.parent_path();
        }

        // Fall back options if no standard assets folder found
        if (assets_path.empty()) {
            // If the document exists, use its parent directory as assets path
            fs::path doc_path = fs::absolute(document_path);
            if (fs::exists(doc_path)) {
                assets_path = doc_path.parent_path().string();
            } else {
                // Try executable's assets folder
                fs::path exe_path = fs::path(argv[0]).parent_path();
                if (exe_path.empty()) {
                    exe_path = ".";
                }
                fs::path exe_assets = fs::absolute(exe_path) / "assets";
                if (fs::exists(exe_assets)) {
                    assets_path = exe_assets.string();
                } else {
                    // Last resort: current directory
                    assets_path = fs::absolute(".").string();
                }
            }
        }
    }

    // Make document_path absolute if it's relative and exists
    if (!fs::path(document_path).is_absolute()) {
        fs::path abs_doc = fs::absolute(document_path);
        if (fs::exists(abs_doc)) {
            document_path = abs_doc.string();
        }
    }

    // Make assets_path absolute
    assets_path = fs::absolute(assets_path).string();
    std::cout << "Assets path: " << assets_path << std::endl;

    // Determine main assets path (where home.rml lives)
    // This is important for goHome() when running test apps
    g_main_assets_path = assets_path;

    // Check if we're running from test-apps folder
    fs::path assets_fs = fs::path(assets_path);
    if (assets_fs.filename().string().find("com.") == 0 ||
        assets_fs.parent_path().filename() == "test-apps") {
        // Running a test app - find the main assets
        fs::path test_apps_root = assets_fs.parent_path();
        if (test_apps_root.filename() != "test-apps") {
            test_apps_root = test_apps_root.parent_path();
        }
        fs::path main_assets = test_apps_root.parent_path() / "src" / "main" / "assets";
        if (fs::exists(main_assets / "apps" / "shell" / "shell.rml")) {
            g_main_assets_path = main_assets.string();
            std::cout << "Main assets: " << g_main_assets_path << std::endl;
        }
    }
    std::cout << "Resolution: " << g_width << "x" << g_height << std::endl;

    // Print mode info
    if (g_test_mode == TestMode::Interactive) {
        std::cout << "Press F5 to reload, F12 for debugger, ESC for back, R to record" << std::endl;
    } else if (g_test_mode == TestMode::Record) {
        std::cout << "Recording mode: actions will be saved to " << g_test_output_path << std::endl;
    } else if (g_test_mode == TestMode::Playback) {
        std::cout << "Playback mode: playing " << g_test_input_path << std::endl;
    } else if (g_test_mode == TestMode::Screenshot) {
        std::cout << "Screenshot mode: will save to " << g_test_output_path << std::endl;
    } else if (g_test_mode == TestMode::DumpHierarchy) {
        std::cout << "Hierarchy dump mode: will save to " << g_test_output_path << std::endl;
    }

    // Open log file if specified
    if (!g_log_file_path.empty()) {
        g_log_file.open(g_log_file_path, std::ios::out | std::ios::trunc);
        if (g_log_file.is_open()) {
            std::cout << "Logging to: " << g_log_file_path << std::endl;
        } else {
            std::cerr << "Warning: Failed to open log file: " << g_log_file_path << std::endl;
        }
    }

    // Log hierarchy file path if specified
    if (!g_hierarchy_file_path.empty()) {
        std::cout << "Hierarchy dump to: " << g_hierarchy_file_path << std::endl;
    }

    // Initialize GLFW
    glfwSetErrorCallback(ErrorCallback);
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return 1;
    }
    
    // Create window with OpenGL 3.3 core context
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
    
    // Create window with document name in title
    std::string window_title = "Mosis Designer - " + fs::path(document_path).filename().string();
    g_window = glfwCreateWindow(g_width, g_height, window_title.c_str(), nullptr, nullptr);
    if (!g_window) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return 1;
    }
    
    glfwMakeContextCurrent(g_window);
    glfwSwapInterval(1); // VSync
    
    // Set callbacks
    glfwSetKeyCallback(g_window, KeyCallback);
    glfwSetMouseButtonCallback(g_window, MouseButtonCallback);
    glfwSetCursorPosCallback(g_window, CursorPosCallback);
    glfwSetScrollCallback(g_window, ScrollCallback);
    glfwSetFramebufferSizeCallback(g_window, FramebufferSizeCallback);
    
    // Load OpenGL functions with GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cerr << "Failed to initialize GLAD" << std::endl;
        glfwDestroyWindow(g_window);
        glfwTerminate();
        return 1;
    }
    
    std::cout << "OpenGL " << glGetString(GL_VERSION) << std::endl;

    // Get actual framebuffer size (may differ from window size on HiDPI displays)
    int fb_width, fb_height;
    glfwGetFramebufferSize(g_window, &fb_width, &fb_height);
    std::cout << "Framebuffer size: " << fb_width << "x" << fb_height << std::endl;

    // Create platform abstraction and set as global singleton
    auto platform = std::make_unique<mosis::DesktopPlatform>(g_window, g_width, g_height);
    platform->SetAssetsPath(assets_path);
    g_platform = platform.get();
    mosis::SetPlatform(std::move(platform));

    // Initialize RmlUi (use framebuffer size for rendering, logical size for context)
    if (!InitializeRmlUi(assets_path, fb_width, fb_height)) {
        std::cerr << "Failed to initialize RmlUi" << std::endl;
        glfwDestroyWindow(g_window);
        glfwTerminate();
        return 1;
    }
    
    // Load initial document
    if (!LoadDocument(document_path)) {
        std::cerr << "Failed to load document: " << document_path << std::endl;
    }

    // Note: In simulator mode, the Lua script will call simulator.getApps()
    // when the document is ready, so we don't need to call PopulateSimulatorApps() here

    // Initialize test mode components
    if (g_test_mode == TestMode::Record) {
        g_action_recorder = new mosis::testing::ActionRecorder(g_width, g_height);
        g_action_recorder->StartRecording();
        std::cout << "Recording started. Close window to save." << std::endl;
    } else if (g_test_mode == TestMode::Playback) {
        g_action_player = new mosis::testing::ActionPlayer(g_context);
        if (!g_action_player->LoadFromFile(g_test_input_path)) {
            std::cerr << "Failed to load actions from: " << g_test_input_path << std::endl;
            delete g_action_player;
            g_action_player = nullptr;
        } else {
            g_action_player->Start();
            std::cout << "Playback started..." << std::endl;
        }
    }

    // Set up hot-reload (only in interactive mode)
    if (g_test_mode == TestMode::Interactive) {
        g_hot_reload = new mosis::HotReload(assets_path, []() {
            g_needs_reload = true;
        });
        g_hot_reload->Start();
        std::cout << "Hot-reload enabled for: " << assets_path << std::endl;
    }

    // For screenshot/hierarchy modes, render one frame then capture
    if (g_test_mode == TestMode::Screenshot || g_test_mode == TestMode::DumpHierarchy) {
        // Render one frame
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        if (g_context) {
            g_context->Update();
            g_render_interface->BeginFrame();
            g_context->Render();
            g_render_interface->EndFrame(0);
        }

        // Capture screenshot BEFORE swap (glReadPixels reads from back buffer)
        if (g_test_mode == TestMode::Screenshot) {
            mosis::testing::VisualCapture capture(fb_width, fb_height);
            if (capture.CaptureScreenshot(g_test_output_path)) {
                std::cout << "Screenshot saved to: " << g_test_output_path << std::endl;
            } else {
                std::cerr << "Failed to save screenshot" << std::endl;
            }
        } else {
            mosis::testing::UIInspector inspector(g_context);
            if (inspector.SaveHierarchy(g_test_output_path)) {
                std::cout << "UI hierarchy saved to: " << g_test_output_path << std::endl;
            } else {
                std::cerr << "Failed to save hierarchy" << std::endl;
            }
        }

        glfwSwapBuffers(g_window);

        // Cleanup and exit
        ShutdownRmlUi();
        glfwDestroyWindow(g_window);
        glfwTerminate();
        return 0;
    }

    // Main loop
    while (!glfwWindowShouldClose(g_window)) {
        glfwPollEvents();

        // Handle hot-reload
        if (g_needs_reload) {
            g_needs_reload = false;
            ReloadDocument();
        }

        // Update action playback
        if (g_action_player && g_action_player->IsPlaying()) {
            g_action_player->Update();

            // Check if playback finished
            if (g_action_player->IsFinished()) {
                std::cout << "Playback complete" << std::endl;

                // Capture screenshot if --screenshot-after was specified
                if (!g_test_output_path.empty()) {
                    int fb_width, fb_height;
                    glfwGetFramebufferSize(g_window, &fb_width, &fb_height);
                    mosis::testing::VisualCapture capture(fb_width, fb_height);
                    if (capture.CaptureScreenshot(g_test_output_path)) {
                        std::cout << "Screenshot saved to: " << g_test_output_path << std::endl;
                    } else {
                        std::cerr << "Failed to save screenshot" << std::endl;
                    }
                }

                // Exit after playback
                glfwSetWindowShouldClose(g_window, GLFW_TRUE);
            }
        }

        // Clear
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // Update sandbox (process timers)
        if (g_sandbox) {
            g_sandbox->Update();
        }

        // Update and render
        if (g_context) {
            g_context->Update();
            g_render_interface->BeginFrame();
            g_context->Render();
            g_render_interface->EndFrame(0);
        }

        // Dump UI hierarchy each frame if configured
        if (!g_hierarchy_file_path.empty() && g_context) {
            mosis::testing::UIInspector inspector(g_context);
            inspector.SetCurrentScreen(g_current_screen_url);
            inspector.SaveHierarchy(g_hierarchy_file_path);
        }

        glfwSwapBuffers(g_window);
    }

    // Save recording if in record mode
    if (g_action_recorder && g_action_recorder->IsRecording()) {
        g_action_recorder->StopRecording();
        if (g_action_recorder->SaveToFile(g_test_output_path)) {
            std::cout << "Recording saved to: " << g_test_output_path << std::endl;
        } else {
            std::cerr << "Failed to save recording" << std::endl;
        }
    }

    // Cleanup
    delete g_action_recorder;
    delete g_action_player;
    if (g_hot_reload) {
        g_hot_reload->Stop();
        delete g_hot_reload;
    }
    if (g_log_file.is_open()) {
        g_log_file.close();
    }
    g_sandbox.reset();
    ShutdownRmlUi();
    glfwDestroyWindow(g_window);
    glfwTerminate();

    return 0;
}


bool InitializeRmlUi(const std::string& assets_path, int fb_width, int fb_height) {
    // Create render interface
    g_render_interface = new RenderInterface_GL3();
    if (!*g_render_interface) {
        std::cerr << "Failed to create GL3 render interface" << std::endl;
        return false;
    }
    // Use framebuffer size (physical pixels) for the render interface viewport
    g_render_interface->SetViewport(fb_width, fb_height);

    // Initialize RmlUi
    Rml::SetSystemInterface(&g_system_interface);
    Rml::SetFileInterface(&g_platform->GetFileInterface());
    Rml::SetRenderInterface(g_render_interface);
    
    if (!Rml::Initialise()) {
        std::cerr << "Failed to initialize RmlUi" << std::endl;
        return false;
    }
    
    // Initialize Lua bindings
    Rml::Lua::Initialise();

    // Get Lua state
    lua_State* L = Rml::Lua::Interpreter::GetLuaState();

    // Initialize sandbox with APIs (timers, JSON, crypto, fs)
    mosis::DesktopSandboxConfig sandbox_config;
    sandbox_config.data_root = assets_path + "/sandbox_data";
    g_sandbox = std::make_unique<mosis::DesktopSandbox>(sandbox_config);
    g_sandbox->RegisterAPIs(L);

    // Register loadScreen function for navigation
    lua_pushcfunction(L, [](lua_State* L) -> int {
        const char* path = luaL_checkstring(L, 1);

        if (!g_context) {
            lua_pushboolean(L, false);
            return 1;
        }

        // Close existing documents (except debugger)
        while (g_context->GetNumDocuments() > 1) {
            auto* doc = g_context->GetDocument(0);
            if (doc && doc->GetSourceURL().find("__rmlui") == std::string::npos) {
                doc->Close();
            } else {
                break;
            }
        }

        // Load new document
        auto* document = g_context->LoadDocument(path);
        if (document) {
            document->Show();
            SetLuaDocumentGlobal(document);  // Set 'document' global for Lua scripts
            g_current_document_path = path;
            g_current_screen_url = path;  // Track current screen for hierarchy dump
            // Log using RmlUi logging so it appears in log file
            Rml::Log::Message(Rml::Log::LT_INFO, "Loaded screen: %s", path);
            lua_pushboolean(L, true);
        } else {
            Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load screen: %s", path);
            lua_pushboolean(L, false);
        }
        return 1;
    });
    lua_setglobal(L, "loadScreen");

    // Register goHome function to return to home screen
    lua_pushcfunction(L, [](lua_State* L) -> int {
        if (!g_context) {
            lua_pushboolean(L, false);
            return 1;
        }

        std::cout << "goHome called - returning to home screen" << std::endl;

        // Reset sandbox back to home context
        if (g_sandbox && !g_current_app_id.empty()) {
            g_sandbox->UnregisterAPIs(L);
            g_sandbox->Reset();

            mosis::DesktopSandboxConfig config;
            config.app_id = "com.mosis.home";
            config.data_root = g_main_assets_path + "/sandbox_data";
            g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
            g_sandbox->RegisterAPIs(L);
            std::cout << "Sandbox reset to home context" << std::endl;
        }

        // Close existing documents (except debugger)
        while (g_context->GetNumDocuments() > 1) {
            auto* doc = g_context->GetDocument(0);
            if (doc && doc->GetSourceURL().find("__rmlui") == std::string::npos) {
                doc->Close();
            } else {
                break;
            }
        }

        // Load shell (which shows home by default)
        std::string home_path = (fs::path(g_main_assets_path) / "apps" / "shell" / "shell.rml").string();
        std::cout << "Loading shell from: " << home_path << std::endl;

        auto* document = g_context->LoadDocument(home_path);
        if (document) {
            document->Show();
            SetLuaDocumentGlobal(document);  // Set 'document' global for Lua scripts
            g_current_document_path = home_path;
            g_current_screen_url = home_path;
            g_current_app_id = "";  // Clear current app
            Rml::Log::Message(Rml::Log::LT_INFO, "Returned to home screen");
            lua_pushboolean(L, true);
        } else {
            Rml::Log::Message(Rml::Log::LT_ERROR, "Failed to load home screen from: %s", home_path.c_str());
            lua_pushboolean(L, false);
        }
        return 1;
    });
    lua_setglobal(L, "goHome");

    // Register switchAppSandbox function for third-party app launching
    lua_pushcfunction(L, [](lua_State* L) -> int {
        const char* app_id = luaL_checkstring(L, 1);
        const char* install_path = luaL_checkstring(L, 2);

        std::cout << "switchAppSandbox called for: " << app_id << " at " << install_path << std::endl;

        // Store current app ID
        g_current_app_id = app_id;

        // Reset sandbox for the new app
        if (g_sandbox) {
            g_sandbox->UnregisterAPIs(L);
            g_sandbox->Reset();

            // Re-configure sandbox with app-specific data root
            mosis::DesktopSandboxConfig config;
            config.app_id = app_id;
            config.data_root = std::string(install_path) + "/sandbox_data";
            g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
            g_sandbox->RegisterAPIs(L);
            std::cout << "Sandbox switched to: " << app_id << std::endl;
        }

        lua_pushboolean(L, true);
        return 1;
    });
    lua_setglobal(L, "switchAppSandbox");

    // Register loadAppContent function for shell-based app loading
    // loadAppContent(element, path) - loads RML content into an element's inner_rml
    lua_pushcfunction(L, [](lua_State* L) -> int {
        // Get element from first argument (RmlUi element userdata)
        Rml::Element* element = Rml::Lua::LuaType<Rml::Element>::check(L, 1);
        const char* path = luaL_checkstring(L, 2);

        if (!element) {
            std::cerr << "loadAppContent: Invalid element" << std::endl;
            lua_pushboolean(L, false);
            return 1;
        }

        // Resolve path relative to assets directory
        std::string full_path;
        if (fs::path(path).is_absolute()) {
            full_path = path;
        } else {
            full_path = (fs::path(g_main_assets_path) / path).string();
        }

        // Read file content
        std::ifstream file(full_path);
        if (!file.is_open()) {
            std::cerr << "loadAppContent: Cannot open file: " << full_path << std::endl;
            lua_pushboolean(L, false);
            return 1;
        }

        std::stringstream buffer;
        buffer << file.rdbuf();
        std::string content = buffer.str();

        // Set as inner_rml
        element->SetInnerRML(content);

        Rml::Log::Message(Rml::Log::LT_INFO, "Loaded app content from: %s", path);
        lua_pushboolean(L, true);
        return 1;
    });
    lua_setglobal(L, "loadAppContent");

    std::cout << "Registered Lua loadScreen, goHome, switchAppSandbox, and loadAppContent functions" << std::endl;

    // Register simulator API (if in simulator mode)
    if (g_simulator_mode) {
        // Create simulator table
        lua_newtable(L);

        // simulator.launchApp(entry, path, id)
        lua_pushcfunction(L, [](lua_State* L) -> int {
            const char* entry = luaL_checkstring(L, 1);
            const char* app_path = luaL_checkstring(L, 2);
            const char* app_id = luaL_checkstring(L, 3);

            if (!g_context) {
                lua_pushboolean(L, false);
                return 1;
            }

            std::cout << "Simulator: Launching app " << app_id << std::endl;
            std::cout << "  Entry: " << entry << std::endl;
            std::cout << "  Path: " << app_path << std::endl;

            // Store current app ID
            g_current_app_id = app_id;

            // Reset sandbox for the new app
            if (g_sandbox) {
                g_sandbox->UnregisterAPIs(L);
                g_sandbox->Reset();

                // Re-configure sandbox with app-specific data root
                mosis::DesktopSandboxConfig config;
                config.app_id = app_id;
                config.data_root = std::string(app_path) + "/sandbox_data";
                g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
                g_sandbox->RegisterAPIs(L);
            }

            // Close existing documents
            while (g_context->GetNumDocuments() > 0) {
                auto* doc = g_context->GetDocument(0);
                if (doc) doc->Close();
            }

            // Load the app
            auto* document = g_context->LoadDocument(entry);
            if (document) {
                document->Show();
                SetLuaDocumentGlobal(document);  // Set 'document' global for Lua scripts
                g_current_document_path = entry;
                g_current_screen_url = entry;
                std::cout << "Simulator: App launched successfully" << std::endl;
                lua_pushboolean(L, true);
            } else {
                std::cerr << "Simulator: Failed to load app: " << entry << std::endl;
                g_current_app_id.clear();
                lua_pushboolean(L, false);
            }
            return 1;
        });
        lua_setfield(L, -2, "launchApp");

        // simulator.goHome()
        lua_pushcfunction(L, [](lua_State* L) -> int {
            if (!g_context || g_simulator_home_path.empty()) {
                lua_pushboolean(L, false);
                return 1;
            }

            std::cout << "Simulator: Returning to home" << std::endl;
            g_current_app_id.clear();

            // Reset sandbox to default state
            if (g_sandbox) {
                g_sandbox->UnregisterAPIs(L);
                g_sandbox->Reset();

                mosis::DesktopSandboxConfig config;
                config.data_root = "./sandbox_data";
                g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
                g_sandbox->RegisterAPIs(L);
            }

            // Close existing documents
            while (g_context->GetNumDocuments() > 0) {
                auto* doc = g_context->GetDocument(0);
                if (doc) doc->Close();
            }

            // Load simulator home
            auto* document = g_context->LoadDocument(g_simulator_home_path);
            if (document) {
                document->Show();
                SetLuaDocumentGlobal(document);  // Set 'document' global for Lua scripts
                g_current_document_path = g_simulator_home_path;
                g_current_screen_url = g_simulator_home_path;

                // Re-populate apps in home screen
                // This will be done by calling setApps from C++ after document loads
                lua_pushboolean(L, true);
            } else {
                lua_pushboolean(L, false);
            }
            return 1;
        });
        lua_setfield(L, -2, "goHome");

        // simulator.isInApp()
        lua_pushcfunction(L, [](lua_State* L) -> int {
            lua_pushboolean(L, !g_current_app_id.empty());
            return 1;
        });
        lua_setfield(L, -2, "isInApp");

        // simulator.getCurrentAppId()
        lua_pushcfunction(L, [](lua_State* L) -> int {
            if (g_current_app_id.empty()) {
                lua_pushnil(L);
            } else {
                lua_pushstring(L, g_current_app_id.c_str());
            }
            return 1;
        });
        lua_setfield(L, -2, "getCurrentAppId");

        // simulator.getApps() - returns the list of discovered apps
        lua_pushcfunction(L, [](lua_State* L) -> int {
            // Discover apps if not already done
            if (g_discovered_apps.empty() && !g_test_apps_path.empty()) {
                g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path);
            }

            // Create apps table
            lua_newtable(L);
            int app_index = 1;
            for (const auto& app : g_discovered_apps) {
                lua_pushinteger(L, app_index++);
                lua_newtable(L);

                lua_pushstring(L, app.id.c_str());
                lua_setfield(L, -2, "id");

                lua_pushstring(L, app.name.c_str());
                lua_setfield(L, -2, "name");

                lua_pushstring(L, app.GetIconPath().c_str());
                lua_setfield(L, -2, "icon");

                lua_pushstring(L, app.GetEntryPath().c_str());
                lua_setfield(L, -2, "entry");

                lua_pushstring(L, app.app_path.c_str());
                lua_setfield(L, -2, "path");

                lua_pushstring(L, app.description.c_str());
                lua_setfield(L, -2, "description");

                lua_settable(L, -3);  // apps[index] = app_table
            }
            return 1;
        });
        lua_setfield(L, -2, "getApps");

        lua_setglobal(L, "simulator");
        std::cout << "Registered simulator Lua API" << std::endl;

        // Also register mosis.apps API for compatibility with home.lua
        // Create mosis table (or get existing)
        lua_getglobal(L, "mosis");
        if (lua_isnil(L, -1)) {
            lua_pop(L, 1);
            lua_newtable(L);
        }

        // Create mosis.apps table
        lua_newtable(L);

        // mosis.apps.getInstalled() - returns installed apps
        lua_pushcfunction(L, [](lua_State* L) -> int {
            // Discover apps if not already done
            if (g_discovered_apps.empty() && !g_test_apps_path.empty()) {
                g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path);
                std::cout << "Discovered " << g_discovered_apps.size() << " apps in " << g_test_apps_path << std::endl;
            }

            // Create apps table (array format expected by home.lua)
            lua_newtable(L);
            int app_index = 1;
            for (const auto& app : g_discovered_apps) {
                lua_pushinteger(L, app_index++);
                lua_newtable(L);

                lua_pushstring(L, app.name.c_str());
                lua_setfield(L, -2, "name");

                lua_pushstring(L, app.id.c_str());
                lua_setfield(L, -2, "package_id");

                lua_pushstring(L, app.GetIconPath().c_str());
                lua_setfield(L, -2, "icon");

                lua_pushboolean(L, app.is_system_app);
                lua_setfield(L, -2, "is_system_app");

                lua_pushstring(L, app.app_path.c_str());
                lua_setfield(L, -2, "install_path");

                lua_pushstring(L, app.entry.c_str());
                lua_setfield(L, -2, "entry_point");

                lua_settable(L, -3);  // apps[index] = app_table
            }
            return 1;
        });
        lua_setfield(L, -2, "getInstalled");

        // mosis.apps.launch(package_id) - launch an app
        lua_pushcfunction(L, [](lua_State* L) -> int {
            const char* package_id = luaL_checkstring(L, 1);

            // Find the app
            for (const auto& app : g_discovered_apps) {
                if (app.id == package_id) {
                    std::cout << "mosis.apps.launch: Starting " << package_id << std::endl;
                    g_current_app_id = package_id;

                    // Reset sandbox for the new app
                    if (g_sandbox) {
                        g_sandbox->UnregisterAPIs(L);
                        g_sandbox->Reset();

                        mosis::DesktopSandboxConfig config;
                        config.app_id = package_id;
                        config.data_root = app.app_path + "/sandbox_data";
                        g_sandbox = std::make_unique<mosis::DesktopSandbox>(config);
                        g_sandbox->RegisterAPIs(L);
                    }

                    lua_pushboolean(L, true);
                    return 1;
                }
            }

            std::cerr << "mosis.apps.launch: App not found: " << package_id << std::endl;
            lua_pushboolean(L, false);
            return 1;
        });
        lua_setfield(L, -2, "launch");

        lua_setfield(L, -2, "apps");  // mosis.apps = apps_table
        lua_setglobal(L, "mosis");
        std::cout << "Registered mosis.apps Lua API" << std::endl;
    }

    // Load fonts - search for fonts directory in multiple locations
    std::string fonts_root;
    std::vector<std::string> font_search_paths = {
        assets_path + "/fonts",
        assets_path + "/../src/main/assets/fonts",  // If assets_path is test-app dir
        assets_path + "/../../src/main/assets/fonts",
        std::filesystem::absolute("src/main/assets/fonts").string(),
        std::filesystem::absolute("assets/fonts").string(),  // For simulator mode
    };
    for (const auto& search_path : font_search_paths) {
        if (std::filesystem::exists(search_path + "/LatoLatin-Regular.ttf")) {
            fonts_root = search_path;
            break;
        }
    }
    if (fonts_root.empty()) {
        std::cerr << "Warning: Could not find fonts directory" << std::endl;
    } else {
        std::cout << "Fonts path: " << fonts_root << std::endl;
        std::vector<std::string> fonts = {
            fonts_root + "/LatoLatin-Regular.ttf",
            fonts_root + "/LatoLatin-Bold.ttf",
            fonts_root + "/LatoLatin-Italic.ttf",
        };
        for (const auto& font : fonts) {
            if (!Rml::LoadFontFace(font)) {
                std::cerr << "Warning: Failed to load font: " << font << std::endl;
            }
        }
    }
    
    // Create context
    g_context = Rml::CreateContext("main", Rml::Vector2i(g_width, g_height));
    if (!g_context) {
        std::cerr << "Failed to create RmlUi context" << std::endl;
        return false;
    }
    
    // Initialize debugger
    Rml::Debugger::Initialise(g_context);
    
    return true;
}

void ShutdownRmlUi() {
    if (g_context) {
        Rml::RemoveContext("main");
        g_context = nullptr;
    }

    // Rml::Lua is shut down automatically when Rml::Shutdown() is called
    Rml::Shutdown();

    delete g_render_interface;
    g_render_interface = nullptr;

    // Platform (and its file interface) is managed by the global singleton
}

bool LoadDocument(const std::string& path) {
    if (!g_context) return false;
    
    // Close existing documents
    while (g_context->GetNumDocuments() > 0) {
        auto* doc = g_context->GetDocument(0);
        if (doc) doc->Close();
    }
    
    // Load new document
    auto* document = g_context->LoadDocument(path);
    if (!document) {
        std::cerr << "Failed to load: " << path << std::endl;
        return false;
    }

    document->Show();
    SetLuaDocumentGlobal(document);  // Set 'document' global for Lua scripts
    g_current_document_path = path;
    g_current_screen_url = path;  // Track current screen for hierarchy dump
    std::cout << "Loaded: " << path << std::endl;

    return true;
}

void ReloadDocument() {
    std::cout << "Reloading..." << std::endl;

    // Reset sandbox (clear timers, re-register APIs)
    if (g_sandbox) {
        lua_State* L = Rml::Lua::Interpreter::GetLuaState();
        g_sandbox->UnregisterAPIs(L);
        g_sandbox->Reset();
        g_sandbox->RegisterAPIs(L);
    }

    // Reload stylesheets
    for (int i = 0; i < g_context->GetNumDocuments(); ++i) {
        auto* doc = g_context->GetDocument(i);
        if (doc) {
            doc->ReloadStyleSheet();
        }
    }

    // Reload document
    if (!g_current_document_path.empty()) {
        LoadDocument(g_current_document_path);
    }

    // Re-populate simulator apps if on home screen
    if (g_simulator_mode && g_current_app_id.empty()) {
        PopulateSimulatorApps();
    }

    std::cout << "Reload complete" << std::endl;
}

void PopulateSimulatorApps() {
    if (!g_simulator_mode || g_test_apps_path.empty()) return;

    // Discover apps
    g_discovered_apps = mosis::AppDiscovery::DiscoverApps(g_test_apps_path);
    std::cout << "Discovered " << g_discovered_apps.size() << " apps" << std::endl;

    // Get Lua state
    lua_State* L = Rml::Lua::Interpreter::GetLuaState();

    // Call setApps(apps) in Lua
    lua_getglobal(L, "setApps");
    if (!lua_isfunction(L, -1)) {
        lua_pop(L, 1);
        std::cerr << "Warning: setApps function not found in Lua" << std::endl;
        return;
    }

    // Create apps table
    lua_newtable(L);
    int app_index = 1;
    for (const auto& app : g_discovered_apps) {
        lua_pushinteger(L, app_index++);
        lua_newtable(L);

        lua_pushstring(L, app.id.c_str());
        lua_setfield(L, -2, "id");

        lua_pushstring(L, app.name.c_str());
        lua_setfield(L, -2, "name");

        lua_pushstring(L, app.GetIconPath().c_str());
        lua_setfield(L, -2, "icon");

        lua_pushstring(L, app.GetEntryPath().c_str());
        lua_setfield(L, -2, "entry");

        lua_pushstring(L, app.app_path.c_str());
        lua_setfield(L, -2, "path");

        lua_pushstring(L, app.description.c_str());
        lua_setfield(L, -2, "description");

        lua_settable(L, -3);  // apps[index] = app_table
    }

    // Call setApps(apps)
    if (lua_pcall(L, 1, 0, 0) != LUA_OK) {
        std::cerr << "Error calling setApps: " << lua_tostring(L, -1) << std::endl;
        lua_pop(L, 1);
    }
}
