#include "game/ui/console.hpp"

#include <SDL3/SDL.h>

#include <cctype>      // Para toupper
#include <functional>  // Para function
#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/rendering/sprite/sprite.hpp"   // Para Sprite
#include "core/rendering/surface.hpp"         // Para Surface
#include "core/rendering/text.hpp"            // Para Text
#include "core/resources/resource_cache.hpp"  // Para Resource
#include "game/game_control.hpp"              // Para GameControl (refresh_player_color)
#include "game/options.hpp"                   // Para Options
#include "game/scene_manager.hpp"             // Para SceneManager
#include "game/ui/notifier.hpp"               // Para Notifier

#ifdef _DEBUG
#include "core/system/debug.hpp"  // Para Debug
#endif

// ── Sistema de comandos ────────────────────────────────────────────────────────

struct ConsoleCommand {
    std::string_view keyword;
    std::function<std::string(const std::vector<std::string>& args)> execute;
};

// Convierte la entrada a uppercase y la divide en tokens por espacios
static auto parseTokens(const std::string& input) -> std::vector<std::string> {
    std::vector<std::string> tokens;
    std::string token;
    for (unsigned char c : input) {
        if (c == ' ') {
            if (!token.empty()) {
                tokens.push_back(token);
                token.clear();
            }
        } else {
            token += static_cast<char>(std::toupper(c));
        }
    }
    if (!token.empty()) {
        tokens.push_back(token);
    }
    return tokens;
}

// Macro para comando de toggle booleano (evita repetición en ON/OFF)
#define BOOL_TOGGLE_CMD(label, getter, toggle_fn)                    \
    [](const std::vector<std::string>& args) -> std::string {        \
        if (args.empty()) {                                          \
            (toggle_fn);                                             \
            return label " " + std::string((getter) ? "ON" : "OFF"); \
        }                                                            \
        if (args[0] == "ON") {                                       \
            if (getter) { return label " already ON"; }              \
            (toggle_fn);                                             \
            return label " ON";                                      \
        }                                                            \
        if (args[0] == "OFF") {                                      \
            if (!(getter)) { return label " already OFF"; }          \
            (toggle_fn);                                             \
            return label " OFF";                                     \
        }                                                            \
        return "Usage: " label " [ON|OFF]";                          \
    }

// Texto de ayuda común para HELP y ?
static void printHelp() {
    SDL_Log("=== JDD CONSOLE COMMANDS ===");
    SDL_Log("  SS [ON|OFF|SIZE]               Supersampling");
    SDL_Log("  SS UPSCALE [NEAREST|LINEAR]    SS upscale filter");
    SDL_Log("  SS DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]  SS downscale algorithm");
    SDL_Log("  SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI]  Toggle/select shader (F4)");
    SDL_Log("  SET PLAYER SKIN <1|2>          Change player skin (GAME only)");
    SDL_Log("  BORDER [ON|OFF]                Decorative border (B)");
    SDL_Log("  FULLSCREEN [ON|OFF]            Fullscreen mode (F3)");
    SDL_Log("  ZOOM [UP|DOWN]                 Window zoom (F1/F2)");
    SDL_Log("  INTSCALE [ON|OFF]              Integer scaling (F7)");
    SDL_Log("  VSYNC [ON|OFF]                 Vertical sync");
    SDL_Log("  DRIVER [LIST|AUTO|NONE|<name>] GPU driver (restart to apply)");
    SDL_Log("  PALETTE [NEXT|PREV]            Color palette (F5/F6)");
#ifdef _DEBUG
    SDL_Log("  DEBUG                          Toggle debug overlay (F12)");
    SDL_Log("  ROOM <1-60>|NEXT|PREV          Change to room number (GAME only)");
    SDL_Log("  SET INITIAL [ROOM|POS]         Set initial room/position from current state (GAME only)");
    SDL_Log("  SET INITIAL SCENE [<name>]     Set initial debug scene (GAME|LOGO|TITLE|LOADING|CREDITS|ENDING|ENDING2)");
    SDL_Log("  SET ITEMS <0-200>              Set collected items count (GAME only)");
    SDL_Log("  CHEAT INFINITE LIVES [ON|OFF]  Infinite lives (GAME only)");
    SDL_Log("  CHEAT INVINCIBILITY [ON|OFF]   Invincibility (GAME only)");
    SDL_Log("  CHEAT OPEN THE JAIL            Open the jail (GAME only)");
    SDL_Log("  CHEAT CLOSE THE JAIL           Close the jail (GAME only)");
#endif
    SDL_Log("  AUDIO [ON|OFF|VOL <0-100>]     Audio master");
    SDL_Log("  MUSIC [ON|OFF|VOL <0-100>]     Music volume");
    SDL_Log("  SOUND [ON|OFF|VOL <0-100>]     Sound volume");
    SDL_Log("  SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]");
    SDL_Log("  SIZE                           Window size in pixels");
    SDL_Log("  KIOSK [ON]                     Enable kiosk mode");
    SDL_Log("  EXIT / QUIT                    Quit application");
    SDL_Log("  SHOW [FPS|INFO|NOTIFICATION|CHEEVO]  Show debug overlay or test notification");
    SDL_Log("  HIDE [FPS|INFO]               Hide debug overlay");
    SDL_Log("  HELP/?                         Show this help in terminal");
}

