#include "game/ui/console_commands.hpp"

#include <SDL3/SDL.h>

#include <algorithm>  // Para ranges::transform, ranges::find
#include <array>      // Para array
#include <iostream>   // Para cout
#include <sstream>    // Para ostringstream
#include <string>     // Para string
#include <vector>     // Para vector

#include "core/audio/audio.hpp"                // Para Audio
#include "core/locale/locale.hpp"              // Para Locale
#include "core/rendering/render_info.hpp"      // Para RenderInfo
#include "core/rendering/screen.hpp"           // Para Screen
#include "core/resources/resource_helper.hpp"  // Para Resource::Helper
#include "core/resources/resource_list.hpp"    // Para Resource::List
#include "external/fkyaml_node.hpp"            // Para fkyaml::node
#include "game/game_control.hpp"               // Para GameControl
#include "game/options.hpp"                    // Para Options
#include "game/scene_manager.hpp"              // Para SceneManager
#include "game/ui/notifier.hpp"                // Para Notifier
#include "utils/utils.hpp"                     // Para toUpper, prettyName

#ifdef _DEBUG
#include "core/system/debug.hpp"       // Para Debug
#include "game/editor/map_editor.hpp"  // Para MapEditor
#endif

// ── Helpers ──────────────────────────────────────────────────────────────────

// Toggle genérico para comandos booleanos ON/OFF (reemplaza macro BOOL_TOGGLE_CMD)
static auto boolToggle(
    const std::string& label,
    const bool& option,
    const std::function<void()>& toggle_fn,
    const std::vector<std::string>& args) -> std::string {
    if (args.empty()) {
        toggle_fn();
        return label + " " + (option ? "ON" : "OFF");
    }
    if (args[0] == "ON") {
        if (option) { return label + " already ON"; }
        toggle_fn();
        return label + " ON";
    }
    if (args[0] == "OFF") {
        if (!option) { return label + " already OFF"; }
        toggle_fn();
        return label + " OFF";
    }
    return "usage: " + label + " [on|off]";
}

// ── Command handlers ─────────────────────────────────────────────────────────

// SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]]
static auto cmdSs(const std::vector<std::string>& args) -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; }
    static const std::array<std::string_view, 3> DOWNSCALE_NAMES = {"Bilinear", "Lanczos2", "Lanczos3"};
    if (!args.empty() && args[0] == "SIZE") {
        if (!Options::video.supersampling.enabled) { return "Supersampling is OFF: no texture"; }
        const auto [w, h] = Screen::get()->getSsTextureSize();
        if (w == 0) { return "SS texture: not active"; }
        return "SS texture: " + std::to_string(w) + "x" + std::to_string(h);
    }
    if (!args.empty() && args[0] == "UPSCALE") {
        if (args.size() == 1) {
            Screen::get()->setLinearUpscale(!Options::video.supersampling.linear_upscale);
            return std::string("Upscale: ") + (Options::video.supersampling.linear_upscale ? "Linear" : "Nearest");
        }
        if (args[1] == "NEAREST") {
            if (!Options::video.supersampling.linear_upscale) { return "Upscale already Nearest"; }
            Screen::get()->setLinearUpscale(false);
            return "Upscale: Nearest";
        }
        if (args[1] == "LINEAR") {
            if (Options::video.supersampling.linear_upscale) { return "Upscale already Linear"; }
            Screen::get()->setLinearUpscale(true);
            return "Upscale: Linear";
        }
        return "usage: ss upscale [nearest|linear]";
    }
    if (!args.empty() && args[0] == "DOWNSCALE") {
        if (args.size() == 1) {
            return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(Options::video.supersampling.downscale_algo)]);
        }
        int algo = -1;
        if (args[1] == "BILINEAR") { algo = 0; }
        if (args[1] == "LANCZOS2") { algo = 1; }
        if (args[1] == "LANCZOS3") { algo = 2; }
        if (algo == -1) { return "usage: ss downscale [bilinear|lanczos2|lanczos3]"; }
        if (Options::video.supersampling.downscale_algo == algo) {
            return std::string("Downscale already ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(algo)]);
        }
        Screen::get()->setDownscaleAlgo(algo);
        return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(algo)]);
    }
    if (args.empty()) {
        Screen::get()->toggleSupersampling();
        return std::string("PostFX Supersampling ") + (Options::video.supersampling.enabled ? "ON" : "OFF");
    }
    if (args[0] == "ON") {
        if (Options::video.supersampling.enabled) { return "Supersampling already ON"; }
        Screen::get()->toggleSupersampling();
        return "PostFX Supersampling ON";
    }
    if (args[0] == "OFF") {
        if (!Options::video.supersampling.enabled) { return "Supersampling already OFF"; }
        Screen::get()->toggleSupersampling();
        return "PostFX Supersampling OFF";
    }
    return "usage: ss [on|off|size|upscale [nearest|linear]|downscale [bilinear|lanczos2|lanczos3]]";
}

// Helper: aplica un preset por dirección (NEXT/PREV) o nombre; devuelve mensaje
static auto applyPreset(const std::vector<std::string>& args) -> std::string {
    const bool IS_CRTPI = Options::video.shader.current_shader == Rendering::ShaderType::CRTPI;
    const auto& presets_postfx = Options::postfx_presets;
    const auto& presets_crtpi = Options::crtpi_presets;
    const std::string SHADER_LABEL = IS_CRTPI ? "CrtPi" : "PostFX";

    auto& current_idx = IS_CRTPI ? Options::video.shader.current_crtpi_preset
                                 : Options::video.shader.current_postfx_preset;
    const int COUNT = static_cast<int>(IS_CRTPI ? presets_crtpi.size() : presets_postfx.size());

    if (COUNT == 0) { return "No " + SHADER_LABEL + " presets available"; }

    const auto PRESET_NAME = [&]() -> std::string {
        const auto& name = IS_CRTPI ? presets_crtpi[static_cast<size_t>(current_idx)].name    // NOLINT(clang-analyzer-core.CallAndMessage)
                                    : presets_postfx[static_cast<size_t>(current_idx)].name;  // NOLINT(clang-analyzer-core.CallAndMessage)
        return prettyName(name);
    };

    if (args.empty()) {
        return SHADER_LABEL + " preset: " + PRESET_NAME();
    }

    if (args[0] == "NEXT") {
        current_idx = (current_idx + 1) % COUNT;
    } else if (args[0] == "PREV") {
        current_idx = (current_idx - 1 + COUNT) % COUNT;
    } else {
        // Buscar por nombre (case-insensitive, con guiones)
        std::string search = args[0];
        std::ranges::transform(search, search.begin(), ::toupper);
        bool found = false;
        for (int i = 0; i < COUNT; ++i) {
            const auto& name = IS_CRTPI ? presets_crtpi[static_cast<size_t>(i)].name
                                        : presets_postfx[static_cast<size_t>(i)].name;
            if (toUpper(name) == search) {
                current_idx = i;
                found = true;
                break;
            }
        }
        if (!found) {
            std::string name_lower = args[0];
            std::ranges::transform(name_lower, name_lower.begin(), ::tolower);
            return "Unknown preset: " + name_lower;
        }
    }

    if (IS_CRTPI) {
        Screen::get()->reloadCrtPi();
    } else {
        Screen::get()->reloadPostFX();
    }
    return SHADER_LABEL + " preset: " + PRESET_NAME();
}

