#include "McRFPy_API.h"
#include "McRFPy_Automation.h"
#include "McRFPy_Libtcod.h"
#include "McRFPy_Doc.h"
#include "platform.h"
#include "PyAnimation.h"
#include "PyDrawable.h"
#include "PyTimer.h"
#include "PyWindow.h"
#include "PySceneObject.h"
#include "PyFOV.h"
#include "GameEngine.h"
#include "ImGuiConsole.h"
#include "BenchmarkLogger.h"
#include "UI.h"
#include "UILine.h"
#include "UICircle.h"
#include "UIArc.h"
#include "GridLayers.h"
#include "Resources.h"
#include "PyScene.h"
#include <filesystem>
#include <cstring>
#include <libtcod.h>

std::vector<sf::SoundBuffer>* McRFPy_API::soundbuffers = nullptr;
sf::Music* McRFPy_API::music = nullptr;
sf::Sound* McRFPy_API::sfx = nullptr;

std::shared_ptr<PyFont> McRFPy_API::default_font;
std::shared_ptr<PyTexture> McRFPy_API::default_texture;
PyObject* McRFPy_API::mcrf_module;

// Exception handling state
std::atomic<bool> McRFPy_API::exception_occurred{false};
std::atomic<int> McRFPy_API::exit_code{0};