// Tabla de comandos disponibles
static const std::vector<ConsoleCommand> COMMANDS = {
    // SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]] — Supersampling
    {.keyword = "SS", .execute = [](const std::vector<std::string>& args) -> std::string {
         static const std::array<std::string_view, 3> DOWNSCALE_NAMES = {"Bilinear", "Lanczos2", "Lanczos3"};
         if (!args.empty() && args[0] == "SIZE") {
             if (!Options::video.supersampling) { 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.linear_upscale);
                 return std::string("Upscale: ") + (Options::video.linear_upscale ? "Linear" : "Nearest");
             }
             if (args[1] == "NEAREST") {
                 if (!Options::video.linear_upscale) { return "Upscale already Nearest"; }
                 Screen::get()->setLinearUpscale(false);
                 return "Upscale: Nearest";
             }
             if (args[1] == "LINEAR") {
                 if (Options::video.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.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.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 ? "ON" : "OFF");
         }
         if (args[0] == "ON") {
             if (Options::video.supersampling) { return "Supersampling already ON"; }
             Screen::get()->toggleSupersampling();
             return "PostFX Supersampling ON";
         }
         if (args[0] == "OFF") {
             if (!Options::video.supersampling) { return "Supersampling already OFF"; }
             Screen::get()->toggleSupersampling();
             return "PostFX Supersampling OFF";
         }
         return "Usage: SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]]";
     }},

    // SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI] — Toggle/cicla/selecciona shader (F4 / Shift+F4)
    {.keyword = "SHADER", .execute = [](const std::vector<std::string>& args) -> std::string {
         if (args.empty()) {
             Screen::get()->toggleShaders();
             return std::string("Shaders ") + (Options::video.postfx ? "ON" : "OFF");
         }
         if (args[0] == "ON") {
             if (Options::video.postfx) { return "Shaders already ON"; }
             Screen::get()->toggleShaders();
             return "Shaders ON";
         }
         if (args[0] == "OFF") {
             if (!Options::video.postfx) { return "Shaders already OFF"; }
             Screen::get()->toggleShaders();
             return "Shaders 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") {
             // SHADER NEXT PRESET → cicla presets del shader activo
             if (args.size() >= 2 && args[1] == "PRESET") {
                 if (Options::current_shader == Rendering::ShaderType::CRTPI) {
                     if (Options::crtpi_presets.empty()) { return "No CrtPi presets available"; }
                     Options::current_crtpi_preset =
                         (Options::current_crtpi_preset + 1) %
                         static_cast<int>(Options::crtpi_presets.size());
                     Screen::get()->reloadCrtPi();
                     return "CrtPi preset: " +
                         Options::crtpi_presets[static_cast<size_t>(Options::current_crtpi_preset)].name;
                 }
                 if (Options::postfx_presets.empty()) { return "No PostFX presets available"; }
                 Options::current_postfx_preset =
                     (Options::current_postfx_preset + 1) %
                     static_cast<int>(Options::postfx_presets.size());
                 Screen::get()->reloadPostFX();
                 return "PostFX preset: " +
                     Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)].name;
             }
             // SHADER NEXT → cicla entre tipos de shader (POSTFX ↔ CRTPI)
             Screen::get()->nextShader();
             return std::string("Shader: ") +
                 (Options::current_shader == Rendering::ShaderType::CRTPI ? "CRTPI" : "POSTFX");
         }
         return "Usage: SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI]";
     }},

    // BORDER [ON|OFF] — Borde decorativo (B)
    {.keyword = "BORDER", .execute = BOOL_TOGGLE_CMD("Border", Options::video.border.enabled, Screen::get()->toggleBorder())},

    // FULLSCREEN [ON|OFF [PLEASE]] — Pantalla completa (F3); OFF bloqueado en kiosk sin PLEASE
    {.keyword = "FULLSCREEN", .execute = [](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 — Zoom de ventana (F1/F2)
    {.keyword = "ZOOM", .execute = [](const std::vector<std::string>& args) -> std::string {
         if (args.empty()) { return "Usage: ZOOM [UP|DOWN]"; }
         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);
         }
         return "Usage: ZOOM [UP|DOWN]";
     }},

    // INTSCALE [ON|OFF] — Escalado entero (F7)
    {.keyword = "INTSCALE", .execute = [](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] — Sincronización vertical
    {.keyword = "VSYNC", .execute = BOOL_TOGGLE_CMD("VSync", Options::video.vertical_sync, Screen::get()->toggleVSync())},

    // DRIVER [LIST|AUTO|<nombre>] — Driver GPU (aplica en el próximo arranque)
    {.keyword = "DRIVER", .execute = [](const std::vector<std::string>& args) -> std::string {
         // Sin argumentos: muestra el driver activo (permitido en kiosk)
         if (args.empty()) {
             const auto& driver = Screen::get()->getGPUDriver();
             return "GPU: " + (driver.empty() ? std::string("sdl") : driver);
         }
         // LIST: lista drivers disponibles marcando el activo con * (permitido en kiosk)
         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;
         }
         // Cambiar driver: bloqueado en kiosk salvo PLEASE
         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);
         // Validar que el nombre existe en la lista de drivers SDL
         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 — Paleta de colores (F5/F6)
    {.keyword = "PALETTE", .execute = [](const std::vector<std::string>& args) -> std::string {
         if (args.empty()) { return "Usage: PALETTE [NEXT|PREV]"; }
         if (args[0] == "NEXT") {
             Screen::get()->nextPalette();
             return "Palette: " + Options::video.palette;
         }
         if (args[0] == "PREV") {
             Screen::get()->previousPalette();
             return "Palette: " + Options::video.palette;
         }
         return "Usage: PALETTE [NEXT|PREV]";
     }},

#ifdef _DEBUG
    // DEBUG [ON|OFF] — Activa/desactiva el modo debug del juego (tecla 0); solo en escena GAME
    {.keyword = "DEBUG", .execute = [](const std::vector<std::string>& args) -> std::string {
         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.empty() && args[0] == "ON") {
             if (ENABLED) { return "Debug mode already ON"; }
             GameControl::toggle_debug_mode();
             return "Debug mode ON";
         }
         if (!args.empty() && args[0] == "OFF") {
             if (!ENABLED) { return "Debug mode already OFF"; }
             GameControl::toggle_debug_mode();
             return "Debug mode OFF";
         }
         if (!args.empty()) { return "Usage: DEBUG [ON|OFF]"; }
         GameControl::toggle_debug_mode();
         return std::string("Debug mode ") + (Debug::get()->isEnabled() ? "ON" : "OFF");
     }},

    // SHOW FPS / SHOW INFO / SHOW NOTIFICATION / SHOW CHEEVO / HIDE FPS / HIDE INFO
    {.keyword = "SHOW", .execute = [](const std::vector<std::string>& args) -> std::string {
         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] != "FPS" && args[0] != "INFO")) { return "Usage: SHOW [FPS|INFO|NOTIFICATION|CHEEVO]"; }
         if (RenderInfo::get()->isActive()) { return "Info overlay already ON"; }
         RenderInfo::get()->toggle();
         return "Info overlay ON";
     }},
    {.keyword = "HIDE", .execute = [](const std::vector<std::string>& args) -> std::string {
         if (args.empty() || (args[0] != "FPS" && args[0] != "INFO")) { return "Usage: HIDE [FPS|INFO]"; }
         if (!RenderInfo::get()->isActive()) { return "Info overlay already OFF"; }
         RenderInfo::get()->toggle();
         return "Info overlay OFF";
     }},

    // ROOM <num>|NEXT|PREV — Cambia a la habitación indicada (1-60); solo en escena GAME
    {.keyword = "ROOM", .execute = [](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: ROOM <1-60>|NEXT|PREV"; }
         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"; }
         }
         if (num < 1 || num > 60) { return "Room must be between 1 and 60"; }
         char buf[16];
         std::snprintf(buf, sizeof(buf), "%02d.yaml", num);
         if (GameControl::change_room && GameControl::change_room(buf)) {
             return std::string("Room: ") + buf;
         }
         return std::string("Room not found: ") + buf;
     }},