// SHADER [ON|OFF|NEXT|POSTFX|CRTPI|PRESET [NEXT|PREV|<name>]]
static auto cmdShader(const std::vector<std::string>& args) -> std::string {
    if (!Screen::get()->isHardwareAccelerated()) { return "No GPU acceleration"; }
    if (args.empty()) {
        Screen::get()->toggleShaders();
        return std::string("Shader ") + (Options::video.shader.enabled ? "ON" : "OFF");
    }
    if (args[0] == "ON") {
        if (Options::video.shader.enabled) { return "Shader already ON"; }
        Screen::get()->toggleShaders();
        return "Shader ON";
    }
    if (args[0] == "OFF") {
        if (!Options::video.shader.enabled) { return "Shader already OFF"; }
        Screen::get()->toggleShaders();
        return "Shader OFF";
    }
    if (args[0] == "POSTFX") {
        Screen::get()->setActiveShader(Rendering::ShaderType::POSTFX);
        return "Shader: PostFX";
    }
    if (args[0] == "CRTPI") {
        Screen::get()->setActiveShader(Rendering::ShaderType::CRTPI);
        return "Shader: CrtPi";
    }
    if (args[0] == "NEXT") {
        Screen::get()->nextShader();
        return std::string("Shader: ") +
            (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI ? "CrtPi" : "PostFX");
    }
    if (args[0] == "PRESET") {
        const std::vector<std::string> REST(args.begin() + 1, args.end());
        return applyPreset(REST);
    }
    return "usage: shader [on|off|next|postfx|crtpi|preset [next|prev|<name>]]";
}

// BORDER [ON|OFF]
static auto cmdBorder(const std::vector<std::string>& args) -> std::string {
    return boolToggle("Border", Options::video.border.enabled, [] { Screen::get()->toggleBorder(); }, args);
}

// FULLSCREEN [ON|OFF [PLEASE]]
static auto cmdFullscreen(const std::vector<std::string>& args) -> std::string {
    const bool EXPLICIT_ON = !args.empty() && args[0] == "ON";
    const bool EXPLICIT_OFF = !args.empty() && args[0] == "OFF";
    const bool WITH_PLEASE = !args.empty() && args.back() == "PLEASE";
    const bool IS_TOGGLE = args.empty();
    const bool TURNING_OFF = EXPLICIT_OFF || (IS_TOGGLE && Options::video.fullscreen);

    if (TURNING_OFF && Options::kiosk.enabled && !WITH_PLEASE) {
        return "Not allowed in kiosk mode";
    }
    if (EXPLICIT_ON) {
        if (Options::video.fullscreen) { return "Fullscreen already ON"; }
        Screen::get()->toggleVideoMode();
        return "Fullscreen ON";
    }
    if (EXPLICIT_OFF) {
        if (!Options::video.fullscreen) { return "Fullscreen already OFF"; }
        Screen::get()->toggleVideoMode();
        return "Fullscreen OFF";
    }
    if (IS_TOGGLE) {
        Screen::get()->toggleVideoMode();
        return std::string("Fullscreen ") + (Options::video.fullscreen ? "ON" : "OFF");
    }
    return "usage: fullscreen [on|off]";
}

// ZOOM UP/DOWN/<num>
static auto cmdZoom(const std::vector<std::string>& args) -> std::string {
    if (args.empty()) { return "usage: zoom [up|down|<1-" + std::to_string(Screen::getMaxZoom()) + ">]"; }
    if (args[0] == "UP") {
        if (!Screen::get()->incWindowZoom()) { return "Max zoom reached"; }
        return "Zoom " + std::to_string(Options::window.zoom);
    }
    if (args[0] == "DOWN") {
        if (!Screen::get()->decWindowZoom()) { return "Min zoom reached"; }
        return "Zoom " + std::to_string(Options::window.zoom);
    }
    try {
        const int N = std::stoi(args[0]);
        const int MAX = Screen::getMaxZoom();
        if (N < 1 || N > MAX) {
            return "Zoom must be between 1 and " + std::to_string(MAX);
        }
        if (N == Options::window.zoom) { return "Zoom already " + std::to_string(N); }
        Screen::get()->setWindowZoom(N);
        return "Zoom " + std::to_string(Options::window.zoom);
    } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
    }
    return "usage: zoom [up|down|<1-" + std::to_string(Screen::getMaxZoom()) + ">]";
}

// INTSCALE [ON|OFF]
static auto cmdIntscale(const std::vector<std::string>& args) -> std::string {
    const bool ON = args.empty() ? !Options::video.integer_scale
                                 : (args[0] == "ON");
    if (!args.empty() && args[0] != "ON" && args[0] != "OFF") {
        return "usage: intscale [on|off]";
    }
    if (ON == Options::video.integer_scale) {
        return std::string("IntScale already ") + (ON ? "ON" : "OFF");
    }
    Screen::get()->toggleIntegerScale();
    Screen::get()->setVideoMode(Options::video.fullscreen);
    return std::string("IntScale ") + (Options::video.integer_scale ? "ON" : "OFF");
}

// VSYNC [ON|OFF]
static auto cmdVsync(const std::vector<std::string>& args) -> std::string {
    return boolToggle("VSync", Options::video.vertical_sync, [] { Screen::get()->toggleVSync(); }, args);
}

// DRIVER [LIST|AUTO|NONE|<name>]
static auto cmdDriver(const std::vector<std::string>& args) -> std::string {
    if (args.empty()) {
        const auto& driver = Screen::get()->getGPUDriver();
        return "GPU: " + (driver.empty() ? std::string("sdl") : driver);
    }
    if (args[0] == "LIST") {
        const int COUNT = SDL_GetNumGPUDrivers();
        if (COUNT <= 0) { return "No GPU drivers found"; }
        const std::string& active = Screen::get()->getGPUDriver();
        std::string result = "Drivers:";
        for (int i = 0; i < COUNT; ++i) {
            const char* name = SDL_GetGPUDriver(i);
            if (name != nullptr) {
                result += ' ';
                result += name;
                if (active == name) { result += '*'; }
            }
        }
        SDL_Log("SDL GPU drivers: %s", result.c_str());
        return result;
    }
    const bool HAS_PLEASE = std::ranges::find(args, std::string("PLEASE")) != args.end();
    if (Options::kiosk.enabled && !HAS_PLEASE) {
        return "Not allowed in kiosk mode";
    }
    if (args[0] == "AUTO") {
        Options::video.gpu.preferred_driver.clear();
        Options::saveToFile();
        return "Driver: auto (restart)";
    }
    if (args[0] == "NONE") {
        Options::video.gpu.preferred_driver = "none";
        Options::saveToFile();
        return "Driver: none (SDL fallback, restart)";
    }
    std::string driver_lower = args[0];
    std::ranges::transform(driver_lower, driver_lower.begin(), ::tolower);
    const int COUNT = SDL_GetNumGPUDrivers();
    bool found = false;
    for (int i = 0; i < COUNT && !found; ++i) {
        const char* name = SDL_GetGPUDriver(i);
        if (name != nullptr && driver_lower == name) { found = true; }
    }
    if (!found) {
        return "Unknown driver: " + driver_lower + ". Use DRIVER LIST or NONE";
    }
    Options::video.gpu.preferred_driver = driver_lower;
    Options::saveToFile();
    return "Driver: " + driver_lower + " (restart)";
}