static PyMethodDef mcrfpyMethods[] = {

    {"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS,
     MCRF_FUNCTION(createSoundBuffer,
         MCRF_SIG("(filename: str)", "int"),
         MCRF_DESC("Load a sound effect from a file and return its buffer ID."),
         MCRF_ARGS_START
         MCRF_ARG("filename", "Path to the sound file (WAV, OGG, FLAC)")
         MCRF_RETURNS("int: Buffer ID for use with playSound()")
         MCRF_RAISES("RuntimeError", "If the file cannot be loaded")
     )},
	{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS,
     MCRF_FUNCTION(loadMusic,
         MCRF_SIG("(filename: str)", "None"),
         MCRF_DESC("Load and immediately play background music from a file."),
         MCRF_ARGS_START
         MCRF_ARG("filename", "Path to the music file (WAV, OGG, FLAC)")
         MCRF_RETURNS("None")
         MCRF_NOTE("Only one music track can play at a time. Loading new music stops the current track.")
     )},
	{"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS,
     MCRF_FUNCTION(setMusicVolume,
         MCRF_SIG("(volume: int)", "None"),
         MCRF_DESC("Set the global music volume."),
         MCRF_ARGS_START
         MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)")
         MCRF_RETURNS("None")
     )},
	{"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS,
     MCRF_FUNCTION(setSoundVolume,
         MCRF_SIG("(volume: int)", "None"),
         MCRF_DESC("Set the global sound effects volume."),
         MCRF_ARGS_START
         MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)")
         MCRF_RETURNS("None")
     )},
	{"playSound", McRFPy_API::_playSound, METH_VARARGS,
     MCRF_FUNCTION(playSound,
         MCRF_SIG("(buffer_id: int)", "None"),
         MCRF_DESC("Play a sound effect using a previously loaded buffer."),
         MCRF_ARGS_START
         MCRF_ARG("buffer_id", "Sound buffer ID returned by createSoundBuffer()")
         MCRF_RETURNS("None")
         MCRF_RAISES("RuntimeError", "If the buffer ID is invalid")
     )},
	{"getMusicVolume", McRFPy_API::_getMusicVolume, METH_NOARGS,
     MCRF_FUNCTION(getMusicVolume,
         MCRF_SIG("()", "int"),
         MCRF_DESC("Get the current music volume level."),
         MCRF_RETURNS("int: Current volume (0-100)")
     )},
	{"getSoundVolume", McRFPy_API::_getSoundVolume, METH_NOARGS,
     MCRF_FUNCTION(getSoundVolume,
         MCRF_SIG("()", "int"),
         MCRF_DESC("Get the current sound effects volume level."),
         MCRF_RETURNS("int: Current volume (0-100)")
     )},

    {"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS,
     MCRF_FUNCTION(sceneUI,
         MCRF_SIG("(scene: str = None)", "list"),
         MCRF_DESC("Get all UI elements for a scene."),
         MCRF_ARGS_START
         MCRF_ARG("scene", "Scene name. If None, uses current scene")
         MCRF_RETURNS("list: All UI elements (Frame, Caption, Sprite, Grid) in the scene")
         MCRF_RAISES("KeyError", "If the specified scene doesn't exist")
     )},

    {"currentScene", McRFPy_API::_currentScene, METH_NOARGS,
     MCRF_FUNCTION(currentScene,
         MCRF_SIG("()", "str"),
         MCRF_DESC("Get the name of the currently active scene."),
         MCRF_RETURNS("str: Name of the current scene")
     )},
    {"setScene", McRFPy_API::_setScene, METH_VARARGS,
     MCRF_FUNCTION(setScene,
         MCRF_SIG("(scene: str, transition: str = None, duration: float = 0.0)", "None"),
         MCRF_DESC("Switch to a different scene with optional transition effect."),
         MCRF_ARGS_START
         MCRF_ARG("scene", "Name of the scene to switch to")
         MCRF_ARG("transition", "Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')")
         MCRF_ARG("duration", "Transition duration in seconds (default: 0.0 for instant)")
         MCRF_RETURNS("None")
         MCRF_RAISES("KeyError", "If the scene doesn't exist")
         MCRF_RAISES("ValueError", "If the transition type is invalid")
     )},
    {"createScene", McRFPy_API::_createScene, METH_VARARGS,
     MCRF_FUNCTION(createScene,
         MCRF_SIG("(name: str)", "None"),
         MCRF_DESC("Create a new empty scene."),
         MCRF_ARGS_START
         MCRF_ARG("name", "Unique name for the new scene")
         MCRF_RETURNS("None")
         MCRF_RAISES("ValueError", "If a scene with this name already exists")
         MCRF_NOTE("The scene is created but not made active. Use setScene() to switch to it.")
     )},
    {"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS,
     MCRF_FUNCTION(keypressScene,
         MCRF_SIG("(handler: callable)", "None"),
         MCRF_DESC("Set the keyboard event handler for the current scene."),
         MCRF_ARGS_START
         MCRF_ARG("handler", "Callable that receives (key_name: str, is_pressed: bool)")
         MCRF_RETURNS("None")
         MCRF_NOTE("Example: def on_key(key, pressed): if key == 'A' and pressed: print('A key pressed') mcrfpy.keypressScene(on_key)")
     )},

    {"setTimer", McRFPy_API::_setTimer, METH_VARARGS,
     MCRF_FUNCTION(setTimer,
         MCRF_SIG("(name: str, handler: callable, interval: int)", "None"),
         MCRF_DESC("Create or update a recurring timer."),
         MCRF_ARGS_START
         MCRF_ARG("name", "Unique identifier for the timer")
         MCRF_ARG("handler", "Function called with (runtime: float) parameter")
         MCRF_ARG("interval", "Time between calls in milliseconds")
         MCRF_RETURNS("None")
         MCRF_NOTE("If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.")
     )},
    {"delTimer", McRFPy_API::_delTimer, METH_VARARGS,
     MCRF_FUNCTION(delTimer,
         MCRF_SIG("(name: str)", "None"),
         MCRF_DESC("Stop and remove a timer."),
         MCRF_ARGS_START
         MCRF_ARG("name", "Timer identifier to remove")
         MCRF_RETURNS("None")
         MCRF_NOTE("No error is raised if the timer doesn't exist.")
     )},
    {"step", McRFPy_API::_step, METH_VARARGS,
     MCRF_FUNCTION(step,
         MCRF_SIG("(dt: float = None)", "float"),
         MCRF_DESC("Advance simulation time (headless mode only)."),
         MCRF_ARGS_START
         MCRF_ARG("dt", "Time to advance in seconds. If None, advances to the next scheduled event (timer/animation).")
         MCRF_RETURNS("float: Actual time advanced in seconds. Returns 0.0 in windowed mode.")
         MCRF_NOTE("In windowed mode, this is a no-op and returns 0.0. Use this for deterministic simulation control in headless/testing scenarios.")
     )},
    {"exit", McRFPy_API::_exit, METH_NOARGS,
     MCRF_FUNCTION(exit,
         MCRF_SIG("()", "None"),
         MCRF_DESC("Cleanly shut down the game engine and exit the application."),
         MCRF_RETURNS("None")
         MCRF_NOTE("This immediately closes the window and terminates the program.")
     )},
    {"setScale", McRFPy_API::_setScale, METH_VARARGS,
     MCRF_FUNCTION(setScale,
         MCRF_SIG("(multiplier: float)", "None"),
         MCRF_DESC("Scale the game window size."),
         MCRF_ARGS_START
         MCRF_ARG("multiplier", "Scale factor (e.g., 2.0 for double size)")
         MCRF_RETURNS("None")
         MCRF_NOTE("The internal resolution remains 1024x768, but the window is scaled. This is deprecated - use Window.resolution instead.")
     )},

    {"find", McRFPy_API::_find, METH_VARARGS,
     MCRF_FUNCTION(find,
         MCRF_SIG("(name: str, scene: str = None)", "UIDrawable | None"),
         MCRF_DESC("Find the first UI element with the specified name."),
         MCRF_ARGS_START
         MCRF_ARG("name", "Exact name to search for")
         MCRF_ARG("scene", "Scene to search in (default: current scene)")
         MCRF_RETURNS("Frame, Caption, Sprite, Grid, or Entity if found; None otherwise")
         MCRF_NOTE("Searches scene UI elements and entities within grids.")
     )},
    {"findAll", McRFPy_API::_findAll, METH_VARARGS,
     MCRF_FUNCTION(findAll,
         MCRF_SIG("(pattern: str, scene: str = None)", "list"),
         MCRF_DESC("Find all UI elements matching a name pattern."),
         MCRF_ARGS_START
         MCRF_ARG("pattern", "Name pattern with optional wildcards (* matches any characters)")
         MCRF_ARG("scene", "Scene to search in (default: current scene)")
         MCRF_RETURNS("list: All matching UI elements and entities")
         MCRF_NOTE("Example: findAll('enemy*') finds all elements starting with 'enemy', findAll('*_button') finds all elements ending with '_button'")
     )},

    {"getMetrics", McRFPy_API::_getMetrics, METH_NOARGS,
     MCRF_FUNCTION(getMetrics,
         MCRF_SIG("()", "dict"),
         MCRF_DESC("Get current performance metrics."),
         MCRF_RETURNS("dict: Performance data with keys: frame_time (last frame duration in seconds), avg_frame_time (average frame time), fps (frames per second), draw_calls (number of draw calls), ui_elements (total UI element count), visible_elements (visible element count), current_frame (frame counter), runtime (total runtime in seconds)")
     )},

    {"setDevConsole", McRFPy_API::_setDevConsole, METH_VARARGS,
     MCRF_FUNCTION(setDevConsole,
         MCRF_SIG("(enabled: bool)", "None"),
         MCRF_DESC("Enable or disable the developer console overlay."),
         MCRF_ARGS_START
         MCRF_ARG("enabled", "True to enable the console (default), False to disable")
         MCRF_RETURNS("None")
         MCRF_NOTE("When disabled, the grave/tilde key will not open the console. Use this to ship games without debug features.")
     )},

    {"start_benchmark", McRFPy_API::_startBenchmark, METH_NOARGS,
     MCRF_FUNCTION(start_benchmark,
         MCRF_SIG("()", "None"),
         MCRF_DESC("Start capturing benchmark data to a file."),
         MCRF_RETURNS("None")
         MCRF_RAISES("RuntimeError", "If a benchmark is already running")
         MCRF_NOTE("Benchmark filename is auto-generated from PID and timestamp. Use end_benchmark() to stop and get filename.")
     )},

    {"end_benchmark", McRFPy_API::_endBenchmark, METH_NOARGS,
     MCRF_FUNCTION(end_benchmark,
         MCRF_SIG("()", "str"),
         MCRF_DESC("Stop benchmark capture and write data to JSON file."),
         MCRF_RETURNS("str: The filename of the written benchmark data")
         MCRF_RAISES("RuntimeError", "If no benchmark is currently running")
         MCRF_NOTE("Returns the auto-generated filename (e.g., 'benchmark_12345_20250528_143022.json')")
     )},

    {"log_benchmark", McRFPy_API::_logBenchmark, METH_VARARGS,
     MCRF_FUNCTION(log_benchmark,
         MCRF_SIG("(message: str)", "None"),
         MCRF_DESC("Add a log message to the current benchmark frame."),
         MCRF_ARGS_START
         MCRF_ARG("message", "Text to associate with the current frame")
         MCRF_RETURNS("None")
         MCRF_RAISES("RuntimeError", "If no benchmark is currently running")
         MCRF_NOTE("Messages appear in the 'logs' array of each frame in the output JSON.")
     )},

    {NULL, NULL, 0, NULL}
};