#endif

    // CHEAT <subcomando> — Trucos de juego; solo en escena GAME; no aparece en ayuda en builds Release
    {.keyword = "CHEAT", .execute = [](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: 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;
             const std::vector<std::string> REST(args.begin() + 2, args.end());
             if (REST.empty()) {
                 cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED;
             } else if (REST[0] == "ON") {
                 if (cheat == State::ENABLED) { return "Infinite lives already ON"; }
                 cheat = State::ENABLED;
             } else if (REST[0] == "OFF") {
                 if (cheat == State::DISABLED) { return "Infinite lives already OFF"; }
                 cheat = State::DISABLED;
             } else {
                 return "Usage: CHEAT INFINITE LIVES [ON|OFF]";
             }
             if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); }
             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]";
             }
             if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); }
             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]";
     }},

    // SET PLAYER SKIN <1|2>             — Cambia la skin del jugador (disponible en todos los builds, GAME)
    // SET INITIAL [ROOM|POS]            — Guarda habitación/posición actual como inicio (solo _DEBUG, GAME)
    // SET INITIAL SCENE [<name>]        — Guarda escena como escena inicial de debug (solo _DEBUG)
    // SET ITEMS <0-200>                 — Fija el contador de items recogidos (solo _DEBUG, GAME)
    {.keyword = "SET", .execute = [](const std::vector<std::string>& args) -> std::string {
         if (args.size() >= 3 && args[0] == "PLAYER" && args[1] == "SKIN") {
             if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
             int num = 0;
             try {
                 num = std::stoi(args[2]);
             } catch (...) {}
             if (num < 1 || num > 2) { return "Usage: SET PLAYER SKIN <1|2>"; }
             if (!GameControl::change_player_skin) { return "Game not initialized"; }
             GameControl::change_player_skin(num);
             return "Player skin: " + std::to_string(num);
         }
#ifdef _DEBUG
         // SET INITIAL SCENE [<nombre>] — disponible desde cualquier escena
         if (args.size() >= 2 && args[0] == "INITIAL" && 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] == "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 {
                     return "Unknown scene: " + args[2];
                 }
             }
             Debug::get()->setInitialScene(target);
             Debug::get()->saveToFile();
             return "Initial scene set to: " + name;
         }

         if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }

         // SET ITEMS <0-200> — Fija el contador de items recogidos
         if (args[0] == "ITEMS") {
             if (args.size() < 2) { return "Usage: SET ITEMS <0-200>"; }
             int count = 0;
             try {
                 count = std::stoi(args[1]);
             } catch (...) { return "Usage: SET 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);
         }

         if (args.empty() || args[0] != "INITIAL") { return "Usage: SET INITIAL [ROOM|POS|SCENE] | SET ITEMS <0-200> | SET PLAYER SKIN <1|2>"; }

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

         if (!DO_ROOM && !DO_POS) { return "Usage: SET INITIAL [ROOM|POS|SCENE]"; }
         if (!GameControl::set_initial_room || !GameControl::set_initial_pos) { return "Game not initialized"; }

         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;
#else
         return "Usage: SET PLAYER SKIN <1|2>";
#endif
     }},

    // SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART] — Cambiar o reiniciar escena
    {.keyword = "SCENE", .execute = [](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|ENDING|ENDING2|RESTART]"; }

         // RESTART: reinicia la escena actual (funciona desde cualquier escena)
         if (args[0] == "RESTART") {
             SceneManager::scene_before_restart = SceneManager::current;
             SceneManager::current = SceneManager::Scene::RESTART_CURRENT;
             return "Restarting...";
         }

         // Para el resto: si pedimos la escena que ya está activa → también reiniciar
         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] == "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];
     }},

    // RESTART — Reiniciar desde el principio (equivale a SCENE LOGO)
    {.keyword = "RESTART", .execute = [](const std::vector<std::string>&) -> std::string {
         SceneManager::current = SceneManager::Scene::LOGO;
         return "Restarting...";
     }},

    // KIOSK [ON|OFF PLEASE|PLEASE] — Modo kiosko
    {.keyword = "KIOSK", .execute = [](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]";
     }},

    // AUDIO [ON|OFF|VOL <0-100>] — Audio maestro (estado + volumen)
    {.keyword = "AUDIO", .execute = [](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>] — Volumen e interruptor de música
    {.keyword = "MUSIC", .execute = [](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>] — Volumen e interruptor de efectos de sonido
    {.keyword = "SOUND", .execute = [](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]";
     }},

    // EXIT / QUIT — Cerrar la aplicacion (bloqueado en kiosk)
    {.keyword = "EXIT", .execute = [](const std::vector<std::string>& args) -> std::string {
         if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
             return "Not allowed in kiosk mode";
         }
         SceneManager::current = SceneManager::Scene::QUIT;
         return "Quitting...";
     }},
    {.keyword = "QUIT", .execute = [](const std::vector<std::string>& args) -> std::string {
         if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
             return "Not allowed in kiosk mode";
         }
         SceneManager::current = SceneManager::Scene::QUIT;
         return "Quitting...";
     }},

    // SIZE — Devuelve el tamaño actual de la ventana en píxeles
    {.keyword = "SIZE", .execute = [](const std::vector<std::string>&) -> 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);
     }},

    // HELP / ? — Muestra ayuda en la terminal del sistema
    {.keyword = "HELP", .execute = [](const std::vector<std::string>&) -> std::string {
         printHelp();
         return "Help printed to terminal";
     }},
    {.keyword = "?", .execute = [](const std::vector<std::string>&) -> std::string {
         printHelp();
         return "Help printed to terminal";
     }},
};