// PALETTE NEXT/PREV/SORT/DEFAULT/<name>
static auto cmdPalette(const std::vector<std::string>& args) -> std::string {
    const auto PAL_NAME = []() -> std::string {
        return Screen::get()->getPalettePrettyName();
    };
    if (args.empty()) { return "usage: palette [next|prev|sort [original|luminance|spectrum]|default|<name>]"; }
    if (args[0] == "NEXT") {
        Screen::get()->nextPalette();
        return "Palette: " + PAL_NAME();
    }
    if (args[0] == "PREV") {
        Screen::get()->previousPalette();
        return "Palette: " + PAL_NAME();
    }
    if (args[0] == "DEFAULT") {
        Screen::get()->setPaletteByName(Defaults::Video::PALETTE_NAME);
        return "Palette: " + PAL_NAME();
    }
    if (args[0] == "SORT") {
        if (args.size() == 1) {
            Screen::get()->nextPaletteSortMode();
            return "Palette sort: " + Screen::get()->getPaletteSortModeName();
        }
        if (args[1] == "ORIGINAL") {
            Screen::get()->setPaletteSortMode(PaletteSortMode::ORIGINAL);
        } else if (args[1] == "LUMINANCE") {
            Screen::get()->setPaletteSortMode(PaletteSortMode::LUMINANCE);
        } else if (args[1] == "SPECTRUM") {
            Screen::get()->setPaletteSortMode(PaletteSortMode::SPECTRUM);
        } else {
            return "Unknown sort mode. Use: original, luminance, spectrum";
        }
        return "Palette sort: " + Screen::get()->getPaletteSortModeName();
    }
    if (!Screen::get()->setPaletteByName(args[0])) {
        std::string arg_lower = args[0];
        std::ranges::transform(arg_lower, arg_lower.begin(), ::tolower);
        return "Unknown palette: " + arg_lower;
    }
    return "Palette: " + PAL_NAME();
}

// AUDIO [ON|OFF|VOL <0-100>]
static auto cmdAudio(const std::vector<std::string>& args) -> std::string {
    if (args.empty()) {
        const int VOL = static_cast<int>(Options::audio.volume * 100.0F);
        return std::string("Audio ") + (Options::audio.enabled ? "ON" : "OFF") +
            " vol:" + std::to_string(VOL);
    }
    if (args[0] == "ON") {
        if (Options::audio.enabled) { return "Audio already ON"; }
        Options::audio.enabled = true;
        Audio::get()->enable(true);
        return "Audio ON";
    }
    if (args[0] == "OFF") {
        if (!Options::audio.enabled) { return "Audio already OFF"; }
        Options::audio.enabled = false;
        Audio::get()->enable(false);
        return "Audio OFF";
    }
    if (args[0] == "VOL" && args.size() >= 2) {
        try {
            const int VAL = std::stoi(args[1]);
            if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; }
            Options::audio.volume = static_cast<float>(VAL) / 100.0F;
            Audio::get()->enable(Options::audio.enabled);
            return "Audio vol:" + std::to_string(VAL);
        } catch (...) { return "usage: audio vol <0-100>"; }
    }
    return "usage: audio [on|off|vol n]";
}

// MUSIC [ON|OFF|VOL <0-100>]
static auto cmdMusic(const std::vector<std::string>& args) -> std::string {
    if (args.empty()) {
        const int VOL = static_cast<int>(Options::audio.music.volume * 100.0F);
        return std::string("Music ") + (Options::audio.music.enabled ? "ON" : "OFF") +
            " vol:" + std::to_string(VOL);
    }
    if (args[0] == "ON") {
        if (Options::audio.music.enabled) { return "Music already ON"; }
        Options::audio.music.enabled = true;
        Audio::get()->enableMusic(true);
        Audio::get()->setMusicVolume(Options::audio.music.volume);
        return "Music ON";
    }
    if (args[0] == "OFF") {
        if (!Options::audio.music.enabled) { return "Music already OFF"; }
        Audio::get()->setMusicVolume(0.0F);
        Audio::get()->enableMusic(false);
        Options::audio.music.enabled = false;
        return "Music OFF";
    }
    if (args[0] == "VOL" && args.size() >= 2) {
        try {
            const int VAL = std::stoi(args[1]);
            if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; }
            Options::audio.music.volume = static_cast<float>(VAL) / 100.0F;
            if (Options::audio.music.enabled) {
                Audio::get()->setMusicVolume(Options::audio.music.volume);
            }
            return "Music vol:" + std::to_string(VAL);
        } catch (...) { return "usage: music vol <0-100>"; }
    }
    return "usage: music [on|off|vol n]";
}

// SOUND [ON|OFF|VOL <0-100>]
static auto cmdSound(const std::vector<std::string>& args) -> std::string {
    if (args.empty()) {
        const int VOL = static_cast<int>(Options::audio.sound.volume * 100.0F);
        return std::string("Sound ") + (Options::audio.sound.enabled ? "ON" : "OFF") +
            " vol:" + std::to_string(VOL);
    }
    if (args[0] == "ON") {
        if (Options::audio.sound.enabled) { return "Sound already ON"; }
        Options::audio.sound.enabled = true;
        Audio::get()->enableSound(true);
        Audio::get()->setSoundVolume(Options::audio.sound.volume);
        return "Sound ON";
    }
    if (args[0] == "OFF") {
        if (!Options::audio.sound.enabled) { return "Sound already OFF"; }
        Audio::get()->setSoundVolume(0.0F);
        Audio::get()->enableSound(false);
        Options::audio.sound.enabled = false;
        return "Sound OFF";
    }
    if (args[0] == "VOL" && args.size() >= 2) {
        try {
            const int VAL = std::stoi(args[1]);
            if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; }
            Options::audio.sound.volume = static_cast<float>(VAL) / 100.0F;
            if (Options::audio.sound.enabled) {
                Audio::get()->setSoundVolume(Options::audio.sound.volume);
            }
            return "Sound vol:" + std::to_string(VAL);
        } catch (...) { return "usage: sound vol <0-100>"; }
    }
    return "usage: sound [on|off|vol n]";
}