static PyModuleDef mcrfpyModule = {
    PyModuleDef_HEAD_INIT, /* m_base - Always initialize this member to PyModuleDef_HEAD_INIT. */
    "mcrfpy",              /* m_name */
    PyDoc_STR("McRogueFace Python API\n\n"
              "Core game engine interface for creating roguelike games with Python.\n\n"
              "This module provides:\n"
              "- Scene management (createScene, setScene, currentScene)\n"
              "- UI components (Frame, Caption, Sprite, Grid)\n"
              "- Entity system for game objects\n"
              "- Audio playback (sound effects and music)\n"
              "- Timer system for scheduled events\n"
              "- Input handling\n"
              "- Performance metrics\n\n"
              "Example:\n"
              "    import mcrfpy\n"
              "    \n"
              "    # Create a new scene\n"
              "    mcrfpy.createScene('game')\n"
              "    mcrfpy.setScene('game')\n"
              "    \n"
              "    # Add UI elements\n"
              "    frame = mcrfpy.Frame(10, 10, 200, 100)\n"
              "    caption = mcrfpy.Caption('Hello World', 50, 50)\n"
              "    mcrfpy.sceneUI().extend([frame, caption])\n"),
    -1,                    /* m_size - Setting m_size to -1 means that the module does not support sub-interpreters, because it has global state. */
    mcrfpyMethods,         /* m_methods */
    NULL,                  /* m_slots - An array of slot definitions ...  When using single-phase initialization, m_slots must be NULL. */
    NULL,                  /* traverseproc m_traverse - A traversal function to call during GC traversal of the module object */
    NULL,                  /* inquiry m_clear - A clear function to call during GC clearing of the module object */
    NULL                   /* freefunc m_free - A function to call during deallocation of the module object */
};

// Module initializer fn, passed to PyImport_AppendInittab
PyObject* PyInit_mcrfpy()
{
    PyObject* m = PyModule_Create(&mcrfpyModule);
    
    if (m == NULL) 
    {
        return NULL;
    }
    
    using namespace mcrfpydef;
    PyTypeObject* pytypes[] = {
        /*SFML exposed types*/
        &PyColorType, /*&PyLinkedColorType,*/ &PyFontType, &PyTextureType, &PyVectorType,

        /*Base classes*/
        &PyDrawableType,

        /*UI widgets*/
        &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
        &PyUILineType, &PyUICircleType, &PyUIArcType,

        /*game map & perspective data*/
        &PyUIGridPointType, &PyUIGridPointStateType,

        /*grid layers (#147)*/
        &PyColorLayerType, &PyTileLayerType,

        /*collections & iterators*/
        &PyUICollectionType, &PyUICollectionIterType,
        &PyUIEntityCollectionType, &PyUIEntityCollectionIterType,

        /*animation*/
        &PyAnimationType,

        /*timer*/
        &PyTimerType,

        /*window singleton*/
        &PyWindowType,

        /*scene class*/
        &PySceneType,

        nullptr};
    
    // Set up PyWindowType methods and getsetters before PyType_Ready
    PyWindowType.tp_methods = PyWindow::methods;
    PyWindowType.tp_getset = PyWindow::getsetters;
    
    // Set up PySceneType methods and getsetters
    PySceneType.tp_methods = PySceneClass::methods;
    PySceneType.tp_getset = PySceneClass::getsetters;
    
    // Set up weakref support for all types that need it
    PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
    PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
    PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
    PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
    PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
    PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
    PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
    PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
    PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
    
    int i = 0;
    auto t = pytypes[i];
    while (t != nullptr)
    {
        //std::cout << "Registering type: " << t->tp_name << std::endl;
        if (PyType_Ready(t) < 0) {
            std::cout << "ERROR: PyType_Ready failed for " << t->tp_name << std::endl;
            return NULL;
        }
        //std::cout << "  tp_alloc after PyType_Ready: " << (void*)t->tp_alloc << std::endl;
        PyModule_AddType(m, t);
        i++;
        t = pytypes[i];
    }

    // Add default_font and default_texture to module
    McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
    McRFPy_API::default_texture = std::make_shared<PyTexture>("assets/kenney_tinydungeon.png", 16, 16);
    // These will be set later when the window is created
    PyModule_AddObject(m, "default_font", Py_None);
    PyModule_AddObject(m, "default_texture", Py_None);
    
    // Add FOV enum class (uses Python's IntEnum) (#114)
    PyObject* fov_class = PyFOV::create_enum_class(m);
    if (!fov_class) {
        // If enum creation fails, continue without it (non-fatal)
        PyErr_Clear();
    }

    // Add default_fov module property - defaults to FOV.BASIC
    // New grids copy this value at creation time
    if (fov_class) {
        PyObject* default_fov = PyObject_GetAttrString(fov_class, "BASIC");
        if (default_fov) {
            PyModule_AddObject(m, "default_fov", default_fov);
        } else {
            PyErr_Clear();
            // Fallback to integer
            PyModule_AddIntConstant(m, "default_fov", FOV_BASIC);
        }
    } else {
        // Fallback to integer if enum failed
        PyModule_AddIntConstant(m, "default_fov", FOV_BASIC);
    }
    
    // Add automation submodule
    PyObject* automation_module = McRFPy_Automation::init_automation_module();
    if (automation_module != NULL) {
        PyModule_AddObject(m, "automation", automation_module);
        
        // Also add to sys.modules for proper import behavior
        PyObject* sys_modules = PyImport_GetModuleDict();
        PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module);
    }
    
    // Add libtcod submodule
    PyObject* libtcod_module = McRFPy_Libtcod::init_libtcod_module();
    if (libtcod_module != NULL) {
        PyModule_AddObject(m, "libtcod", libtcod_module);
        
        // Also add to sys.modules for proper import behavior
        PyObject* sys_modules = PyImport_GetModuleDict();
        PyDict_SetItemString(sys_modules, "mcrfpy.libtcod", libtcod_module);
    }
    
    //McRFPy_API::mcrf_module = m;
    return m;
}