// ── Singleton ─────────────────────────────────────────────────────────────────

// [SINGLETON]
Console* Console::console = nullptr;

// [SINGLETON]
void Console::init(const std::string& font_name) {
    Console::console = new Console(font_name);
}

// [SINGLETON]
void Console::destroy() {
    delete Console::console;
    Console::console = nullptr;
}

// [SINGLETON]
auto Console::get() -> Console* {
    return Console::console;
}

// Constructor
Console::Console(const std::string& font_name)
    : text_(Resource::Cache::get()->getText(font_name)) {
    const int TEXT_SIZE = 6;
    const int PADDING_IN_V = TEXT_SIZE / 2;
    height_ = static_cast<float>((TEXT_SIZE * 2) + (PADDING_IN_V * 2));
    y_ = -height_;

    msg_line_ = std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION);

    buildSurface();
}

// Crea la Surface con el aspecto visual de la consola
void Console::buildSurface() {
    const float WIDTH = Options::game.width;

    surface_ = std::make_shared<Surface>(WIDTH, height_);

    // Posición inicial (fuera de pantalla por arriba)
    SDL_FRect sprite_rect = {.x = 0, .y = y_, .w = WIDTH, .h = height_};
    sprite_ = std::make_shared<Sprite>(surface_, sprite_rect);

    // Dibujo inicial del texto
    redrawText();
}