#ifdef _DEBUG
// DEBUG [MODE [ON|OFF]|START [HERE|ROOM|POS|SCENE <name>]]
static auto cmdDebug(const std::vector<std::string>& args) -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    // --- START subcommands (START SCENE works from any scene) ---
    if (!args.empty() && args[0] == "START") {
        // START SCENE [<name>] — works from any scene
        if (args.size() >= 2 && args[1] == "SCENE") {
            SceneManager::Scene target = SceneManager::current;
            std::string name = "current";
            if (args.size() >= 3) {
                if (args[2] == "GAME") {
                    target = SceneManager::Scene::GAME;
                    name = "game";
                } else if (args[2] == "DEMO") {
                    target = SceneManager::Scene::DEMO;
                    name = "demo";
                } else if (args[2] == "LOGO") {
                    target = SceneManager::Scene::LOGO;
                    name = "logo";
                } else if (args[2] == "LOADING") {
                    target = SceneManager::Scene::LOADING_SCREEN;
                    name = "loading";
                } else if (args[2] == "TITLE") {
                    target = SceneManager::Scene::TITLE;
                    name = "title";
                } else if (args[2] == "CREDITS") {
                    target = SceneManager::Scene::CREDITS;
                    name = "credits";
                } else if (args[2] == "ENDING") {
                    target = SceneManager::Scene::ENDING;
                    name = "ending";
                } else if (args[2] == "ENDING2") {
                    target = SceneManager::Scene::ENDING2;
                    name = "ending2";
                } else {
                    std::string scene_lower = args[2];
                    std::ranges::transform(scene_lower, scene_lower.begin(), ::tolower);
                    return "Unknown scene: " + scene_lower;
                }
            }
            Debug::get()->setInitialScene(target);
            Debug::get()->saveToFile();
            return "Initial scene set to: " + name;
        }

        // START HERE|ROOM|POS — requires GAME scene
        if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
        if (!GameControl::set_initial_room || !GameControl::set_initial_pos) { return "Game not initialized"; }

        const bool DO_ROOM = args.size() == 1 || args[1] == "HERE" || args[1] == "ROOM";
        const bool DO_POS = args.size() == 1 || args[1] == "HERE" || args[1] == "POS";

        if (!DO_ROOM && !DO_POS) { return "usage: debug start [here|room|pos|scene <name>]"; }

        std::string result;
        if (DO_ROOM) { result = GameControl::set_initial_room(); }
        if (DO_POS) {
            if (!result.empty()) { result += ", "; }
            result += GameControl::set_initial_pos();
        }
        return result;
    }

    // --- MODE [ON|OFF] toggle (requires GAME scene) ---
    if (!args.empty() && args[0] == "MODE") {
        if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
        if (!GameControl::toggle_debug_mode) { return "Game not initialized"; }
        const bool ENABLED = Debug::get()->isEnabled();
        if (args.size() >= 2 && args[1] == "ON") {
            if (ENABLED) { return "Debug mode already ON"; }
            GameControl::toggle_debug_mode();
            return "Debug mode ON";
        }
        if (args.size() >= 2 && args[1] == "OFF") {
            if (!ENABLED) { return "Debug mode already OFF"; }
            GameControl::toggle_debug_mode();
            return "Debug mode OFF";
        }
        if (args.size() >= 2) { return "usage: debug mode [on|off]"; }
        GameControl::toggle_debug_mode();
        return std::string("Debug mode ") + (Debug::get()->isEnabled() ? "ON" : "OFF");
    }

    return "usage: debug [mode [on|off]|start [here|room|pos|scene <name>]]";
}

// ROOM <num>|NEXT|PREV
// Helper: cambia de room teniendo en cuenta el editor (exit → change → re-enter)
static auto changeRoomWithEditor(const std::string& room_file) -> std::string {
    if (!GameControl::change_room) { return "Game not initialized"; }

    const bool EDITOR_WAS_ACTIVE = (MapEditor::get() != nullptr) && MapEditor::get()->isActive();

    // Si el editor está activo, salir primero (guarda y recarga la room actual)
    if (EDITOR_WAS_ACTIVE && GameControl::exit_editor) {
        MapEditor::get()->setReenter(true);
        GameControl::exit_editor();
    }

    // Cambiar de habitación
    if (!GameControl::change_room(room_file)) {
        // Si falla, re-entrar al editor en la room original
        if (EDITOR_WAS_ACTIVE && GameControl::enter_editor) {
            GameControl::enter_editor();
        }
        return std::string("Room not found: ") + room_file;
    }

    // Si el editor estaba activo, re-entrar en la nueva room
    if (EDITOR_WAS_ACTIVE && GameControl::enter_editor) {
        GameControl::enter_editor();
    }

    return std::string("Room: ") + room_file;
}

static auto cmdRoom(const std::vector<std::string>& args) -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
    if (args.empty()) { return "usage: room <1-60>|next|prev|left|right|up|down"; }

    // DELETE: borrar la habitación actual
    if (args[0] == "DELETE") {
        if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; }
        return MapEditor::get()->deleteRoom();
    }

    // NEW [LEFT|RIGHT|UP|DOWN]: crear habitación nueva (opcionalmente conectada)
    if (args[0] == "NEW") {
        if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; }
        std::string direction = (args.size() >= 2) ? args[1] : "";
        return MapEditor::get()->createNewRoom(direction);
    }

    // Direcciones: LEFT, RIGHT, UP, DOWN
    if (args[0] == "LEFT" || args[0] == "RIGHT" || args[0] == "UP" || args[0] == "DOWN") {
        if (!GameControl::get_adjacent_room) { return "Game not initialized"; }
        const std::string ADJACENT = GameControl::get_adjacent_room(args[0]);
        if (ADJACENT == "0" || ADJACENT.empty()) {
            return "No room " + toLower(args[0]);
        }
        return changeRoomWithEditor(ADJACENT);
    }

    int num = 0;
    if (args[0] == "NEXT" || args[0] == "PREV") {
        if (!GameControl::get_current_room) { return "Game not initialized"; }
        const std::string CURRENT = GameControl::get_current_room();
        try {
            num = std::stoi(CURRENT.substr(0, CURRENT.find('.')));
        } catch (...) { return "Cannot determine current room"; }
        num += (args[0] == "NEXT") ? 1 : -1;
    } else {
        try {
            num = std::stoi(args[0]);
        } catch (...) { return "usage: room <1-60>|next|prev|left|right|up|down"; }
    }
    if (num < 1 || num > 60) { return "Room must be between 1 and 60"; }
    char buf[16];
    std::snprintf(buf, sizeof(buf), "%02d.yaml", num);
    return changeRoomWithEditor(buf);
}

// ITEMS <0-200>
static auto cmdItems(const std::vector<std::string>& args) -> std::string {
    if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
    if (args.empty()) { return "usage: items <0-200>"; }
    int count = 0;
    try {
        count = std::stoi(args[0]);
    } catch (...) { return "usage: items <0-200>"; }
    if (count < 0 || count > 200) { return "Items must be between 0 and 200"; }
    if (!GameControl::set_items) { return "Game not initialized"; }
    GameControl::set_items(count);
    return "Items: " + std::to_string(count);
}

// SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|DEMO|ENDING|ENDING2|RESTART]
static auto cmdScene(const std::vector<std::string>& args) -> std::string {
    if (Options::kiosk.enabled) { return "Not allowed in kiosk mode"; }
    if (args.empty()) { return "usage: scene [logo|loading|title|credits|game|demo|ending|ending2|restart]"; }

    if (args[0] == "RESTART") {
        SceneManager::scene_before_restart = SceneManager::current;
        SceneManager::current = SceneManager::Scene::RESTART_CURRENT;
        return "Restarting...";
    }

    const auto GO_TO = [](SceneManager::Scene target, const std::string& label) -> std::string {
        if (SceneManager::current == target) {
            SceneManager::scene_before_restart = target;
            SceneManager::current = SceneManager::Scene::RESTART_CURRENT;
        } else {
            SceneManager::current = target;
        }
        return "Scene: " + label;
    };

    if (args[0] == "LOGO") { return GO_TO(SceneManager::Scene::LOGO, "Logo"); }
    if (args[0] == "LOADING") { return GO_TO(SceneManager::Scene::LOADING_SCREEN, "Loading"); }
    if (args[0] == "TITLE") { return GO_TO(SceneManager::Scene::TITLE, "Title"); }
    if (args[0] == "CREDITS") { return GO_TO(SceneManager::Scene::CREDITS, "Credits"); }
    if (args[0] == "GAME") { return GO_TO(SceneManager::Scene::GAME, "Game"); }
    if (args[0] == "DEMO") { return GO_TO(SceneManager::Scene::DEMO, "Demo"); }
    if (args[0] == "ENDING") { return GO_TO(SceneManager::Scene::ENDING, "Ending"); }
    if (args[0] == "ENDING2") { return GO_TO(SceneManager::Scene::ENDING2, "Ending 2"); }
    return "Unknown scene: " + args[0];
}