// init_python - configure interpreter details here
PyStatus init_python(const char *program_name)
{
    PyStatus status;

	//**preconfig to establish locale**
    PyPreConfig preconfig;
    PyPreConfig_InitIsolatedConfig(&preconfig);
    preconfig.utf8_mode = 1;
    
    status = Py_PreInitialize(&preconfig);
    if (PyStatus_Exception(status)) {
    	Py_ExitStatusException(status);
    }

    PyConfig config;
    PyConfig_InitIsolatedConfig(&config);
	config.dev_mode = 0;
    
    // Configure UTF-8 for stdio
    PyConfig_SetString(&config, &config.stdio_encoding, L"UTF-8");
    PyConfig_SetString(&config, &config.stdio_errors, L"surrogateescape");
    config.configure_c_stdio = 1;

    // Set sys.executable to the McRogueFace binary path
    auto exe_filename = executable_filename();
    PyConfig_SetString(&config, &config.executable, exe_filename.c_str());

	PyConfig_SetBytesString(&config, &config.home,
        narrow_string(executable_path() + L"/lib/Python").c_str());

    status = PyConfig_SetBytesString(&config, &config.program_name,
                                     program_name);

    // Check for sibling venv/ directory (self-contained deployment)
    auto exe_dir = std::filesystem::path(executable_path());
    auto sibling_venv = exe_dir / "venv";
    if (std::filesystem::exists(sibling_venv)) {
        // Platform-specific site-packages path
#ifdef _WIN32
        auto site_packages = sibling_venv / "Lib" / "site-packages";
#else
        auto site_packages = sibling_venv / "lib" / "python3.14" / "site-packages";
#endif
        if (std::filesystem::exists(site_packages)) {
            // Prepend so venv packages take priority over bundled
            PyWideStringList_Insert(&config.module_search_paths, 0,
                                   site_packages.wstring().c_str());
            config.module_search_paths_set = 1;
        }
    }

    // under Windows, the search paths are correct; under Linux, they need manual insertion
#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1
    if (!config.module_search_paths_set) {
        config.module_search_paths_set = 1;
    }

    // search paths for python libs/modules/scripts
    const wchar_t* str_arr[] = {
        L"/scripts",
        L"/lib/Python/lib.linux-x86_64-3.14",
        L"/lib/Python",
        L"/lib/Python/Lib"
        // Note: venv site-packages handled above via sibling_venv detection
    };


    for(auto s : str_arr) {
        status = PyWideStringList_Append(&config.module_search_paths, (executable_path() + s).c_str());
        if (PyStatus_Exception(status)) {
            continue;
        }
    }
#endif

    status = Py_InitializeFromConfig(&config);

    PyConfig_Clear(&config);

    return status;
}

PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config)
{
    // If Python is already initialized, just return success
    if (Py_IsInitialized()) {
        return PyStatus_Ok();
    }

    PyStatus status;
    PyConfig pyconfig;
    PyConfig_InitIsolatedConfig(&pyconfig);

    // Configure UTF-8 for stdio
    PyConfig_SetString(&pyconfig, &pyconfig.stdio_encoding, L"UTF-8");
    PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape");
    pyconfig.configure_c_stdio = 1;

    // Set sys.executable to the McRogueFace binary path
    auto exe_path = executable_filename();
    PyConfig_SetString(&pyconfig, &pyconfig.executable, exe_path.c_str());

    // Set interactive mode (replaces deprecated Py_InspectFlag)
    if (config.interactive_mode) {
        pyconfig.inspect = 1;
    }

    // Don't modify sys.path based on script location (replaces PySys_SetArgvEx updatepath=0)
    pyconfig.safe_path = 1;

    // Construct Python argv from config (replaces deprecated PySys_SetArgvEx)
    // Python convention:
    //   - Script mode: argv[0] = script_path, argv[1:] = script_args
    //   - -c mode: argv[0] = "-c"
    //   - -m mode: argv[0] = module_name, argv[1:] = script_args
    //   - Interactive only: argv[0] = ""
    std::vector<std::wstring> argv_storage;

    if (!config.script_path.empty()) {
        // Script execution: argv[0] = script path
        argv_storage.push_back(config.script_path.wstring());
        for (const auto& arg : config.script_args) {
            std::wstring warg(arg.begin(), arg.end());
            argv_storage.push_back(warg);
        }
    } else if (!config.python_command.empty()) {
        // -c command: argv[0] = "-c"
        argv_storage.push_back(L"-c");
    } else if (!config.python_module.empty()) {
        // -m module: argv[0] = module name
        std::wstring wmodule(config.python_module.begin(), config.python_module.end());
        argv_storage.push_back(wmodule);
        for (const auto& arg : config.script_args) {
            std::wstring warg(arg.begin(), arg.end());
            argv_storage.push_back(warg);
        }
    } else {
        // Interactive mode or no script: argv[0] = ""
        argv_storage.push_back(L"");
    }

    // Build wchar_t* array for PyConfig
    std::vector<wchar_t*> argv_ptrs;
    for (auto& ws : argv_storage) {
        argv_ptrs.push_back(const_cast<wchar_t*>(ws.c_str()));
    }

    status = PyConfig_SetWideStringList(&pyconfig, &pyconfig.argv,
                                         argv_ptrs.size(), argv_ptrs.data());
    if (PyStatus_Exception(status)) {
        return status;
    }

    // Check if we're in a virtual environment (symlinked into a venv)
    auto exe_wpath = executable_filename();
    auto exe_path_fs = std::filesystem::path(exe_wpath);
    auto exe_dir = exe_path_fs.parent_path();
    auto venv_root = exe_dir.parent_path();

    if (std::filesystem::exists(venv_root / "pyvenv.cfg")) {
        // We're running from within a venv!
        // Add venv's site-packages to module search paths
        auto site_packages = venv_root / "lib" / "python3.14" / "site-packages";
        PyWideStringList_Append(&pyconfig.module_search_paths,
                               site_packages.wstring().c_str());
        pyconfig.module_search_paths_set = 1;
    }

    // Check for sibling venv/ directory (self-contained deployment)
    auto sibling_venv = exe_dir / "venv";
    if (std::filesystem::exists(sibling_venv)) {
        // Platform-specific site-packages path
#ifdef _WIN32
        auto site_packages = sibling_venv / "Lib" / "site-packages";
#else
        auto site_packages = sibling_venv / "lib" / "python3.14" / "site-packages";
#endif
        if (std::filesystem::exists(site_packages)) {
            // Prepend so venv packages take priority over bundled
            PyWideStringList_Insert(&pyconfig.module_search_paths, 0,
                                   site_packages.wstring().c_str());
            pyconfig.module_search_paths_set = 1;
        }
    }

    // Set Python home to our bundled Python
    auto python_home = executable_path() + L"/lib/Python";
    PyConfig_SetString(&pyconfig, &pyconfig.home, python_home.c_str());

    // Set up module search paths
#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1
    if (!pyconfig.module_search_paths_set) {
        pyconfig.module_search_paths_set = 1;
    }

    // search paths for python libs/modules/scripts
    const wchar_t* str_arr[] = {
        L"/scripts",
        L"/lib/Python/lib.linux-x86_64-3.14",
        L"/lib/Python",
        L"/lib/Python/Lib"
        // Note: venv site-packages handled above via sibling_venv detection
    };

    for(auto s : str_arr) {
        status = PyWideStringList_Append(&pyconfig.module_search_paths, (executable_path() + s).c_str());
        if (PyStatus_Exception(status)) {
            continue;
        }
    }
#endif

    // Register mcrfpy module before initialization
    PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);

    status = Py_InitializeFromConfig(&pyconfig);
    PyConfig_Clear(&pyconfig);

    return status;
}