// Redibuja el texto dinámico sobre la surface (fondo + borde + líneas)
void Console::redrawText() {
    const float WIDTH = Options::game.width;
    const int TEXT_SIZE = 6;
    const int PADDING_IN_H = TEXT_SIZE;
    const int PADDING_IN_V = TEXT_SIZE / 2;

    auto previous_renderer = Screen::get()->getRendererSurface();
    Screen::get()->setRendererSurface(surface_);

    // Fondo y borde
    surface_->clear(BG_COLOR);
    SDL_FRect rect = {.x = 0, .y = 0, .w = WIDTH, .h = height_};
    surface_->drawRectBorder(&rect, BORDER_COLOR);

    // Línea 1: mensajes
    text_->writeColored(PADDING_IN_H, PADDING_IN_V, msg_line_, MSG_COLOR);

    // Línea 2: prompt + input + cursor
    const bool SHOW_CURSOR = cursor_visible_ && (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS);
    const std::string INPUT_STR = "> " + input_line_ + (SHOW_CURSOR ? "_" : "");
    text_->writeColored(PADDING_IN_H, PADDING_IN_V + TEXT_SIZE, INPUT_STR, BORDER_COLOR);

    Screen::get()->setRendererSurface(previous_renderer);
}

// Actualiza la animación de la consola
void Console::update(float delta_time) {
    if (status_ == Status::HIDDEN) {
        return;
    }

    // Parpadeo del cursor (solo cuando activa)
    if (status_ == Status::ACTIVE) {
        cursor_timer_ += delta_time;
        const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME;
        if (cursor_timer_ >= THRESHOLD) {
            cursor_timer_ = 0.0F;
            cursor_visible_ = !cursor_visible_;
        }
    }

    // Redibujar texto cada frame
    redrawText();

    switch (status_) {
        case Status::RISING: {
            y_ += SLIDE_SPEED * delta_time;
            if (y_ >= 0.0F) {
                y_ = 0.0F;
                status_ = Status::ACTIVE;
            }
            break;
        }
        case Status::VANISHING: {
            y_ -= SLIDE_SPEED * delta_time;
            if (y_ <= -height_) {
                y_ = -height_;
                status_ = Status::HIDDEN;
            }
            break;
        }
        default:
            break;
    }

    SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_};
    sprite_->setPosition(rect);
}