// EDIT [ON|OFF|REVERT]
static auto cmdEdit(const std::vector<std::string>& args) -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    if (args.empty()) {
        // Toggle: si está activo → off, si no → on
        if ((MapEditor::get() != nullptr) && MapEditor::get()->isActive()) {
            if (GameControl::exit_editor) { GameControl::exit_editor(); }
            return "Editor OFF";
        }
        if (GameControl::enter_editor) {
            GameControl::enter_editor();
            return "Editor ON";
        }
        return "Not in game";
    }
    if (args[0] == "ON") {
        if (GameControl::enter_editor) {
            GameControl::enter_editor();
            return "Editor ON";
        }
        return "Not in game";
    }
    if (args[0] == "OFF") {
        if (GameControl::exit_editor) {
            GameControl::exit_editor();
            return "Editor OFF";
        }
        return "Not in game";
    }
    if (args[0] == "REVERT") {
        if (GameControl::revert_editor) {
            return GameControl::revert_editor();
        }
        return "Editor not active";
    }
    // EDIT SHOW/HIDE INFO/GRID
    if ((args[0] == "SHOW" || args[0] == "HIDE") && args.size() >= 2) {
        if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; }
        bool show = (args[0] == "SHOW");
        if (args[1] == "INFO") { return MapEditor::get()->showInfo(show); }
        if (args[1] == "GRID") { return MapEditor::get()->showGrid(show); }
    }
    // EDIT MAPBG/MAPCONN <color>
    if (args[0] == "MAPBG" && args.size() >= 2) {
        if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; }
        return MapEditor::get()->setMiniMapBg(args[1]);
    }
    if (args[0] == "MAPCONN" && args.size() >= 2) {
        if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; }
        return MapEditor::get()->setMiniMapConn(args[1]);
    }
    return "usage: edit [on|off|revert|show|hide|mapbg|mapconn] [...]";
}

// SET <property> <value> — modifica propiedad del enemigo seleccionado o de la habitación
static auto cmdSet(const std::vector<std::string>& args) -> std::string {
    if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; }
    if (args.empty()) { return "usage: set <property> <value>"; }

    // SET TILE no necesita argumento (abre el tile picker visual)
    if (args[0] == "TILE" && MapEditor::get()->hasSelectedItem()) {
        return MapEditor::get()->setItemProperty("TILE", "");
    }

    if (args.size() < 2) { return "usage: set <property> <value>"; }

    // Si hay enemigo seleccionado, aplicar a enemigo
    if (MapEditor::get()->hasSelectedEnemy()) {
        return MapEditor::get()->setEnemyProperty(args[0], args[1]);
    }

    // Si hay item seleccionado, aplicar a item
    if (MapEditor::get()->hasSelectedItem()) {
        return MapEditor::get()->setItemProperty(args[0], args[1]);
    }

    // Si no, aplicar a la habitación
    return MapEditor::get()->setRoomProperty(args[0], args[1]);
}

// ENEMY [ADD|DELETE|DUPLICATE]
static auto cmdEnemy(const std::vector<std::string>& args) -> std::string {
    if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; }
    if (args.empty()) { return "usage: enemy <add|delete|duplicate>"; }
    if (args[0] == "ADD") { return MapEditor::get()->addEnemy(); }
    if (args[0] == "DELETE") {
        if (!MapEditor::get()->hasSelectedEnemy()) { return "No enemy selected"; }
        return MapEditor::get()->deleteEnemy();
    }
    if (args[0] == "DUPLICATE") {
        if (!MapEditor::get()->hasSelectedEnemy()) { return "No enemy selected"; }
        return MapEditor::get()->duplicateEnemy();
    }
    return "usage: enemy <add|delete|duplicate>";
}

// ITEM [ADD|DELETE|DUPLICATE]
static auto cmdItem(const std::vector<std::string>& args) -> std::string {
    if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; }
    if (args.empty()) { return "usage: item <add|delete|duplicate>"; }
    if (args[0] == "ADD") { return MapEditor::get()->addItem(); }
    if (args[0] == "DELETE") {
        if (!MapEditor::get()->hasSelectedItem()) { return "No item selected"; }
        return MapEditor::get()->deleteItem();
    }
    if (args[0] == "DUPLICATE") {
        if (!MapEditor::get()->hasSelectedItem()) { return "No item selected"; }
        return MapEditor::get()->duplicateItem();
    }
    return "usage: item <add|delete|duplicate>";
}
#endif

// SHOW [INFO|NOTIFICATION|CHEEVO]
static auto cmdShow(const std::vector<std::string>& args) -> std::string {
#ifdef _DEBUG
    if (!args.empty() && args[0] == "NOTIFICATION") {
        Notifier::get()->show({"NOTIFICATION"});
        return "Notification shown";
    }
    if (!args.empty() && args[0] == "CHEEVO") {
        Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c1")}, Notifier::Style::CHEEVO, -1, false);  // NOLINT(readability-static-accessed-through-instance)
        return "Cheevo notification shown";
    }
    if (args.empty() || args[0] != "INFO") { return "usage: show [info|notification|cheevo]"; }
#else
    if (args.empty() || args[0] != "INFO") { return "usage: show [info]"; }
#endif
    if (RenderInfo::get()->isActive()) { return "Info overlay already ON"; }
    RenderInfo::get()->toggle();
    return "Info overlay ON";
}

// HIDE [INFO]
static auto cmdHide(const std::vector<std::string>& args) -> std::string {
    if (args.empty() || args[0] != "INFO") { return "usage: hide [info]"; }
    if (!RenderInfo::get()->isActive()) { return "Info overlay already OFF"; }
    RenderInfo::get()->toggle();
    return "Info overlay OFF";
}