/*
void McRFPy_API::setSpriteTexture(int ti)
{
    int tx = ti % texture_width, ty = ti / texture_width;
    sprite.setTextureRect(sf::IntRect(
                tx * texture_size,
                ty * texture_size,
                texture_size, texture_size));
}
*/

// functionality
//void McRFPy_API::

void McRFPy_API::api_init() {

    // build API exposure before python initialization
    if (!Py_IsInitialized()) {
        PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
        // use full path version of argv[0] from OS to init python
        init_python(narrow_string(executable_filename()).c_str());
    }

    //texture.loadFromFile("./assets/kenney_tinydungeon.png");
    //texture_size = 16, texture_width = 12, texture_height= 11;
    //texture_sprite_count = texture_width * texture_height;
    //texture.setSmooth(false);

    // Add default_font and default_texture to module
    McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
    std::cout << PyUnicode_AsUTF8(PyObject_Repr(McRFPy_API::mcrf_module)) << std::endl;

    //PyModule_AddObject(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
    PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
    //PyModule_AddObject(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
    PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());

    //sprite.setTexture(texture);
    //sprite.setScale(sf::Vector2f(4.0f, 4.0f));
    //setSpriteTexture(0);
}

void McRFPy_API::api_init(const McRogueFaceConfig& config) {
    // Initialize Python with proper argv constructed from config
    PyStatus status = init_python_with_config(config);
    if (PyStatus_Exception(status)) {
        Py_ExitStatusException(status);
    }
    
    McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
    
    // For -m module execution, let Python handle it
    if (!config.python_module.empty() && config.python_module != "venv") {
        // Py_RunMain() will handle -m execution
        return;
    }
    
    // Execute based on mode - this is handled in main.cpp now
    // The actual execution logic is in run_python_interpreter()
    
    // Set up default resources only if in game mode
    if (!config.python_mode) {
        //PyModule_AddObject(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
        PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
        //PyModule_AddObject(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
        PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
    }
}

void McRFPy_API::executeScript(std::string filename)
{
    std::filesystem::path script_path(filename);
    
    // If the path is relative and the file doesn't exist, try resolving it relative to the executable
    if (script_path.is_relative() && !std::filesystem::exists(script_path)) {
        // Get the directory where the executable is located using platform-specific function
        std::wstring exe_dir_w = executable_path();
        std::filesystem::path exe_dir(exe_dir_w);
        
        // Try the script path relative to the executable directory
        std::filesystem::path resolved_path = exe_dir / script_path;
        if (std::filesystem::exists(resolved_path)) {
            script_path = resolved_path;
        }
    }
    
    FILE* PScriptFile = fopen(script_path.string().c_str(), "r");
    if(PScriptFile) {
        PyRun_SimpleFile(PScriptFile, script_path.string().c_str());
        fclose(PScriptFile);
    } else {
        std::cout << "Failed to open script: " << script_path.string() << std::endl;
    }
}

void McRFPy_API::api_shutdown()
{
    // Clean up audio resources in correct order
    if (sfx) {
        sfx->stop();
        delete sfx;
        sfx = nullptr;
    }
    if (music) {
        music->stop();
        delete music;
        music = nullptr;
    }
    if (soundbuffers) {
        soundbuffers->clear();
        delete soundbuffers;
        soundbuffers = nullptr;
    }
    
    Py_Finalize();
}

void McRFPy_API::executePyString(std::string pycode)
{
    PyRun_SimpleString(pycode.c_str());
}

void McRFPy_API::REPL()
{
    PyRun_InteractiveLoop(stdin, "<stdin>");
}

void McRFPy_API::REPL_device(FILE * fp, const char *filename)
{
    PyRun_InteractiveLoop(fp, filename);
}

// python connection


/*
PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) {
	for (auto e : McRFPy_API::entities.getEntities("player")) {
		e->cGrid->grid->refreshTCODsight(e->cGrid->x, e->cGrid->y);
	}
	Py_INCREF(Py_None);
    return Py_None;
}
*/

PyObject* McRFPy_API::_createSoundBuffer(PyObject* self, PyObject* args) {
	const char *fn_cstr;
	if (!PyArg_ParseTuple(args, "s", &fn_cstr)) return NULL;
	// Initialize soundbuffers if needed
	if (!McRFPy_API::soundbuffers) {
		McRFPy_API::soundbuffers = new std::vector<sf::SoundBuffer>();
	}
	auto b = sf::SoundBuffer();
	b.loadFromFile(fn_cstr);
	McRFPy_API::soundbuffers->push_back(b);
    Py_INCREF(Py_None);
    return Py_None;
}

PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
	const char *fn_cstr;
	PyObject* loop_obj = Py_False;
	if (!PyArg_ParseTuple(args, "s|O", &fn_cstr, &loop_obj)) return NULL;
	// Initialize music if needed
	if (!McRFPy_API::music) {
		McRFPy_API::music = new sf::Music();
	}
	McRFPy_API::music->stop();
	McRFPy_API::music->openFromFile(fn_cstr);
	McRFPy_API::music->setLoop(PyObject_IsTrue(loop_obj));
	McRFPy_API::music->play();
    Py_INCREF(Py_None);
    return Py_None;
}

PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
	int vol;
	if (!PyArg_ParseTuple(args, "i", &vol)) return NULL;
	if (!McRFPy_API::music) {
		McRFPy_API::music = new sf::Music();
	}
	McRFPy_API::music->setVolume(vol);
    Py_INCREF(Py_None);
    return Py_None;
}

PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
	float vol;
	if (!PyArg_ParseTuple(args, "f", &vol)) return NULL;
	if (!McRFPy_API::sfx) {
		McRFPy_API::sfx = new sf::Sound();
	}
	McRFPy_API::sfx->setVolume(vol);
    Py_INCREF(Py_None);
    return Py_None;
}

PyObject* McRFPy_API::_playSound(PyObject* self, PyObject* args) {
	float index;
	if (!PyArg_ParseTuple(args, "f", &index)) return NULL;
	if (!McRFPy_API::soundbuffers || index >= McRFPy_API::soundbuffers->size()) return NULL;
	if (!McRFPy_API::sfx) {
		McRFPy_API::sfx = new sf::Sound();
	}
	McRFPy_API::sfx->stop();
	McRFPy_API::sfx->setBuffer((*McRFPy_API::soundbuffers)[index]);
	McRFPy_API::sfx->play();
    Py_INCREF(Py_None);
    return Py_None;
}