// Renderiza la consola
void Console::render() {
    if (status_ == Status::HIDDEN) {
        return;
    }
    sprite_->render();
}

// Activa o desactiva la consola
void Console::toggle() {
    switch (status_) {
        case Status::HIDDEN:
            status_ = Status::RISING;
            input_line_.clear();
            cursor_timer_ = 0.0F;
            cursor_visible_ = true;
            SDL_StartTextInput(SDL_GetKeyboardFocus());
            if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(static_cast<int>(height_)); }
            if (on_toggle) { on_toggle(true); }
            break;
        case Status::ACTIVE:
            status_ = Status::VANISHING;
            msg_line_ = std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION);
            history_index_ = -1;
            saved_input_.clear();
            SDL_StopTextInput(SDL_GetKeyboardFocus());
            if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(static_cast<int>(height_)); }
            if (on_toggle) { on_toggle(false); }
            break;
        default:
            // Durante RISING o VANISHING no se hace nada
            break;
    }
}

// Procesa el evento SDL: entrada de texto, Backspace, Enter
void Console::handleEvent(const SDL_Event& event) {
    if (status_ != Status::ACTIVE) { return; }

    if (event.type == SDL_EVENT_TEXT_INPUT) {
        if (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS) {
            input_line_ += event.text.text;
        }
        return;
    }

    if (event.type == SDL_EVENT_KEY_DOWN) {
        switch (event.key.scancode) {
            case SDL_SCANCODE_BACKSPACE:
                if (!input_line_.empty()) { input_line_.pop_back(); }
                break;
            case SDL_SCANCODE_RETURN:
            case SDL_SCANCODE_KP_ENTER:
                processCommand();
                break;
            case SDL_SCANCODE_UP:
                // Navegar hacia atrás en el historial
                if (history_index_ < static_cast<int>(history_.size()) - 1) {
                    if (history_index_ == -1) { saved_input_ = input_line_; }
                    ++history_index_;
                    input_line_ = history_[static_cast<size_t>(history_index_)];
                }
                break;
            case SDL_SCANCODE_DOWN:
                // Navegar hacia el presente en el historial
                if (history_index_ >= 0) {
                    --history_index_;
                    input_line_ = (history_index_ == -1)
                        ? saved_input_
                        : history_[static_cast<size_t>(history_index_)];
                }
                break;
            default:
                break;
        }
    }
}

// Ejecuta el comando introducido y reinicia la línea de input
void Console::processCommand() {
    if (!input_line_.empty()) {
        // Añadir al historial (sin duplicados consecutivos)
        if (history_.empty() || history_.front() != input_line_) {
            history_.push_front(input_line_);
            if (static_cast<int>(history_.size()) > MAX_HISTORY_SIZE) {
                history_.pop_back();
            }
        }

        const auto TOKENS = parseTokens(input_line_);
        if (!TOKENS.empty()) {
            const std::string& cmd = TOKENS[0];
            const std::vector<std::string> ARGS(TOKENS.begin() + 1, TOKENS.end());
            bool found = false;
            for (const auto& command : COMMANDS) {
                if (command.keyword == cmd) {
                    msg_line_ = command.execute(ARGS);
                    found = true;
                    break;
                }
            }
            if (!found) {
                msg_line_ = "Unknown: " + cmd;
            }
            if (static_cast<int>(msg_line_.size()) > MAX_LINE_CHARS) {
                msg_line_.resize(MAX_LINE_CHARS);
            }
        }
    }
    input_line_.clear();
    history_index_ = -1;
    saved_input_.clear();
    cursor_timer_ = 0.0F;
    cursor_visible_ = true;
}

// Indica si la consola está activa (visible o en animación)
auto Console::isActive() -> bool {
    return status_ != Status::HIDDEN;
}

// Devuelve los píxeles visibles de la consola (sincronizado con la animación)
auto Console::getVisibleHeight() -> int {
    if (status_ == Status::HIDDEN) { return 0; }
    return static_cast<int>(y_ + height_);
}