// CHEAT [subcomando]
static auto cmdCheat(const std::vector<std::string>& args) -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
    if (args.empty()) { return "usage: cheat [infinite lives|invincibility|open the jail|close the jail]"; }

    // CHEAT INFINITE LIVES [ON|OFF]
    if (args[0] == "INFINITE") {
        if (args.size() < 2 || args[1] != "LIVES") { return "usage: cheat infinite lives [on|off]"; }
        auto& cheat = Options::cheats.infinite_lives;
        using State = Options::Cheat::State;
        if (args.size() == 2) {
            cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED;
        } else if (args[2] == "ON") {
            if (cheat == State::ENABLED) { return "Infinite lives already ON"; }
            cheat = State::ENABLED;
        } else if (args[2] == "OFF") {
            if (cheat == State::DISABLED) { return "Infinite lives already OFF"; }
            cheat = State::DISABLED;
        } else {
            return "usage: cheat infinite lives [on|off]";
        }
        return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF");
    }

    // CHEAT INVINCIBILITY [ON|OFF]
    if (args[0] == "INVINCIBILITY" || args[0] == "INVENCIBILITY") {
        auto& cheat = Options::cheats.invincible;
        using State = Options::Cheat::State;
        if (args.size() == 1) {
            cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED;
        } else if (args[1] == "ON") {
            if (cheat == State::ENABLED) { return "Invincibility already ON"; }
            cheat = State::ENABLED;
        } else if (args[1] == "OFF") {
            if (cheat == State::DISABLED) { return "Invincibility already OFF"; }
            cheat = State::DISABLED;
        } else {
            return "usage: cheat invincibility [on|off]";
        }
        return std::string("Invincibility ") + (cheat == State::ENABLED ? "ON" : "OFF");
    }

    // CHEAT OPEN THE JAIL
    if (args[0] == "OPEN") {
        if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat open the jail"; }
        if (Options::cheats.jail_is_open == Options::Cheat::State::ENABLED) { return "Jail already open"; }
        Options::cheats.jail_is_open = Options::Cheat::State::ENABLED;
        return "Jail opened";
    }

    // CHEAT CLOSE THE JAIL
    if (args[0] == "CLOSE") {
        if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "usage: cheat close the jail"; }
        if (Options::cheats.jail_is_open == Options::Cheat::State::DISABLED) { return "Jail already closed"; }
        Options::cheats.jail_is_open = Options::Cheat::State::DISABLED;
        return "Jail closed";
    }

    return "usage: cheat [infinite lives|invincibility|open the jail|close the jail]";
}

// PLAYER SKIN / PLAYER COLOR
static auto cmdPlayer(const std::vector<std::string>& args) -> std::string {
    if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }

    // PLAYER SKIN <name>
    if (args.size() >= 2 && args[0] == "SKIN") {
        if (!GameControl::change_player_skin) { return "Game not initialized"; }
        std::string skin_name = args[1];
        std::ranges::transform(skin_name, skin_name.begin(), ::tolower);
        GameControl::change_player_skin(skin_name);
        return "Player skin: " + skin_name;
    }

    // PLAYER COLOR DEFAULT
    if (args.size() >= 2 && args[0] == "COLOR" && args[1] == "DEFAULT") {
        if (!GameControl::change_player_color) { return "Game not initialized"; }
        GameControl::change_player_color(-1);
        return "Player color: default";
    }

    // PLAYER COLOR <0-15>
    if (args.size() >= 2 && args[0] == "COLOR") {
        int color = -1;
        try {
            color = std::stoi(args[1]);
        } catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
        }
        if (color < 0 || color > 15) { return "usage: player color <0-15>|default"; }
        if (!GameControl::change_player_color) { return "Game not initialized"; }
        GameControl::change_player_color(color);
        return "Player color: " + std::to_string(color);
    }

    return "usage: player skin <name> | player color <0-15>|default";
}

// RESTART
static auto cmdRestart(const std::vector<std::string>& /*unused*/) -> std::string {
    SceneManager::current = SceneManager::Scene::LOGO;
    Audio::get()->stopMusic();
    return "Restarting...";
}

// KIOSK [ON|OFF PLEASE|PLEASE]
static auto cmdKiosk(const std::vector<std::string>& args) -> std::string {
    const bool DISABLE = (!args.empty() && args[0] == "PLEASE") ||
        (args.size() >= 2 && args[0] == "OFF" && args[1] == "PLEASE");
    if (DISABLE) {
        Options::kiosk.enabled = false;
        return "Kiosk mode OFF";
    }
    if (!args.empty() && args[0] == "OFF") {
        return "Not allowed in kiosk mode";
    }
    if (args.empty() || args[0] == "ON") {
        if (Options::kiosk.enabled) { return "Kiosk mode already ON"; }
        Options::kiosk.enabled = true;
        if (!Options::video.fullscreen) { Screen::get()->toggleVideoMode(); }
        return "Kiosk mode ON";
    }
    return "usage: kiosk [on]";
}

// EXIT / QUIT
static auto cmdExit(const std::vector<std::string>& args) -> std::string {
#ifdef __EMSCRIPTEN__
    (void)args;
    return "Not allowed in web version";
#else
    if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
        return "Not allowed in kiosk mode";
    }
    SceneManager::current = SceneManager::Scene::QUIT;
    return "Quitting...";
#endif
}

// SIZE
static auto cmdSize(const std::vector<std::string>& /*unused*/) -> std::string {
    int w = 0;
    int h = 0;
    SDL_GetWindowSize(SDL_GetRenderWindow(Screen::get()->getRenderer()), &w, &h);
    return std::to_string(w) + "x" + std::to_string(h);
}

// ── CommandRegistry ──────────────────────────────────────────────────────────