PyObject* McRFPy_API::_getMusicVolume(PyObject* self, PyObject* args) {
	if (!McRFPy_API::music) {
		return Py_BuildValue("f", 0.0f);
	}
	return Py_BuildValue("f", McRFPy_API::music->getVolume());
}

PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
	if (!McRFPy_API::sfx) {
		return Py_BuildValue("f", 0.0f);
	}
	return Py_BuildValue("f", McRFPy_API::sfx->getVolume());
}

// Removed deprecated player_input, computerTurn, playerTurn functions
// These were part of the old turn-based system that is no longer used

/*
PyObject* McRFPy_API::_camFollow(PyObject* self, PyObject* args) {
	PyObject* set_camfollow = NULL;
	//std::cout << "camFollow Parse Args" << std::endl;
	if (!PyArg_ParseTuple(args, "|O", &set_camfollow)) return NULL;
	//std::cout << "Parsed" << std::endl;
	if (set_camfollow == NULL) {
		// return value
		//std::cout << "null; Returning value " << McRFPy_API::do_camfollow << std::endl;
		Py_INCREF(McRFPy_API::do_camfollow ? Py_True : Py_False);
		return McRFPy_API::do_camfollow ? Py_True : Py_False;
	}
	//std::cout << "non-null; setting value" << std::endl;
	
	McRFPy_API::do_camfollow = PyObject_IsTrue(set_camfollow);
	Py_INCREF(Py_None);
    return Py_None;
}
*/

//McRFPy_API::_sceneUI
PyObject* McRFPy_API::_sceneUI(PyObject* self, PyObject* args) {
    using namespace mcrfpydef;
	const char *scene_cstr;
	if (!PyArg_ParseTuple(args, "s", &scene_cstr)) return NULL;

    auto ui = Resources::game->scene_ui(scene_cstr);
    if(!ui) 
    {
        PyErr_SetString(PyExc_RuntimeError, "No scene found by that name");
        return NULL;
    }
    //std::cout << "vector returned has size=" << ui->size() << std::endl;
    //Py_INCREF(Py_None);
    //return Py_None;
    PyUICollectionObject* o = (PyUICollectionObject*)PyUICollectionType.tp_alloc(&PyUICollectionType, 0);
        if (o)
            o->data = ui;
        return (PyObject*)o;
}

PyObject* McRFPy_API::_currentScene(PyObject* self, PyObject* args) {
	return Py_BuildValue("s", game->scene.c_str());
}

PyObject* McRFPy_API::_setScene(PyObject* self, PyObject* args) {
	const char* newscene;
	const char* transition_str = nullptr;
	float duration = 0.0f;
	
	// Parse arguments: scene name, optional transition type, optional duration
	if (!PyArg_ParseTuple(args, "s|sf", &newscene, &transition_str, &duration)) return NULL;
	
	// Map transition string to enum
	TransitionType transition_type = TransitionType::None;
	if (transition_str) {
		std::string trans(transition_str);
		if (trans == "fade") transition_type = TransitionType::Fade;
		else if (trans == "slide_left") transition_type = TransitionType::SlideLeft;
		else if (trans == "slide_right") transition_type = TransitionType::SlideRight;
		else if (trans == "slide_up") transition_type = TransitionType::SlideUp;
		else if (trans == "slide_down") transition_type = TransitionType::SlideDown;
	}
	
	game->changeScene(newscene, transition_type, duration);
    Py_INCREF(Py_None);
    return Py_None;		
}

PyObject* McRFPy_API::_createScene(PyObject* self, PyObject* args) {
	const char* newscene;
	if (!PyArg_ParseTuple(args, "s", &newscene)) return NULL;
	game->createScene(newscene);
    Py_INCREF(Py_None);
    return Py_None;		
}

PyObject* McRFPy_API::_keypressScene(PyObject* self, PyObject* args) {
    PyObject* callable;
	if (!PyArg_ParseTuple(args, "O", &callable)) return NULL;
    
    // Validate that the argument is callable
    if (!PyCallable_Check(callable)) {
        PyErr_SetString(PyExc_TypeError, "keypressScene() argument must be callable");
        return NULL;
    }
    
    /*
    if (game->currentScene()->key_callable != NULL and game->currentScene()->key_callable != Py_None)
    {
        Py_DECREF(game->currentScene()->key_callable);
    }
    Py_INCREF(callable);
    game->currentScene()->key_callable = callable;
    Py_INCREF(Py_None);
    */
    game->currentScene()->key_callable = std::make_unique<PyKeyCallable>(callable);
    Py_INCREF(Py_None);
    return Py_None;		
}

PyObject* McRFPy_API::_setTimer(PyObject* self, PyObject* args) { // TODO - compare with UIDrawable mouse & Scene Keyboard methods - inconsistent responsibility for incref/decref around mcrogueface
    const char* name;
    PyObject* callable;
    int interval;
	if (!PyArg_ParseTuple(args, "sOi", &name, &callable, &interval)) return NULL;
    game->manageTimer(name, callable, interval);
    Py_INCREF(Py_None);
    return Py_None;
}

PyObject* McRFPy_API::_delTimer(PyObject* self, PyObject* args) {
	const char* name;
	if (!PyArg_ParseTuple(args, "s", &name)) return NULL;
    game->manageTimer(name, NULL, 0);
    Py_INCREF(Py_None);
    return Py_None;
}

// #153 - Headless simulation control
PyObject* McRFPy_API::_step(PyObject* self, PyObject* args) {
    PyObject* dt_obj = Py_None;
    if (!PyArg_ParseTuple(args, "|O", &dt_obj)) return NULL;

    float dt;
    if (dt_obj == Py_None) {
        // None means "advance to next event"
        dt = -1.0f;
    } else if (PyFloat_Check(dt_obj)) {
        dt = static_cast<float>(PyFloat_AsDouble(dt_obj));
    } else if (PyLong_Check(dt_obj)) {
        dt = static_cast<float>(PyLong_AsLong(dt_obj));
    } else {
        PyErr_SetString(PyExc_TypeError, "step() argument must be a float, int, or None");
        return NULL;
    }

    if (!game) {
        PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
        return NULL;
    }

    float actual_dt = game->step(dt);
    return PyFloat_FromDouble(actual_dt);
}

PyObject* McRFPy_API::_exit(PyObject* self, PyObject* args) {
    game->quit();
    Py_INCREF(Py_None);
    return Py_None;
}