void CommandRegistry::registerHandlers() {  // NOLINT(readability-function-cognitive-complexity)
    handlers_["cmd_ss"] = cmdSs;
    handlers_["cmd_shader"] = cmdShader;
    handlers_["cmd_border"] = cmdBorder;
    handlers_["cmd_fullscreen"] = cmdFullscreen;
    handlers_["cmd_zoom"] = cmdZoom;
    handlers_["cmd_intscale"] = cmdIntscale;
    handlers_["cmd_vsync"] = cmdVsync;
    handlers_["cmd_driver"] = cmdDriver;
    handlers_["cmd_palette"] = cmdPalette;
    handlers_["cmd_audio"] = cmdAudio;
    handlers_["cmd_music"] = cmdMusic;
    handlers_["cmd_sound"] = cmdSound;
    handlers_["cmd_show"] = cmdShow;
    handlers_["cmd_hide"] = cmdHide;
    handlers_["cmd_cheat"] = cmdCheat;
    handlers_["cmd_player"] = cmdPlayer;
    handlers_["cmd_restart"] = cmdRestart;
    handlers_["cmd_kiosk"] = cmdKiosk;
    handlers_["cmd_exit"] = cmdExit;
    handlers_["cmd_quit"] = cmdExit;  // QUIT usa el mismo handler que EXIT
    handlers_["cmd_size"] = cmdSize;
#ifdef _DEBUG
    handlers_["cmd_debug"] = cmdDebug;
    handlers_["cmd_items"] = cmdItems;
    handlers_["cmd_room"] = cmdRoom;
    handlers_["cmd_scene"] = cmdScene;
    handlers_["cmd_edit"] = cmdEdit;
    handlers_["cmd_set"] = cmdSet;
    handlers_["cmd_enemy"] = cmdEnemy;
    handlers_["cmd_item"] = cmdItem;
#endif
    // HELP se registra en load() como lambda que captura this

    // Proveedores de completions dinámicas
    // PALETTE: NEXT, PREV + nombres de paletas disponibles (UPPERCASE)
    dynamic_providers_["PALETTE"] = []() -> std::vector<std::string> {
        std::vector<std::string> result = {"NEXT", "PREV", "SORT", "DEFAULT"};
        if (Screen::get() != nullptr) {
            const auto NAMES = Screen::get()->getPaletteNames();
            std::ranges::transform(NAMES, std::back_inserter(result), [](const auto& name) { return toUpper(name); });
        }
        return result;
    };

    // SHADER PRESET: NEXT, PREV + nombres de presets del shader activo (UPPERCASE)
    dynamic_providers_["SHADER PRESET"] = []() -> std::vector<std::string> {
        std::vector<std::string> result = {"NEXT", "PREV"};
        const bool IS_CRTPI = Options::video.shader.current_shader == Rendering::ShaderType::CRTPI;
        auto upper_name = [](const auto& p) { return toUpper(p.name); };
        if (IS_CRTPI) {
            std::ranges::transform(Options::crtpi_presets, std::back_inserter(result), upper_name);
        } else {
            std::ranges::transform(Options::postfx_presets, std::back_inserter(result), upper_name);
        }
        return result;
    };

#ifdef _DEBUG
    // Colores de la paleta (compartido por SET COLOR, BGCOLOR, BORDER, ITEMCOLOR1, ITEMCOLOR2)
    auto color_provider = []() -> std::vector<std::string> {
        return {"BLACK", "BRIGHT_BLACK", "BLUE", "BRIGHT_BLUE", "RED", "BRIGHT_RED", "MAGENTA", "BRIGHT_MAGENTA", "GREEN", "BRIGHT_GREEN", "CYAN", "BRIGHT_CYAN", "YELLOW", "BRIGHT_YELLOW", "WHITE", "BRIGHT_WHITE"};
    };
    dynamic_providers_["SET COLOR"] = color_provider;
    dynamic_providers_["SET BGCOLOR"] = color_provider;
    dynamic_providers_["EDIT MAPBG"] = color_provider;
    dynamic_providers_["EDIT MAPCONN"] = color_provider;

    // SET: propiedades dinámicas según selección en el editor
    dynamic_providers_["SET"] = []() -> std::vector<std::string> {
        if (MapEditor::get() != nullptr && MapEditor::get()->isActive()) {
            return MapEditor::get()->getSetCompletions();
        }
        return {};
    };

    // SET COLOR/BGCOLOR/etc. ya tienen sus providers arriba

    // HELP: lista de comandos visibles en el scope activo
    dynamic_providers_["HELP"] = [this]() -> std::vector<std::string> {
        return getVisibleKeywords();
    };
    dynamic_providers_["SET BORDER"] = color_provider;
    dynamic_providers_["SET ITEMCOLOR1"] = color_provider;
    dynamic_providers_["SET ITEMCOLOR2"] = color_provider;

    // SET ANIMATION: animaciones de enemigos (nombres sin extensión, UPPERCASE)
    dynamic_providers_["SET ANIMATION"] = []() -> std::vector<std::string> {
        std::vector<std::string> result;
        auto list = Resource::List::get()->getListByType(Resource::List::Type::ANIMATION);
        for (const auto& path : list) {
            if (path.find("enemies") == std::string::npos) { continue; }
            std::string name = getFileName(path);
            auto dot = name.rfind('.');
            if (dot != std::string::npos) { name.resize(dot); }
            result.push_back(toUpper(name));
        }
        return result;
    };

    // SET TILESET: tilesets disponibles (nombres sin extensión, UPPERCASE)
    dynamic_providers_["SET TILESET"] = []() -> std::vector<std::string> {
        std::vector<std::string> result;
        auto list = Resource::List::get()->getListByType(Resource::List::Type::BITMAP);
        for (const auto& path : list) {
            if (path.find("tilesets") == std::string::npos) { continue; }
            std::string name = getFileName(path);
            auto dot = name.rfind('.');
            if (dot != std::string::npos) { name.resize(dot); }
            result.push_back(toUpper(name));
        }
        return result;
    };
#endif
}

void CommandRegistry::load(const std::string& yaml_path) {  // NOLINT(readability-function-cognitive-complexity)
    registerHandlers();

    // Cargar y parsear el YAML
    auto file_data = Resource::Helper::loadFile(yaml_path);
    if (file_data.empty()) {
        std::cerr << "CommandRegistry: Unable to load " << yaml_path << '\n';
        return;
    }

    std::string yaml_content(file_data.begin(), file_data.end());
    fkyaml::node yaml;
    try {
        yaml = fkyaml::node::deserialize(yaml_content);
    } catch (const fkyaml::exception& e) {
        std::cerr << "CommandRegistry: YAML parse error: " << e.what() << '\n';
        return;
    }

    if (!yaml.contains("categories")) { return; }

    for (const auto& cat_node : yaml["categories"]) {
        const auto CATEGORY = cat_node["name"].get_value<std::string>();
        const bool CAT_DEBUG_ONLY = cat_node.contains("debug_only") && cat_node["debug_only"].get_value<bool>();

        // Scopes por defecto de la categoría
        std::vector<std::string> cat_scopes;
        if (cat_node.contains("scope")) {
            const auto& scope_node = cat_node["scope"];
            if (scope_node.is_sequence()) {
                std::ranges::transform(scope_node, std::back_inserter(cat_scopes), [](const auto& s) { return s.template get_value<std::string>(); });
            } else {
                cat_scopes.push_back(scope_node.get_value<std::string>());
            }
        }

        if (!cat_node.contains("commands")) { continue; }

        for (const auto& cmd_node : cat_node["commands"]) {
            CommandDef def;
            def.keyword = cmd_node["keyword"].get_value<std::string>();
            def.handler_id = cmd_node["handler"].get_value<std::string>();
            def.category = CATEGORY;
            def.description = cmd_node.contains("description") ? cmd_node["description"].get_value<std::string>() : "";
            def.usage = cmd_node.contains("usage") ? cmd_node["usage"].get_value<std::string>() : def.keyword;
            def.instant = cmd_node.contains("instant") && cmd_node["instant"].get_value<bool>();
            def.hidden = cmd_node.contains("hidden") && cmd_node["hidden"].get_value<bool>();
            def.debug_only = CAT_DEBUG_ONLY || (cmd_node.contains("debug_only") && cmd_node["debug_only"].get_value<bool>());
            def.help_hidden = cmd_node.contains("help_hidden") && cmd_node["help_hidden"].get_value<bool>();
            def.dynamic_completions = cmd_node.contains("dynamic_completions") && cmd_node["dynamic_completions"].get_value<bool>();

            // Scopes: del comando, o hereda de la categoría, o "global" por defecto
            if (cmd_node.contains("scope")) {
                const auto& scope_node = cmd_node["scope"];
                if (scope_node.is_sequence()) {
                    for (const auto& s : scope_node) { def.scopes.push_back(s.get_value<std::string>()); }
                } else {
                    def.scopes.push_back(scope_node.get_value<std::string>());
                }
            } else if (!cat_scopes.empty()) {
                def.scopes = cat_scopes;
            } else {
                def.scopes.emplace_back("global");
            }

            // Completions estáticas
            if (cmd_node.contains("completions")) {
                auto completions_node = cmd_node["completions"];
                for (auto it = completions_node.begin(); it != completions_node.end(); ++it) {
                    auto path = it.key().get_value<std::string>();
                    std::vector<std::string> opts;
                    std::ranges::transform(*it, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
                    def.completions[path] = std::move(opts);
                }
            }

            // Aplicar debug_extras en debug builds
#ifdef _DEBUG
            if (cmd_node.contains("debug_extras")) {
                const auto& extras = cmd_node["debug_extras"];
                if (extras.contains("description")) { def.description = extras["description"].get_value<std::string>(); }
                if (extras.contains("usage")) { def.usage = extras["usage"].get_value<std::string>(); }
                if (extras.contains("hidden")) { def.hidden = extras["hidden"].get_value<bool>(); }
                if (extras.contains("help_hidden")) { def.help_hidden = extras["help_hidden"].get_value<bool>(); }
                if (extras.contains("completions")) {
                    def.completions.clear();
                    auto extras_completions = extras["completions"];
                    for (auto it = extras_completions.begin(); it != extras_completions.end(); ++it) {
                        auto path = it.key().get_value<std::string>();
                        std::vector<std::string> opts;
                        std::ranges::transform(*it, std::back_inserter(opts), [](const auto& opt) { return opt.template get_value<std::string>(); });
                        def.completions[path] = std::move(opts);
                    }
                }
            }
#endif

            // En Release: saltar comandos debug_only
#ifndef _DEBUG
            if (def.debug_only) { continue; }
#endif

            commands_.push_back(std::move(def));
        }
    }

    // Registrar el handler de HELP (captura this)
    handlers_["cmd_help"] = [this](const std::vector<std::string>& args) -> std::string {
        if (!args.empty()) {
            // HELP <command>: mostrar ayuda detallada de un comando
            const auto* cmd = findCommand(args[0]);
            if (cmd != nullptr) {
                std::string kw_lower = cmd->keyword;
                std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower);
                std::string result = kw_lower + ": " + cmd->description + "\n" + cmd->usage;

                // Listar subcomandos/opciones si hay completions
                auto opts = getCompletions(cmd->keyword);
                if (!opts.empty()) {
                    result += "\noptions:";
                    for (const auto& opt : opts) {
                        std::string opt_lower = opt;
                        std::ranges::transform(opt_lower, opt_lower.begin(), ::tolower);
                        result += " " + opt_lower;
                    }
                }
                return result;
            }
            return "Unknown command: " + args[0];
        }
        std::cout << generateTerminalHelp();
        return generateConsoleHelp();
    };

    // Aplanar completions en el mapa global
    for (const auto& cmd : commands_) {
        for (const auto& [path, opts] : cmd.completions) {
            completions_map_[path] = opts;
        }
    }
}

auto CommandRegistry::findCommand(const std::string& keyword) const -> const CommandDef* {
    auto it = std::ranges::find_if(commands_,
        [&keyword](const auto& cmd) { return cmd.keyword == keyword; });
    return (it != commands_.end()) ? &(*it) : nullptr;
}

auto CommandRegistry::execute(const std::string& keyword, const std::vector<std::string>& args) const -> std::string {
    const auto* def = findCommand(keyword);
    if (def == nullptr) { return ""; }
    const auto IT = handlers_.find(def->handler_id);
    if (IT == handlers_.end()) { return "Handler not found: " + def->handler_id; }
    return IT->second(args);
}

auto CommandRegistry::generateTerminalHelp() const -> std::string {
    std::ostringstream out;
    out << "=== JDD CONSOLE COMMANDS ===" << '\n';

    std::string current_category;
    for (const auto& cmd : commands_) {
        if (cmd.help_hidden) { continue; }

        if (cmd.category != current_category) {
            current_category = cmd.category;
            out << '\n'
                << '[' << current_category << ']' << '\n';
        }

        // Formatear: usage alineado a la izquierda, descripción a la derecha
        const std::string& usage = cmd.usage;
        out << "  " << usage;
        if (!cmd.description.empty()) {
            // Padding para alinear descripción (columna ~40)
            const int PAD = std::max(1, 38 - static_cast<int>(usage.size()));
            out << std::string(static_cast<size_t>(PAD), ' ') << cmd.description;
        }
        out << '\n';
    }

    return out.str();
}

auto CommandRegistry::generateConsoleHelp() const -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    // Agrupar comandos visibles por scope
    std::string global_cmds;
    std::string debug_cmds;
    std::string editor_cmds;

    for (const auto& cmd : commands_) {
        if (cmd.help_hidden) { continue; }
        if (!isCommandVisible(cmd)) { continue; }

        std::string kw_lower = cmd.keyword;
        std::ranges::transform(kw_lower, kw_lower.begin(), ::tolower);

        // Clasificar por el PRIMER scope del comando
        const std::string& primary = cmd.scopes.empty() ? "global" : cmd.scopes[0];

        if (primary == "editor") {
            if (!editor_cmds.empty()) { editor_cmds += ", "; }
            editor_cmds += kw_lower;
        } else if (primary == "debug") {
            if (!debug_cmds.empty()) { debug_cmds += ", "; }
            debug_cmds += kw_lower;
        } else {
            if (!global_cmds.empty()) { global_cmds += ", "; }
            global_cmds += kw_lower;
        }
    }

    // Construir resultado
    std::string result;

    if (active_scope_ == "editor" && !editor_cmds.empty()) {
        result += "Editor:\n" + editor_cmds + "\n";
        result += "keys: 9=editor 8=grid e=eraser m=map\n";
    }

    if (!debug_cmds.empty()) {
        if (!result.empty()) { result += "\n"; }
        result += "Debug:\n" + debug_cmds + "\n";
    }

    if (!result.empty()) { result += "\n"; }
    result += "Commands:\n" + global_cmds + "\n";
    result += "-- more info on the terminal";
    return result;
}

auto CommandRegistry::getCompletions(const std::string& path) const -> std::vector<std::string> {
    // Verificar que el comando raíz es visible en el scope activo
    if (!active_scope_.empty()) {
        std::string root = path;
        auto space = root.find(' ');
        if (space != std::string::npos) { root.resize(space); }
        const auto* cmd = findCommand(root);
        if (cmd != nullptr && !isCommandVisible(*cmd)) { return {}; }
    }

    // Primero: buscar proveedor dinámico (tiene prioridad si existe)
    const auto DYN_IT = dynamic_providers_.find(path);
    if (DYN_IT != dynamic_providers_.end()) {
        return DYN_IT->second();
    }
    // Fallback: completions estáticas del YAML
    const auto IT = completions_map_.find(path);
    if (IT != completions_map_.end()) { return IT->second; }
    return {};
}

// Comprueba si un comando es utilizable en el scope activo (acumulativo)
// Release: global + game
// Debug: global + game + debug
// Editor: global + game + debug + editor
auto CommandRegistry::isCommandVisible(const CommandDef& cmd) const -> bool {
    if (cmd.hidden) { return false; }

    return std::ranges::any_of(cmd.scopes, [this](const auto& s) {
        if (s == "global" || s == "game") { return true; }
        if (s == "debug" && (active_scope_ == "debug" || active_scope_ == "editor")) { return true; }
        return s == "editor" && active_scope_ == "editor";
    });
}

auto CommandRegistry::getVisibleKeywords() const -> std::vector<std::string> {
    std::vector<std::string> result;
    for (const auto& cmd : commands_) {
        if (isCommandVisible(cmd)) {
            result.push_back(cmd.keyword);
        }
    }
    return result;
}