PyObject* McRFPy_API::_setScale(PyObject* self, PyObject* args) {
	float multiplier;
	if (!PyArg_ParseTuple(args, "f", &multiplier)) return NULL;
    if (multiplier < 0.2 || multiplier > 4)
    {
        PyErr_SetString(PyExc_ValueError, "Window scale must be between 0.2 and 4");
        return NULL;
    }
    game->setWindowScale(multiplier);
    Py_INCREF(Py_None);
    return Py_None;
}

void McRFPy_API::markSceneNeedsSort() {
    // Mark the current scene as needing a z_index sort
    auto scene = game->currentScene();
    if (scene && scene->ui_elements) {
        // Cast to PyScene to access ui_elements_need_sort
        PyScene* pyscene = dynamic_cast<PyScene*>(scene);
        if (pyscene) {
            pyscene->ui_elements_need_sort = true;
        }
    }
}

// Helper function to check if a name matches a pattern with wildcards
static bool name_matches_pattern(const std::string& name, const std::string& pattern) {
    if (pattern.find('*') == std::string::npos) {
        // No wildcards, exact match
        return name == pattern;
    }
    
    // Simple wildcard matching - * matches any sequence
    size_t name_pos = 0;
    size_t pattern_pos = 0;
    
    while (pattern_pos < pattern.length() && name_pos < name.length()) {
        if (pattern[pattern_pos] == '*') {
            // Skip consecutive stars
            while (pattern_pos < pattern.length() && pattern[pattern_pos] == '*') {
                pattern_pos++;
            }
            if (pattern_pos == pattern.length()) {
                // Pattern ends with *, matches rest of name
                return true;
            }
            
            // Find next non-star character in pattern
            char next_char = pattern[pattern_pos];
            while (name_pos < name.length() && name[name_pos] != next_char) {
                name_pos++;
            }
        } else if (pattern[pattern_pos] == name[name_pos]) {
            pattern_pos++;
            name_pos++;
        } else {
            return false;
        }
    }
    
    // Skip trailing stars in pattern
    while (pattern_pos < pattern.length() && pattern[pattern_pos] == '*') {
        pattern_pos++;
    }
    
    return pattern_pos == pattern.length() && name_pos == name.length();
}

// Helper to recursively search a collection for named elements
static void find_in_collection(std::vector<std::shared_ptr<UIDrawable>>* collection, const std::string& pattern, 
                             bool find_all, PyObject* results) {
    if (!collection) return;
    
    for (auto& drawable : *collection) {
        if (!drawable) continue;
        
        // Check this element's name
        if (name_matches_pattern(drawable->name, pattern)) {
            // Convert to Python object using RET_PY_INSTANCE logic
            PyObject* py_obj = nullptr;
            
            switch (drawable->derived_type()) {
                case PyObjectsEnum::UIFRAME: {
                    auto frame = std::static_pointer_cast<UIFrame>(drawable);
                    auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame");
                    auto o = (PyUIFrameObject*)type->tp_alloc(type, 0);
                    if (o) {
                        o->data = frame;
                        py_obj = (PyObject*)o;
                    }
                    break;
                }
                case PyObjectsEnum::UICAPTION: {
                    auto caption = std::static_pointer_cast<UICaption>(drawable);
                    auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption");
                    auto o = (PyUICaptionObject*)type->tp_alloc(type, 0);
                    if (o) {
                        o->data = caption;
                        py_obj = (PyObject*)o;
                    }
                    break;
                }
                case PyObjectsEnum::UISPRITE: {
                    auto sprite = std::static_pointer_cast<UISprite>(drawable);
                    auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite");
                    auto o = (PyUISpriteObject*)type->tp_alloc(type, 0);
                    if (o) {
                        o->data = sprite;
                        py_obj = (PyObject*)o;
                    }
                    break;
                }
                case PyObjectsEnum::UIGRID: {
                    auto grid = std::static_pointer_cast<UIGrid>(drawable);
                    auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
                    auto o = (PyUIGridObject*)type->tp_alloc(type, 0);
                    if (o) {
                        o->data = grid;
                        py_obj = (PyObject*)o;
                    }
                    break;
                }
                default:
                    break;
            }
            
            if (py_obj) {
                if (find_all) {
                    PyList_Append(results, py_obj);
                    Py_DECREF(py_obj);
                } else {
                    // For find (not findAll), we store in results and return early
                    PyList_Append(results, py_obj);
                    Py_DECREF(py_obj);
                    return;
                }
            }
        }
        
        // Recursively search in Frame children
        if (drawable->derived_type() == PyObjectsEnum::UIFRAME) {
            auto frame = std::static_pointer_cast<UIFrame>(drawable);
            find_in_collection(frame->children.get(), pattern, find_all, results);
            if (!find_all && PyList_Size(results) > 0) {
                return;  // Found one, stop searching
            }
        }
    }
}

// Also search Grid entities
static void find_in_grid_entities(UIGrid* grid, const std::string& pattern, 
                                bool find_all, PyObject* results) {
    if (!grid || !grid->entities) return;
    
    for (auto& entity : *grid->entities) {
        if (!entity) continue;
        
        // Entities delegate name to their sprite
        if (name_matches_pattern(entity->sprite.name, pattern)) {
            auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
            auto o = (PyUIEntityObject*)type->tp_alloc(type, 0);
            if (o) {
                o->data = entity;
                PyObject* py_obj = (PyObject*)o;
                
                if (find_all) {
                    PyList_Append(results, py_obj);
                    Py_DECREF(py_obj);
                } else {
                    PyList_Append(results, py_obj);
                    Py_DECREF(py_obj);
                    return;
                }
            }
        }
    }
}

PyObject* McRFPy_API::_find(PyObject* self, PyObject* args) {
    const char* name;
    const char* scene_name = nullptr;
    
    if (!PyArg_ParseTuple(args, "s|s", &name, &scene_name)) {
        return NULL;
    }
    
    PyObject* results = PyList_New(0);
    
    // Get the UI elements to search
    std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
    if (scene_name) {
        // Search specific scene
        ui_elements = game->scene_ui(scene_name);
        if (!ui_elements) {
            PyErr_Format(PyExc_ValueError, "Scene '%s' not found", scene_name);
            Py_DECREF(results);
            return NULL;
        }
    } else {
        // Search current scene
        Scene* current = game->currentScene();
        if (!current) {
            PyErr_SetString(PyExc_RuntimeError, "No current scene");
            Py_DECREF(results);
            return NULL;
        }
        ui_elements = current->ui_elements;
    }
    
    // Search the scene's UI elements
    find_in_collection(ui_elements.get(), name, false, results);
    
    // Also search all grids in the scene for entities
    if (PyList_Size(results) == 0 && ui_elements) {
        for (auto& drawable : *ui_elements) {
            if (drawable && drawable->derived_type() == PyObjectsEnum::UIGRID) {
                auto grid = std::static_pointer_cast<UIGrid>(drawable);
                find_in_grid_entities(grid.get(), name, false, results);
                if (PyList_Size(results) > 0) break;
            }
        }
    }
    
    // Return the first result or None
    if (PyList_Size(results) > 0) {
        PyObject* result = PyList_GetItem(results, 0);
        Py_INCREF(result);
        Py_DECREF(results);
        return result;
    }
    
    Py_DECREF(results);
    Py_RETURN_NONE;
}

PyObject* McRFPy_API::_findAll(PyObject* self, PyObject* args) {
    const char* pattern;
    const char* scene_name = nullptr;
    
    if (!PyArg_ParseTuple(args, "s|s", &pattern, &scene_name)) {
        return NULL;
    }
    
    PyObject* results = PyList_New(0);
    
    // Get the UI elements to search
    std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
    if (scene_name) {
        // Search specific scene
        ui_elements = game->scene_ui(scene_name);
        if (!ui_elements) {
            PyErr_Format(PyExc_ValueError, "Scene '%s' not found", scene_name);
            Py_DECREF(results);
            return NULL;
        }
    } else {
        // Search current scene
        Scene* current = game->currentScene();
        if (!current) {
            PyErr_SetString(PyExc_RuntimeError, "No current scene");
            Py_DECREF(results);
            return NULL;
        }
        ui_elements = current->ui_elements;
    }
    
    // Search the scene's UI elements
    find_in_collection(ui_elements.get(), pattern, true, results);
    
    // Also search all grids in the scene for entities
    if (ui_elements) {
        for (auto& drawable : *ui_elements) {
            if (drawable && drawable->derived_type() == PyObjectsEnum::UIGRID) {
                auto grid = std::static_pointer_cast<UIGrid>(drawable);
                find_in_grid_entities(grid.get(), pattern, true, results);
            }
        }
    }
    
    return results;
}

PyObject* McRFPy_API::_getMetrics(PyObject* self, PyObject* args) {
    // Create a dictionary with metrics
    PyObject* dict = PyDict_New();
    if (!dict) return NULL;

    // Add frame time metrics
    PyDict_SetItemString(dict, "frame_time", PyFloat_FromDouble(game->metrics.frameTime));
    PyDict_SetItemString(dict, "avg_frame_time", PyFloat_FromDouble(game->metrics.avgFrameTime));
    PyDict_SetItemString(dict, "fps", PyLong_FromLong(game->metrics.fps));

    // Add draw call metrics
    PyDict_SetItemString(dict, "draw_calls", PyLong_FromLong(game->metrics.drawCalls));
    PyDict_SetItemString(dict, "ui_elements", PyLong_FromLong(game->metrics.uiElements));
    PyDict_SetItemString(dict, "visible_elements", PyLong_FromLong(game->metrics.visibleElements));

    // #144 - Add detailed timing breakdown (in milliseconds)
    PyDict_SetItemString(dict, "grid_render_time", PyFloat_FromDouble(game->metrics.gridRenderTime));
    PyDict_SetItemString(dict, "entity_render_time", PyFloat_FromDouble(game->metrics.entityRenderTime));
    PyDict_SetItemString(dict, "fov_overlay_time", PyFloat_FromDouble(game->metrics.fovOverlayTime));
    PyDict_SetItemString(dict, "python_time", PyFloat_FromDouble(game->metrics.pythonScriptTime));
    PyDict_SetItemString(dict, "animation_time", PyFloat_FromDouble(game->metrics.animationTime));

    // #144 - Add grid-specific metrics
    PyDict_SetItemString(dict, "grid_cells_rendered", PyLong_FromLong(game->metrics.gridCellsRendered));
    PyDict_SetItemString(dict, "entities_rendered", PyLong_FromLong(game->metrics.entitiesRendered));
    PyDict_SetItemString(dict, "total_entities", PyLong_FromLong(game->metrics.totalEntities));

    // Add general metrics
    PyDict_SetItemString(dict, "current_frame", PyLong_FromLong(game->getFrame()));
    PyDict_SetItemString(dict, "runtime", PyFloat_FromDouble(game->runtime.getElapsedTime().asSeconds()));

    return dict;
}

PyObject* McRFPy_API::_setDevConsole(PyObject* self, PyObject* args) {
    int enabled;
    if (!PyArg_ParseTuple(args, "p", &enabled)) {  // "p" for boolean predicate
        return NULL;
    }

    ImGuiConsole::setEnabled(enabled);
    Py_RETURN_NONE;
}

// Benchmark logging implementation (#104)
PyObject* McRFPy_API::_startBenchmark(PyObject* self, PyObject* args) {
    try {
        // Warn if in headless mode - benchmark frames are only recorded by the game loop
        if (game && game->isHeadless()) {
            PyErr_WarnEx(PyExc_UserWarning,
                "Benchmark started in headless mode. Note: step() and screenshot() do not "
                "record benchmark frames. The benchmark API captures per-frame data from the "
                "game loop, which is bypassed when using step()-based simulation control. "
                "For headless performance measurement, use Python's time module instead.", 1);
        }
        g_benchmarkLogger.start();
        Py_RETURN_NONE;
    } catch (const std::runtime_error& e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return NULL;
    }
}

PyObject* McRFPy_API::_endBenchmark(PyObject* self, PyObject* args) {
    try {
        std::string filename = g_benchmarkLogger.end();
        return PyUnicode_FromString(filename.c_str());
    } catch (const std::runtime_error& e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return NULL;
    }
}

PyObject* McRFPy_API::_logBenchmark(PyObject* self, PyObject* args) {
    const char* message;
    if (!PyArg_ParseTuple(args, "s", &message)) {
        return NULL;
    }

    try {
        g_benchmarkLogger.log(message);
        Py_RETURN_NONE;
    } catch (const std::runtime_error& e) {
        PyErr_SetString(PyExc_RuntimeError, e.what());
        return NULL;
    }
}

// Exception handling implementation
void McRFPy_API::signalPythonException() {
    // Check if we should exit on exception (consult config via game)
    if (game && !game->isHeadless()) {
        // In windowed mode, respect the config setting
        // Access config through game engine - but we need to check the config
    }

    // For now, always signal - the game loop will check the config
    exception_occurred.store(true);
    exit_code.store(1);
}

bool McRFPy_API::shouldExit() {
    return exception_occurred.load();
}
