#ifdef _DEBUG

#include "game/editor/map_editor.hpp"

#include <SDL3/SDL.h>

#include <algorithm>
#include <cmath>         // Para std::round
#include <cstdio>        // Para std::remove (borrar fichero)
#include <filesystem>    // Para directory_iterator (getAnimationCompletions)
#include <fstream>       // Para ifstream, ofstream
#include <iostream>      // Para cout
#include <set>           // Para set
#include <system_error>  // Para std::error_code

#include "core/input/key_config.hpp"           // Para KeyConfig
#include "core/input/mouse.hpp"                // Para Mouse
#include "core/rendering/render_info.hpp"      // Para RenderInfo
#include "core/rendering/screen.hpp"           // Para Screen
#include "core/rendering/surface.hpp"          // Para Surface
#include "core/resources/resource_cache.hpp"   // Para Resource::Cache
#include "core/resources/resource_list.hpp"    // Para Resource::List
#include "core/resources/resource_types.hpp"   // Para RoomResource
#include "core/system/debug.hpp"               // Para Debug (persistencia de render_info)
#include "external/fkyaml_node.hpp"            // Para fkyaml::node (loadSettings)
#include "game/editor/editor_statusbar.hpp"    // Para EditorStatusBar
#include "game/entities/player.hpp"            // Para Player
#include "game/game_control.hpp"               // Para GameControl
#include "game/gameplay/door_manager.hpp"      // Para DoorManager
#include "game/gameplay/enemy_manager.hpp"     // Para EnemyManager
#include "game/gameplay/item_manager.hpp"      // Para ItemManager
#include "game/gameplay/key_manager.hpp"       // Para KeyManager
#include "game/gameplay/platform_manager.hpp"  // Para PlatformManager
#include "game/gameplay/room.hpp"              // Para Room
#include "game/gameplay/room_format.hpp"       // Para RoomFormat
#include "game/gameplay/zone.hpp"              // Para Zone::Data
#include "game/gameplay/zone_manager.hpp"      // Para ZoneManager
#include "game/options.hpp"                    // Para Options
#include "game/ui/console.hpp"                 // Para Console
#include "utils/defines.hpp"                   // Para Tile::SIZE, PlayArea
#include "utils/utils.hpp"

// Singleton
MapEditor* MapEditor::instance_ = nullptr;

void MapEditor::init() {
    instance_ = new MapEditor();
}

void MapEditor::destroy() {
    delete instance_;
    instance_ = nullptr;
}

auto MapEditor::get() -> MapEditor* {
    return instance_;
}

// Constructor
MapEditor::MapEditor() {
    loadSettings();
}

// Destructor
MapEditor::~MapEditor() = default;

// Carga las opciones del editor desde editor.yaml
void MapEditor::loadSettings() {
    std::string path = Resource::List::get()->get("editor.yaml");
    if (path.empty()) { return; }

    std::ifstream file(path);
    if (!file.is_open()) { return; }

    std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    file.close();

    try {
        auto yaml = fkyaml::node::deserialize(content);
        if (yaml.contains("grid")) { settings_.grid = yaml["grid"].get_value<bool>(); }
        if (yaml.contains("minimap_bg")) {
            if (yaml["minimap_bg"].is_integer()) {
                settings_.minimap_bg = static_cast<Uint8>(yaml["minimap_bg"].get_value<int>());
            } else if (yaml["minimap_bg"].is_string()) {
                settings_.minimap_bg = static_cast<Uint8>(safeStoi(yaml["minimap_bg"].get_value<std::string>(), 2));
            }
        }
        if (yaml.contains("minimap_conn")) {
            if (yaml["minimap_conn"].is_integer()) {
                settings_.minimap_conn = static_cast<Uint8>(yaml["minimap_conn"].get_value<int>());
            } else if (yaml["minimap_conn"].is_string()) {
                settings_.minimap_conn = static_cast<Uint8>(safeStoi(yaml["minimap_conn"].get_value<std::string>(), 14));
            }
        }
    } catch (...) {
        // @INTENTIONAL: fichero corrupto o vacío → usar defaults
    }
}

// Guarda las opciones del editor a editor.yaml
void MapEditor::saveSettings() const {
    std::string path = Resource::List::get()->get("editor.yaml");
    if (path.empty()) { return; }

    std::ofstream file(path);
    if (!file.is_open()) { return; }

    file << "# Map Editor Settings\n";
    file << "grid: " << (settings_.grid ? "true" : "false") << "\n";
    file << "minimap_bg: " << static_cast<int>(settings_.minimap_bg) << "\n";
    file << "minimap_conn: " << static_cast<int>(settings_.minimap_conn) << "\n";
    file.close();
}

// Muestra/oculta render info (persistente en debug.yaml, fuente de verdad global)
auto MapEditor::showInfo(bool show) -> std::string {
    if (RenderInfo::get()->isActive() != show) {
        RenderInfo::get()->toggle();
    }
    if (Debug::get() != nullptr) { Debug::get()->setRenderInfoEnabled(show); }
    return show ? "Info ON" : "Info OFF";
}

// Muestra/oculta grid (persistente)
auto MapEditor::showGrid(bool show) -> std::string {
    settings_.grid = show;
    saveSettings();
    return show ? "Grid ON" : "Grid OFF";
}

auto MapEditor::setEditingCollision(bool collision) -> std::string {
    editing_collision_ = collision;
    brush_.clear();                                    // Resetear brush al cambiar de modo
    if (editing_collision_) { auto_collision_ = -1; }  // Desactivar auto-collision en modo colisión puro
    return editing_collision_ ? "Editing: collision" : "Editing: draw";
}

void MapEditor::toggleMiniMap() {
    if (!mini_map_) {
        mini_map_ = std::make_unique<MiniMap>(settings_.minimap_bg, settings_.minimap_conn);
        mini_map_->on_navigate = [this](const std::string& room_name) {
            mini_map_visible_ = false;
            reenter_ = true;
            if (GameControl::exit_editor) { GameControl::exit_editor(); }
            if (GameControl::change_room && GameControl::change_room(room_name)) {
                if (GameControl::enter_editor) { GameControl::enter_editor(); }
            }
        };
    }
    mini_map_visible_ = !mini_map_visible_;
    if (mini_map_visible_) {
        // Reconstruir el minimapa (pueden haber cambiado rooms, conexiones, tiles)
        mini_map_ = std::make_unique<MiniMap>(settings_.minimap_bg, settings_.minimap_conn);
        mini_map_->on_navigate = [this](const std::string& room_name) {
            mini_map_visible_ = false;
            reenter_ = true;
            if (GameControl::exit_editor) { GameControl::exit_editor(); }
            if (GameControl::change_room && GameControl::change_room(room_name)) {
                if (GameControl::enter_editor) { GameControl::enter_editor(); }
            }
        };
        mini_map_->centerOnRoom(room_path_);
    }
}

auto MapEditor::setMiniMapBg(const std::string& color) -> std::string {
    settings_.minimap_bg = static_cast<Uint8>(safeStoi(color, 2));
    saveSettings();
    if (mini_map_) {
        mini_map_->rebuild(settings_.minimap_bg, settings_.minimap_conn);
    }
    return "minimap bg: " + std::to_string(settings_.minimap_bg);
}

auto MapEditor::setMiniMapConn(const std::string& color) -> std::string {
    settings_.minimap_conn = static_cast<Uint8>(safeStoi(color, 14));
    saveSettings();
    if (mini_map_) {
        mini_map_->rebuild(settings_.minimap_bg, settings_.minimap_conn);
    }
    return "minimap conn: " + std::to_string(settings_.minimap_conn);
}

// Entra en modo editor
void MapEditor::enter(std::shared_ptr<Room> room, std::shared_ptr<Player> player, const std::string& room_path, std::shared_ptr<Scoreboard::Data> scoreboard_data) {
    if (active_) { return; }

    room_ = std::move(room);
    player_ = std::move(player);
    room_path_ = room_path;
    scoreboard_data_ = std::move(scoreboard_data);

    // Cargar una copia de los datos de la habitación (para boundaries y edición)
    auto room_data_ptr = Resource::Cache::get()->getRoom(room_path);
    if (room_data_ptr) {
        room_data_ = *room_data_ptr;
    }

    // Obtener la ruta completa al fichero del editor (para autosave en disco)
    file_path_ = Resource::List::get()->get(room_path_);

    bool is_reenter = reenter_;
    if (!reenter_) {
        // Solo guardar estado previo en el primer enter (no en re-enter tras cambio de room)
        invincible_before_editor_ = Options::cheats.invincible;
    }
    reenter_ = false;

    // Forzar invencibilidad
    Options::cheats.invincible = Options::Cheat::State::ENABLED;

    // Activar scope de la consola para el editor
    Console::get()->setScope("editor");

    // Resetear enemigos a su posición inicial (pueden haberse movido durante el gameplay)
    room_->resetEnemyPositions(room_data_.enemies);

    // Resetear plataformas a su posición inicial
    room_->getPlatformManager()->resetPositions(room_data_.platforms);

    // Crear la barra de estado
    statusbar_ = std::make_unique<EditorStatusBar>(room_->getNumber());

    // Resetear estado (preservar modo de edición en re-enter)
    drag_ = {};
    selection_.clear();
    eyedropper_ = {};
    if (!is_reenter) {
        brush_.clear();
        painting_ = false;
        editing_collision_ = false;
    }
    painting_ = false;  // Siempre dejar de pintar al cambiar de room

    // Asegurar que collision_tile_map tiene el tamaño correcto
    if (room_data_.collision_tile_map.size() != static_cast<size_t>(Map::WIDTH) * static_cast<size_t>(Map::HEIGHT)) {
        room_data_.collision_tile_map.resize(static_cast<size_t>(Map::WIDTH) * static_cast<size_t>(Map::HEIGHT), 0);
    }

    active_ = true;
    std::cout << "MapEditor: ON (room " << room_path_ << ")\n";
}

// Sale del modo editor
void MapEditor::exit() {
    if (!active_) { return; }

    active_ = false;

    if (!reenter_) {
        // Solo restaurar en el exit final (no en cambio de room)
        Options::cheats.invincible = invincible_before_editor_;
    }

    // Restaurar prompt y scope de la consola
    selection_.clear();
    Console::get()->setPrompt("> ");
    Console::get()->setScope("debug");
    drag_ = {};
    statusbar_.reset();
    room_.reset();
    player_.reset();
    scoreboard_data_.reset();

    std::cout << "MapEditor: OFF\n";
}

// Revierte todos los cambios al estado original
auto MapEditor::revert() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (file_path_.empty()) { return "Error: No file path"; }

    // Restaurar room_data_ desde el cache (que mantiene la versión original) y persistir
    auto room_data_ptr = Resource::Cache::get()->getRoom(room_path_);
    if (room_data_ptr) {
        room_data_ = *room_data_ptr;
    }
    RoomFormat::saveYAML(file_path_, room_data_);

    // Rebuild all entities from room_data_
    auto* enemy_mgr = room_->getEnemyManager();
    enemy_mgr->clear();
    for (const auto& e : room_data_.enemies) {
        enemy_mgr->addEnemy(Enemy::create(e));
    }

    auto* item_mgr = room_->getItemManager();
    item_mgr->clear();
    for (const auto& i : room_data_.items) {
        item_mgr->addItem(std::make_shared<Item>(i));
    }

    auto* platform_mgr = room_->getPlatformManager();
    platform_mgr->clear();
    for (const auto& p : room_data_.platforms) {
        platform_mgr->addPlatform(std::make_shared<MovingPlatform>(p));
    }

    // Restaurar el tilemap completo
    for (int i = 0; i < static_cast<int>(room_data_.tile_map.size()); ++i) {
        room_->setTile(i, room_data_.tile_map[i]);
    }

    selection_.clear();
    brush_.clear();
    return "Reverted to original";
}

// Auto-guarda al YAML tras soltar una entidad
void MapEditor::autosave() {
    if (file_path_.empty()) { return; }

    // Sincronizar posiciones de items desde los sprites vivos a room_data_
    auto* item_mgr = room_->getItemManager();
    for (int i = 0; i < item_mgr->getCount() && i < static_cast<int>(room_data_.items.size()); ++i) {
        SDL_FPoint pos = item_mgr->getItem(i)->getPos();
        room_data_.items[i].x = pos.x;
        room_data_.items[i].y = pos.y;
    }

    // Sincronizar posiciones de llaves desde los sprites vivos a room_data_
    // (mismo patrón que items: el sprite es la fuente de verdad durante el drag)
    auto* key_mgr = room_->getKeyManager();
    for (int i = 0; i < key_mgr->getCount() && i < static_cast<int>(room_data_.keys.size()); ++i) {
        SDL_FPoint pos = key_mgr->getKey(i)->getPos();
        room_data_.keys[i].x = pos.x;
        room_data_.keys[i].y = pos.y;
    }

    // Platforms are already synced via resetToInitialPosition during drag commit
    // Doors are already synced via DoorManager::moveDoor in commitEntityDrag

    RoomFormat::saveYAML(file_path_, room_data_);
}

// Actualiza el editor
void MapEditor::update(float delta_time) {
    // Mantener el ratón siempre visible
    SDL_ShowCursor();
    Mouse::last_mouse_move_time = SDL_GetTicks();

    // Actualizar animaciones de enemigos e items (sin mover enemigos)
    room_->updateEditorMode(delta_time);

    // Actualizar posición del ratón
    updateMousePosition();

    // Si estamos arrastrando, actualizar la posición snapped
    if (drag_.target != DragTarget::NONE) {
        updateDrag();
    }

    // Si estamos pintando tiles, estampar el patrón en la posición del ratón
    if (painting_ && !brush_.isEmpty()) {
        stampBrushAt(mouse_tile_x_, mouse_tile_y_);
    }

    // Actualizar la barra de estado
    updateStatusBarInfo();
    if (statusbar_) {
        statusbar_->update(delta_time);
    }
}

// Renderiza el editor
void MapEditor::renderCollisionOverlay() const {
    auto collision_surface = Resource::Cache::get()->getSurface("collision.gif");
    if (!collision_surface) { return; }
    const int TILE_W = Tile::SIZE;
    for (int y = 0; y < Map::HEIGHT; ++y) {
        for (int x = 0; x < Map::WIDTH; ++x) {
            int index = (y * Map::WIDTH) + x;
            if (index >= static_cast<int>(room_data_.collision_tile_map.size())) { continue; }
            int tile = room_data_.collision_tile_map[index];
            if (tile <= 0) { continue; }  // 0 = vacío, no dibujar
            SDL_FRect clip = {
                .x = static_cast<float>(tile * TILE_W),
                .y = 0,
                .w = static_cast<float>(TILE_W),
                .h = static_cast<float>(TILE_W)};
            collision_surface->render(x * TILE_W, y * TILE_W, &clip);
        }
    }
}

void MapEditor::render() {
    // El tilemap ya ha sido renderizado por Game::renderPlaying() antes de llamar aquí

    // Si estamos editando colisiones, superponer el mapa de colisiones
    if (editing_collision_) { renderCollisionOverlay(); }

    // Grid (debajo de todo)
    if (settings_.grid) {
        renderGrid();
    }

    // Renderizar los marcadores de boundaries y líneas de ruta (debajo de los sprites)
    renderEntityBoundaries();

    // Renderizar entidades normales: enemigos (animados en posición inicial), items,
    // plataformas, llaves, puertas, jugador
    room_->renderEnemies();
    room_->renderItems();
    room_->renderPlatforms();
    room_->renderKeys();
    room_->renderDoors();
    player_->render();

    // Renderizar highlight de selección (encima de los sprites)
    renderSelectionHighlight();

    // Preview del brush bajo el cursor o rect del eyedropper en progreso
    // (solo cuando no hay overlays a pantalla completa abiertos)
    if (!tile_picker_.isOpen() && !mini_map_visible_) {
        if (eyedropper_.active) {
            renderEyedropperRect();
        } else {
            renderBrushPreview();
        }
    }

    // Tile picker o mini mapa (encima de todo en el play area)
    if (tile_picker_.isOpen()) {
        tile_picker_.render();
    } else if (mini_map_visible_ && mini_map_) {
        mini_map_->render(room_path_);
    }

    // Renderizar barra de estado del editor (reemplaza al scoreboard)
    if (statusbar_) {
        statusbar_->render();
    }
}

// Maneja eventos del editor
void MapEditor::handleEvent(const SDL_Event& event) {  // NOLINT(readability-function-cognitive-complexity)
    // Si el tile picker está abierto, los eventos van a él.
    // Excepción: la T lo cierra como toggle (sin tocar el brush).
    const auto* kc = KeyConfig::get();

    if (tile_picker_.isOpen()) {
        if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "tile_picker") && static_cast<int>(event.key.repeat) == 0) {
            tile_picker_.close();
            return;
        }
        tile_picker_.handleEvent(event);
        return;
    }

    // Si el mini mapa está visible, delegar eventos (ESC o M para cerrar)
    if (mini_map_visible_ && mini_map_) {
        if (event.type == SDL_EVENT_KEY_DOWN &&
            (event.key.key == kc->key("EDITOR", "cancel") || event.key.key == kc->key("EDITOR", "minimap")) &&
            static_cast<int>(event.key.repeat) == 0) {
            mini_map_visible_ = false;
            return;
        }
        mini_map_->handleEvent(event, room_path_);
        return;
    }

    // ESC: cancelar eyedropper en progreso (sin tocar el brush previo)
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "cancel") && eyedropper_.active) {
        eyedropper_.active = false;
        return;
    }

    // ESC: desactivar brush
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "cancel") && !brush_.isEmpty()) {
        brush_.clear();
        return;
    }

    // E: toggle borrador (alterna entre brush vacío y brush 1x1 ERASE)
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "eraser") && static_cast<int>(event.key.repeat) == 0) {
        if (brush_.width == 1 && brush_.height == 1 && !brush_.tiles.empty() && brush_.tiles[0] == BrushPattern::ERASE) {
            brush_.clear();
        } else {
            brush_.setSingle(BrushPattern::ERASE);
        }
        return;
    }

    // M: toggle mini mapa
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "minimap") && static_cast<int>(event.key.repeat) == 0) {
        toggleMiniMap();
        return;
    }

    // 8: alternar entre draw y collision
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "collision") && static_cast<int>(event.key.repeat) == 0) {
        setEditingCollision(!editing_collision_);
        return;
    }

    // C: ciclar auto-collision (solo en modo draw)
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "auto_collision") && static_cast<int>(event.key.repeat) == 0 && !editing_collision_) {
        if (auto_collision_ == -1) {
            auto_collision_ = 1;  // WALL
        } else if (auto_collision_ == 1) {
            auto_collision_ = 2;  // PASSABLE
        } else {
            auto_collision_ = -1;  // desactivado
        }
        return;
    }

    // Cursores: navegar a habitación adyacente
    if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
        std::string direction;
        const auto NAV_KEY = event.key.key;
        if (NAV_KEY == kc->key("EDITOR", "nav_up")) {
            direction = "UP";
        } else if (NAV_KEY == kc->key("EDITOR", "nav_down")) {
            direction = "DOWN";
        } else if (NAV_KEY == kc->key("EDITOR", "nav_left")) {
            direction = "LEFT";
        } else if (NAV_KEY == kc->key("EDITOR", "nav_right")) {
            direction = "RIGHT";
        }
        if (!direction.empty() && GameControl::get_adjacent_room) {
            std::string adjacent = GameControl::get_adjacent_room(direction);
            if (!adjacent.empty() && adjacent != "0") {
                autosave();
                reenter_ = true;
                if (GameControl::exit_editor) { GameControl::exit_editor(); }
                if (GameControl::change_room && GameControl::change_room(adjacent)) {
                    if (GameControl::enter_editor) { GameControl::enter_editor(); }
                }
                return;
            }
        }
    }

    // T: abrir TilePicker (el cierre con T también se gestiona arriba, antes de delegar al picker)
    if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == kc->key("EDITOR", "tile_picker") && static_cast<int>(event.key.repeat) == 0) {
        // Deseleccionar entidades
        selection_.clear();

        const std::string TILESET_NAME = editing_collision_ ? "collision.gif" : room_->getTileSetFile();
        int tile_index = (mouse_tile_y_ * 32) + mouse_tile_x_;
        int current = 0;
        if (editing_collision_) {
            current = (tile_index >= 0 && tile_index < static_cast<int>(room_data_.collision_tile_map.size()))
                ? room_data_.collision_tile_map[tile_index]
                : 0;
        } else {
            current = (tile_index >= 0 && tile_index < static_cast<int>(room_data_.tile_map.size()))
                ? room_data_.tile_map[tile_index]
                : -1;
        }

        tile_picker_.on_select = [this, TILESET_NAME](int col, int row, int width, int height) {
            brush_ = buildPatternFromTileset(TILESET_NAME, col, row, width, height);
        };
        tile_picker_.open(TILESET_NAME, current, 0, -1, -1, 0, 1, true);
        return;
    }

    // Click derecho: eyedropper (sample del nivel). Drag = rect, click = single tile.
    if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_RIGHT) {
        // Cancelar painting en curso si lo hubiese
        if (painting_) {
            painting_ = false;
            autosave();
        }
        selection_.clear();
        eyedropper_.active = true;
        eyedropper_.start_tile_x = mouse_tile_x_;
        eyedropper_.start_tile_y = mouse_tile_y_;
        return;
    }
    if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_RIGHT && eyedropper_.active) {
        commitEyedropper();
        eyedropper_.active = false;
        return;
    }

    // Click izquierdo
    if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
        // Si hay brush activo y no hacemos hit en ninguna entidad → pintar
        if (!brush_.isEmpty()) {
            // Comprobar si hay hit en alguna entidad primero
            bool hit_entity = false;
            SDL_FRect player_rect = player_->getRect();
            if (pointInRect(mouse_game_x_, mouse_game_y_, player_rect)) { hit_entity = true; }

            // Check all entity types
            for (auto type : {EntityType::ENEMY, EntityType::PLATFORM, EntityType::ITEM}) {
                for (int i = 0; i < entityCount(type) && !hit_entity; ++i) {
                    if (pointInRect(mouse_game_x_, mouse_game_y_, entityRect(type, i))) { hit_entity = true; }
                }
            }

            if (!hit_entity) {
                // Estampar el patrón y entrar en modo painting
                painting_ = true;
                stampBrushAt(mouse_tile_x_, mouse_tile_y_);
                return;
            }
        }
        handleMouseDown(mouse_game_x_, mouse_game_y_);
    } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
        if (painting_) {
            painting_ = false;
            autosave();
        } else {
            handleMouseUp();
        }
    }
}

// Procesa click del ratón: hit test + inicio de drag
void MapEditor::handleMouseDown(float game_x, float game_y) {
    auto start_drag = [&](DragTarget target, EntityType etype, int index, float entity_x, float entity_y) {
        drag_.target = target;
        drag_.entity_type = etype;
        drag_.index = index;
        drag_.offset_x = game_x - entity_x;
        drag_.offset_y = game_y - entity_y;
        drag_.snap_x = entity_x;
        drag_.snap_y = entity_y;
        drag_.moved = false;
    };

    // 1. Player hit test
    SDL_FRect player_rect = player_->getRect();
    if (pointInRect(game_x, game_y, player_rect)) {
        start_drag(DragTarget::PLAYER, EntityType::NONE, -1, player_rect.x, player_rect.y);
        return;
    }

    // 2. Hit test on entity initials. DOOR antes que KEY/ITEM porque ocupa más
    // espacio (1×4 tiles); ENEMY/PLATFORM van primero porque pueden estar
    // dibujados encima de items pequeños.
    for (auto type : {EntityType::ENEMY, EntityType::PLATFORM, EntityType::DOOR, EntityType::KEY, EntityType::ITEM}) {
        for (int i = 0; i < entityCount(type); ++i) {
            SDL_FRect rect = entityRect(type, i);
            if (pointInRect(game_x, game_y, rect)) {
                start_drag(DragTarget::ENTITY_INITIAL, type, i, rect.x, rect.y);
                return;
            }
        }
    }

    // 3. Hit test on boundaries (enemies only)
    for (auto type : {EntityType::ENEMY}) {
        for (int i = 0; i < entityDataCount(type); ++i) {
            auto bd = entityBoundaries(type, i);
            constexpr auto SZ = static_cast<float>(Tile::SIZE);
            SDL_FRect b1_rect = {static_cast<float>(bd.x1), static_cast<float>(bd.y1), SZ, SZ};
            if (pointInRect(game_x, game_y, b1_rect)) {
                start_drag(DragTarget::ENTITY_BOUND1, type, i, b1_rect.x, b1_rect.y);
                return;
            }
            SDL_FRect b2_rect = {static_cast<float>(bd.x2), static_cast<float>(bd.y2), SZ, SZ};
            if (pointInRect(game_x, game_y, b2_rect)) {
                start_drag(DragTarget::ENTITY_BOUND2, type, i, b2_rect.x, b2_rect.y);
                return;
            }
        }
    }

    // 4. Background click: deselect
    selection_.clear();
}

// Procesa soltar el ratón: commit del drag
void MapEditor::handleMouseUp() {
    if (drag_.target == DragTarget::NONE) { return; }

    // Si no se movió: fue un click → seleccionar/deseleccionar
    if (!drag_.moved) {
        if (drag_.target == DragTarget::ENTITY_INITIAL) {
            if (selection_.is(drag_.entity_type) && selection_.index == drag_.index) {
                selection_.clear();  // deselect
            } else {
                selection_ = {.type = drag_.entity_type, .index = drag_.index};  // select
            }
        } else {
            selection_.clear();
        }
        drag_ = {};
        return;
    }

    // Hubo movimiento: commit del drag
    bool changed = false;

    switch (drag_.target) {
        case DragTarget::PLAYER:
            player_->setDebugPosition(drag_.snap_x, drag_.snap_y);
            player_->finalizeDebugTeleport();
            break;

        case DragTarget::ENTITY_INITIAL:
        case DragTarget::ENTITY_BOUND1:
        case DragTarget::ENTITY_BOUND2:
            changed = commitEntityDrag();
            break;

        case DragTarget::NONE:
            break;
    }

    if (changed) { autosave(); }
    drag_ = {};
}

// Commit de un drag de entidad (initial, bound1, bound2) para cualquier EntityType.
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con una rama por tipo; refactor a visitor requiere cambio de diseño.
auto MapEditor::commitEntityDrag() -> bool {
    const int IDX = drag_.index;
    const int SNAP_X = static_cast<int>(drag_.snap_x);
    const int SNAP_Y = static_cast<int>(drag_.snap_y);

    switch (drag_.target) {
        case DragTarget::ENTITY_INITIAL:
            switch (drag_.entity_type) {
                case EntityType::ENEMY:
                    if (IDX >= 0 && IDX < static_cast<int>(room_data_.enemies.size())) {
                        room_data_.enemies[IDX].x = drag_.snap_x;
                        room_data_.enemies[IDX].y = drag_.snap_y;
                        room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]);
                        selection_ = {.type = EntityType::ENEMY, .index = IDX};
                        return true;
                    }
                    break;
                case EntityType::ITEM:
                    if (IDX >= 0 && IDX < room_->getItemManager()->getCount()) {
                        room_->getItemManager()->getItem(IDX)->setPosition(drag_.snap_x, drag_.snap_y);
                        selection_ = {.type = EntityType::ITEM, .index = IDX};
                        return true;
                    }
                    break;
                case EntityType::PLATFORM:
                    if (IDX >= 0 && IDX < static_cast<int>(room_data_.platforms.size())) {
                        auto& plat = room_data_.platforms[IDX];
                        if (!plat.path.empty()) {
                            float dx = drag_.snap_x - plat.path[0].x;
                            float dy = drag_.snap_y - plat.path[0].y;
                            for (auto& wp : plat.path) {
                                wp.x += dx;
                                wp.y += dy;
                            }
                        }
                        room_->getPlatformManager()->getPlatform(IDX)->resetToInitialPosition(plat);
                        selection_ = {.type = EntityType::PLATFORM, .index = IDX};
                        return true;
                    }
                    break;
                case EntityType::KEY:
                    if (IDX >= 0 && IDX < static_cast<int>(room_data_.keys.size())) {
                        // El sprite ya está en su posición visual final desde moveEntityVisual.
                        // Solo hay que actualizar room_data_; el autosave hará el sync inverso
                        // sprite→data igual que con items.
                        room_data_.keys[IDX].x = drag_.snap_x;
                        room_data_.keys[IDX].y = drag_.snap_y;
                        selection_ = {.type = EntityType::KEY, .index = IDX};
                        return true;
                    }
                    break;
                case EntityType::DOOR:
                    if (IDX >= 0 && IDX < static_cast<int>(room_data_.doors.size())) {
                        // Con el SolidActorManager, moveDoor solo reposiciona el AABB
                        // del Door (no hay tiles que sincronizar). moveEntityVisual ya
                        // movió el sprite durante el drag; moveDoor hace el ajuste final.
                        room_data_.doors[IDX].x = drag_.snap_x;
                        room_data_.doors[IDX].y = drag_.snap_y;
                        room_->getDoorManager()->moveDoor(IDX, drag_.snap_x, drag_.snap_y);
                        selection_ = {.type = EntityType::DOOR, .index = IDX};
                        return true;
                    }
                    break;
                default:
                    break;
            }
            break;

        case DragTarget::ENTITY_BOUND1:
            switch (drag_.entity_type) {
                case EntityType::ENEMY:
                    if (IDX >= 0 && IDX < static_cast<int>(room_data_.enemies.size())) {
                        room_data_.enemies[IDX].x1 = SNAP_X;
                        room_data_.enemies[IDX].y1 = SNAP_Y;
                        room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]);
                        selection_ = {.type = EntityType::ENEMY, .index = IDX};
                        return true;
                    }
                    break;
                default:
                    break;
            }
            break;

        case DragTarget::ENTITY_BOUND2:
            switch (drag_.entity_type) {
                case EntityType::ENEMY:
                    if (IDX >= 0 && IDX < static_cast<int>(room_data_.enemies.size())) {
                        room_data_.enemies[IDX].x2 = SNAP_X;
                        room_data_.enemies[IDX].y2 = SNAP_Y;
                        room_->getEnemyManager()->getEnemy(IDX)->resetToInitialPosition(room_data_.enemies[IDX]);
                        selection_ = {.type = EntityType::ENEMY, .index = IDX};
                        return true;
                    }
                    break;
                default:
                    break;
            }
            break;

        default:
            break;
    }
    return false;
}

// Mueve visualmente la entidad arrastrada a la posición snapped.
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con cases paralelos para cada tipo.
void MapEditor::moveEntityVisual() {
    switch (drag_.target) {
        case DragTarget::ENTITY_INITIAL:
            switch (drag_.entity_type) {
                case EntityType::ENEMY:
                    if (drag_.index >= 0 && drag_.index < room_->getEnemyManager()->getCount()) {
                        Enemy::Data temp_data = room_data_.enemies[drag_.index];
                        temp_data.x = drag_.snap_x;
                        temp_data.y = drag_.snap_y;
                        room_->getEnemyManager()->getEnemy(drag_.index)->resetToInitialPosition(temp_data);
                    }
                    break;
                case EntityType::ITEM:
                    if (drag_.index >= 0 && drag_.index < room_->getItemManager()->getCount()) {
                        room_->getItemManager()->getItem(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y);
                    }
                    break;
                case EntityType::PLATFORM:
                    if (drag_.index >= 0 && drag_.index < room_->getPlatformManager()->getCount()) {
                        MovingPlatform::Data temp_data = room_data_.platforms[drag_.index];
                        if (!temp_data.path.empty()) {
                            float dx = drag_.snap_x - temp_data.path[0].x;
                            float dy = drag_.snap_y - temp_data.path[0].y;
                            for (auto& wp : temp_data.path) {
                                wp.x += dx;
                                wp.y += dy;
                            }
                        }
                        room_->getPlatformManager()->getPlatform(drag_.index)->resetToInitialPosition(temp_data);
                    }
                    break;
                case EntityType::KEY:
                    if (drag_.index >= 0 && drag_.index < room_->getKeyManager()->getCount()) {
                        room_->getKeyManager()->getKey(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y);
                    }
                    break;
                case EntityType::DOOR:
                    // Solo movemos el sprite visualmente. Los WALLs del CollisionMap NO
                    // se tocan durante el drag (la puerta arrastrada no debería bloquear
                    // su nueva posición todavía). El bookkeeping completo se hace en
                    // commitEntityDrag al soltar.
                    if (drag_.index >= 0 && drag_.index < room_->getDoorManager()->getCount()) {
                        room_->getDoorManager()->getDoor(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y);
                    }
                    break;
                default:
                    break;
            }
            break;

        case DragTarget::ENTITY_BOUND1:
        case DragTarget::ENTITY_BOUND2:
            // Los boundaries se actualizan visualmente en renderEntityBoundaries() via drag_.snap
        default:
            break;
    }
}

// Actualiza la posición snapped durante el drag
void MapEditor::updateDrag() {
    float raw_x = mouse_game_x_ - drag_.offset_x;
    float raw_y = mouse_game_y_ - drag_.offset_y;

    float new_snap_x = snapToGrid(raw_x);
    float new_snap_y = snapToGrid(raw_y);

    // Detectar si hubo movimiento real (el snap cambió respecto al inicio)
    if (new_snap_x != drag_.snap_x || new_snap_y != drag_.snap_y) {
        drag_.moved = true;
    }

    drag_.snap_x = new_snap_x;
    drag_.snap_y = new_snap_y;

    // Mientras arrastramos, mover la entidad visualmente a la posición snapped
    switch (drag_.target) {
        case DragTarget::PLAYER:
            player_->setDebugPosition(drag_.snap_x, drag_.snap_y);
            break;

        case DragTarget::ENTITY_INITIAL:
        case DragTarget::ENTITY_BOUND1:
        case DragTarget::ENTITY_BOUND2:
            moveEntityVisual();
            break;

        case DragTarget::NONE:
            break;
    }
}

// Dibuja highlight del elemento seleccionado/arrastrado
void MapEditor::renderSelectionHighlight() {
    auto game_surface = Screen::get()->getRendererSurface();
    if (!game_surface) { return; }

    constexpr auto SZ = static_cast<float>(Tile::SIZE);

    // Highlight de la entidad seleccionada (persistente, color bright_green)
    if (!selection_.isNone() && selection_.index < entityCount(selection_.type)) {
        SDL_FRect rect = entityRect(selection_.type, selection_.index);
        SDL_FRect border = {
            .x = rect.x - 1,
            .y = rect.y - 1,
            .w = rect.w + 2,
            .h = rect.h + 2};
        game_surface->drawRectBorder(&border, 9);
    }

    // Highlight del drag activo (temporal, color bright_white)
    if (drag_.target == DragTarget::NONE || !drag_.moved) { return; }

    const Uint8 DRAG_COLOR = 15;
    SDL_FRect highlight_rect{};

    switch (drag_.target) {
        case DragTarget::PLAYER:
            highlight_rect = player_->getRect();
            break;
        case DragTarget::ENTITY_INITIAL:
            if (drag_.index >= 0 && drag_.index < entityCount(drag_.entity_type)) {
                highlight_rect = entityRect(drag_.entity_type, drag_.index);
            }
            break;
        case DragTarget::ENTITY_BOUND1:
        case DragTarget::ENTITY_BOUND2:
            highlight_rect = {.x = drag_.snap_x, .y = drag_.snap_y, .w = SZ, .h = SZ};
            break;
        case DragTarget::NONE:
            return;
    }

    SDL_FRect border = {
        .x = highlight_rect.x - 1,
        .y = highlight_rect.y - 1,
        .w = highlight_rect.w + 2,
        .h = highlight_rect.h + 2};
    game_surface->drawRectBorder(&border, DRAG_COLOR);
}

// Estampa el patrón del brush en la posición indicada (anclaje top-left).
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- nested loops + casos TRANSPARENT/ERASE/tile normal y ramas collision vs normal.
void MapEditor::stampBrushAt(int tile_x, int tile_y) {
    if (brush_.isEmpty()) { return; }
    for (int dy = 0; dy < brush_.height; ++dy) {
        for (int dx = 0; dx < brush_.width; ++dx) {
            int value = brush_.at(dx, dy);
            if (value == BrushPattern::TRANSPARENT) { continue; }  // skip — no tocar destino
            int tx = tile_x + dx;
            int ty = tile_y + dy;
            if (tx < 0 || tx >= Map::WIDTH || ty < 0 || ty >= Map::HEIGHT) { continue; }
            int idx = (ty * Map::WIDTH) + tx;
            // value es >= 0 (tile real) o BrushPattern::ERASE (escribe -1)
            if (editing_collision_) {
                if (idx >= 0 && idx < static_cast<int>(room_data_.collision_tile_map.size())) {
                    if (room_data_.collision_tile_map[idx] != value) {
                        room_data_.collision_tile_map[idx] = value;
                        room_->setCollisionTile(idx, value);
                    }
                }
            } else {
                if (idx >= 0 && idx < static_cast<int>(room_data_.tile_map.size())) {
                    if (room_data_.tile_map[idx] != value) {
                        room_data_.tile_map[idx] = value;
                        room_->setTile(idx, value);
                    }
                    // Auto-collision: pintar también en collision tilemap
                    if (auto_collision_ >= 0) {
                        int col_value = (value == BrushPattern::ERASE) ? 0 : auto_collision_;
                        if (idx < static_cast<int>(room_data_.collision_tile_map.size()) && room_data_.collision_tile_map[idx] != col_value) {
                            room_data_.collision_tile_map[idx] = col_value;
                            room_->setCollisionTile(idx, col_value);
                        }
                    }
                }
            }
        }
    }
}

// Confirma el eyedropper: muestrea el rect del nivel y lo carga en el brush
void MapEditor::commitEyedropper() {
    int x1 = std::clamp(eyedropper_.start_tile_x, 0, Map::WIDTH - 1);
    int y1 = std::clamp(eyedropper_.start_tile_y, 0, Map::HEIGHT - 1);
    int x2 = std::clamp(mouse_tile_x_, 0, Map::WIDTH - 1);
    int y2 = std::clamp(mouse_tile_y_, 0, Map::HEIGHT - 1);
    int xmin = std::min(x1, x2);
    int ymin = std::min(y1, y2);
    int xmax = std::max(x1, x2);
    int ymax = std::max(y1, y2);
    brush_ = sampleBrush(xmin, ymin, xmax, ymax);
}

// Lee del tilemap activo (draw o collision) un rect y devuelve un BrushPattern.
// Las celdas vacías (-1) en el tilemap origen se mapean a BrushPattern::TRANSPARENT
// para que reestampar el brush no borre contenido del destino.
auto MapEditor::sampleBrush(int x1, int y1, int x2, int y2) const -> BrushPattern {
    BrushPattern p;
    p.width = (x2 - x1) + 1;
    p.height = (y2 - y1) + 1;
    p.tiles.reserve(static_cast<size_t>(p.width) * static_cast<size_t>(p.height));
    const auto& src = editing_collision_ ? room_data_.collision_tile_map : room_data_.tile_map;
    for (int y = y1; y <= y2; ++y) {
        for (int x = x1; x <= x2; ++x) {
            int idx = (y * Map::WIDTH) + x;
            int value = (idx >= 0 && idx < static_cast<int>(src.size())) ? src[idx] : -1;
            // -1 en el tilemap = vacío → TRANSPARENT (no borra al estampar)
            p.tiles.push_back((value == -1) ? BrushPattern::TRANSPARENT : value);
        }
    }
    return p;
}

// Construye un BrushPattern leyendo tiles consecutivos de un tileset.
// Usado por el TilePicker cuando se hace selección rectangular.
auto MapEditor::buildPatternFromTileset(const std::string& tileset_name, int col, int row, int width, int height) -> BrushPattern {
    BrushPattern p;
    auto surface = Resource::Cache::get()->getSurface(tileset_name);
    if (!surface || width <= 0 || height <= 0) { return p; }
    int cols = static_cast<int>(surface->getWidth()) / Tile::SIZE;
    if (cols <= 0) { return p; }
    p.width = width;
    p.height = height;
    p.tiles.reserve(static_cast<size_t>(width) * static_cast<size_t>(height));
    for (int dy = 0; dy < height; ++dy) {
        for (int dx = 0; dx < width; ++dx) {
            int tile = ((row + dy) * cols) + (col + dx);
            p.tiles.push_back(tile);
        }
    }
    return p;
}

// Renderiza la previsualización del brush bajo el cursor (snapped al grid)
void MapEditor::renderBrushPreview() {
    if (brush_.isEmpty()) { return; }
    auto game_surface = Screen::get()->getRendererSurface();
    if (!game_surface) { return; }

    const std::string TILESET_NAME = editing_collision_ ? std::string("collision.gif") : room_->getTileSetFile();
    auto tileset = Resource::Cache::get()->getSurface(TILESET_NAME);
    int cols = (tileset) ? (static_cast<int>(tileset->getWidth()) / Tile::SIZE) : 0;

    constexpr auto TS = static_cast<float>(Tile::SIZE);
    for (int dy = 0; dy < brush_.height; ++dy) {
        for (int dx = 0; dx < brush_.width; ++dx) {
            int value = brush_.at(dx, dy);
            if (value == BrushPattern::TRANSPARENT) { continue; }
            int tx = mouse_tile_x_ + dx;
            int ty = mouse_tile_y_ + dy;
            if (tx < 0 || tx >= Map::WIDTH || ty < 0 || ty >= Map::HEIGHT) { continue; }
            float dst_x = static_cast<float>(tx) * TS;
            float dst_y = static_cast<float>(ty) * TS;
            if (value == BrushPattern::ERASE) {
                SDL_FRect erase_cell = {.x = dst_x, .y = dst_y, .w = TS, .h = TS};
                game_surface->fillRect(&erase_cell, room_data_.bg_color);
            } else if (tileset && cols > 0) {
                SDL_FRect src = {
                    .x = static_cast<float>(value % cols) * TS,
                    .y = static_cast<float>(value / cols) * TS,
                    .w = TS,
                    .h = TS};
                SDL_FRect dst = {.x = dst_x, .y = dst_y, .w = TS, .h = TS};
                tileset->render(&src, &dst);
            }
        }
    }

    // Border alrededor del rectángulo completo del brush (blanco)
    float bx = static_cast<float>(mouse_tile_x_) * TS;
    float by = static_cast<float>(mouse_tile_y_) * TS;
    SDL_FRect border = {
        .x = bx - 1.0F,
        .y = by - 1.0F,
        .w = (static_cast<float>(brush_.width) * TS) + 2.0F,
        .h = (static_cast<float>(brush_.height) * TS) + 2.0F};
    game_surface->drawRectBorder(&border, 15);
}

// Renderiza el rectángulo del eyedropper en progreso (cyan brillante)
void MapEditor::renderEyedropperRect() const {
    auto game_surface = Screen::get()->getRendererSurface();
    if (!game_surface) { return; }
    int x1 = std::clamp(eyedropper_.start_tile_x, 0, Map::WIDTH - 1);
    int y1 = std::clamp(eyedropper_.start_tile_y, 0, Map::HEIGHT - 1);
    int x2 = std::clamp(mouse_tile_x_, 0, Map::WIDTH - 1);
    int y2 = std::clamp(mouse_tile_y_, 0, Map::HEIGHT - 1);
    int xmin = std::min(x1, x2);
    int ymin = std::min(y1, y2);
    int xmax = std::max(x1, x2);
    int ymax = std::max(y1, y2);
    constexpr auto TS = static_cast<float>(Tile::SIZE);
    SDL_FRect rect = {
        .x = static_cast<float>(xmin) * TS,
        .y = static_cast<float>(ymin) * TS,
        .w = static_cast<float>((xmax - xmin) + 1) * TS,
        .h = static_cast<float>((ymax - ymin) + 1) * TS};
    constexpr Uint8 EYEDROPPER_COLOR = 21;  // BRIGHT_CYAN
    game_surface->drawRectBorder(&rect, EYEDROPPER_COLOR);
}

// Alinea un valor a la cuadrícula de 8x8
auto MapEditor::snapToGrid(float value) -> float {
    return std::round(value / static_cast<float>(Tile::SIZE)) * static_cast<float>(Tile::SIZE);
}

// Hit test: punto dentro de rectángulo
auto MapEditor::pointInRect(float px, float py, const SDL_FRect& rect) -> bool {
    return px >= rect.x && px < rect.x + rect.w && py >= rect.y && py < rect.y + rect.h;
}

// --- Entity helpers: acceso abstracto a datos de entidad por tipo ---

auto MapEditor::entityCount(EntityType type) const -> int {
    switch (type) {
        case EntityType::ENEMY:
            return room_->getEnemyManager()->getCount();
        case EntityType::ITEM:
            return room_->getItemManager()->getCount();
        case EntityType::PLATFORM:
            return room_->getPlatformManager()->getCount();
        case EntityType::KEY:
            return room_->getKeyManager()->getCount();
        case EntityType::DOOR:
            return room_->getDoorManager()->getCount();
        default:
            return 0;
    }
}

auto MapEditor::entityRect(EntityType type, int index) -> SDL_FRect {
    switch (type) {
        case EntityType::ENEMY:
            return room_->getEnemyManager()->getEnemy(index)->getRect();
        case EntityType::ITEM:
            return room_->getItemManager()->getItem(index)->getCollider();
        case EntityType::PLATFORM:
            return room_->getPlatformManager()->getPlatform(index)->getRect();
        case EntityType::KEY:
            return room_->getKeyManager()->getKey(index)->getCollider();
        case EntityType::DOOR:
            return room_->getDoorManager()->getDoor(index)->getCollider();
        default:
            return {};
    }
}

auto MapEditor::entityHasBoundaries(EntityType type) -> bool {
    return type == EntityType::ENEMY;
}

auto MapEditor::entityBoundaries(EntityType type, int index) const -> BoundaryData {
    switch (type) {
        case EntityType::ENEMY: {
            const auto& e = room_data_.enemies[index];
            return {.x1 = e.x1, .y1 = e.y1, .x2 = e.x2, .y2 = e.y2};
        }
        default:
            return {};
    }
}

auto MapEditor::entityPosition(EntityType type, int index) const -> std::pair<float, float> {
    switch (type) {
        case EntityType::ENEMY:
            return {room_data_.enemies[index].x, room_data_.enemies[index].y};
        case EntityType::ITEM:
            return {room_data_.items[index].x, room_data_.items[index].y};
        case EntityType::PLATFORM: {
            const auto& path = room_data_.platforms[index].path;
            return path.empty() ? std::pair{0.0F, 0.0F} : std::pair{path[0].x, path[0].y};
        }
        case EntityType::KEY:
            return {room_data_.keys[index].x, room_data_.keys[index].y};
        case EntityType::DOOR:
            return {room_data_.doors[index].x, room_data_.doors[index].y};
        default:
            return {0.0F, 0.0F};
    }
}

auto MapEditor::entityDataCount(EntityType type) const -> int {
    switch (type) {
        case EntityType::ENEMY:
            return static_cast<int>(room_data_.enemies.size());
        case EntityType::ITEM:
            return static_cast<int>(room_data_.items.size());
        case EntityType::PLATFORM:
            return static_cast<int>(room_data_.platforms.size());
        case EntityType::KEY:
            return static_cast<int>(room_data_.keys.size());
        case EntityType::DOOR:
            return static_cast<int>(room_data_.doors.size());
        default:
            return 0;
    }
}

auto MapEditor::entityLabel(EntityType type) -> const char* {
    switch (type) {
        case EntityType::ENEMY:
            return "enemy";
        case EntityType::ITEM:
            return "item";
        case EntityType::PLATFORM:
            return "platform";
        case EntityType::KEY:
            return "key";
        case EntityType::DOOR:
            return "door";
        default:
            return "none";
    }
}

// Dibuja marcadores de boundaries y líneas de ruta para enemigos y plataformas.
// NOLINTNEXTLINE(readability-function-cognitive-complexity) -- switch sobre EntityType con ramas de enemigo patrullante vs plataforma con waypoints.
void MapEditor::renderEntityBoundaries() {
    auto game_surface = Screen::get()->getRendererSurface();
    if (!game_surface) { return; }

    // Colores para la entidad seleccionada (brillantes)
    constexpr Uint8 SEL_BOUND1 = 21;  // BRIGHT_CYAN
    constexpr Uint8 SEL_BOUND2 = 25;  // BRIGHT_YELLOW
    constexpr Uint8 SEL_ROUTE = 26;   // BRIGHT_WHITE

    // Colores para entidades no seleccionadas (apagados)
    constexpr Uint8 DIM_BOUND1 = 11;  // CYAN
    constexpr Uint8 DIM_BOUND2 = 13;  // YELLOW
    constexpr Uint8 DIM_ROUTE = 14;   // WHITE (gris medio)

    constexpr float HALF = Tile::SIZE / 2.0F;

    for (int i = 0; i < entityDataCount(EntityType::ENEMY); ++i) {
        auto [pos_x, pos_y] = entityPosition(EntityType::ENEMY, i);
        auto bd = entityBoundaries(EntityType::ENEMY, i);

        bool is_selected = selection_.is(EntityType::ENEMY) && selection_.index == i;

        // Posiciones base (pueden estar siendo arrastradas)
        float init_x = pos_x;
        float init_y = pos_y;
        auto b1_x = static_cast<float>(bd.x1);
        auto b1_y = static_cast<float>(bd.y1);
        auto b2_x = static_cast<float>(bd.x2);
        auto b2_y = static_cast<float>(bd.y2);

        // Si estamos arrastrando una boundary de esta entidad, usar la posición snapped
        if (drag_.entity_type == EntityType::ENEMY && drag_.index == i) {
            if (drag_.target == DragTarget::ENTITY_BOUND1) {
                b1_x = drag_.snap_x;
                b1_y = drag_.snap_y;
            } else if (drag_.target == DragTarget::ENTITY_BOUND2) {
                b2_x = drag_.snap_x;
                b2_y = drag_.snap_y;
            } else if (drag_.target == DragTarget::ENTITY_INITIAL) {
                init_x = drag_.snap_x;
                init_y = drag_.snap_y;
            }
            is_selected = true;  // Arrastrando = siempre iluminado
        }

        Uint8 color_b1 = is_selected ? SEL_BOUND1 : DIM_BOUND1;
        Uint8 color_b2 = is_selected ? SEL_BOUND2 : DIM_BOUND2;
        Uint8 color_route = is_selected ? SEL_ROUTE : DIM_ROUTE;

        // Dibujar líneas de ruta
        game_surface->drawLine(b1_x + HALF, b1_y + HALF, init_x + HALF, init_y + HALF, color_route);
        game_surface->drawLine(init_x + HALF, init_y + HALF, b2_x + HALF, b2_y + HALF, color_route);

        // Marcadores en las boundaries
        renderBoundaryMarker(b1_x, b1_y, color_b1);
        renderBoundaryMarker(b2_x, b2_y, color_b2);
    }

    // Render platform waypoint routes
    for (int i = 0; i < entityDataCount(EntityType::PLATFORM); ++i) {
        const auto& plat = room_data_.platforms[i];
        if (plat.path.size() < 2) { continue; }

        bool is_selected = selection_.is(EntityType::PLATFORM) && selection_.index == i;

        // If dragging this platform, apply the drag offset to all points for visualization
        float drag_dx = 0.0F;
        float drag_dy = 0.0F;
        if (drag_.entity_type == EntityType::PLATFORM && drag_.index == i && drag_.target == DragTarget::ENTITY_INITIAL) {
            if (!plat.path.empty()) {
                drag_dx = drag_.snap_x - plat.path[0].x;
                drag_dy = drag_.snap_y - plat.path[0].y;
            }
            is_selected = true;
        }

        Uint8 color_wp = is_selected ? SEL_BOUND1 : DIM_BOUND1;
        Uint8 color_route = is_selected ? SEL_ROUTE : DIM_ROUTE;

        // Draw route lines between consecutive waypoints
        for (int j = 0; j < static_cast<int>(plat.path.size()) - 1; ++j) {
            float ax = plat.path[j].x + drag_dx + HALF;
            float ay = plat.path[j].y + drag_dy + HALF;
            float bx = plat.path[j + 1].x + drag_dx + HALF;
            float by = plat.path[j + 1].y + drag_dy + HALF;
            game_surface->drawLine(ax, ay, bx, by, color_route);
        }

        // For circular mode, draw line from last to first
        if (plat.loop == LoopMode::CIRCULAR && plat.path.size() > 2) {
            int last = static_cast<int>(plat.path.size()) - 1;
            float ax = plat.path[last].x + drag_dx + HALF;
            float ay = plat.path[last].y + drag_dy + HALF;
            float bx = plat.path[0].x + drag_dx + HALF;
            float by = plat.path[0].y + drag_dy + HALF;
            game_surface->drawLine(ax, ay, bx, by, color_route);
        }

        // Draw waypoint markers
        for (const auto& wp : plat.path) {
            renderBoundaryMarker(wp.x + drag_dx, wp.y + drag_dy, color_wp);
        }
    }
}

// Dibuja un marcador de boundary (rectángulo pequeño) en una posición
void MapEditor::renderBoundaryMarker(float x, float y, Uint8 color) {
    auto game_surface = Screen::get()->getRendererSurface();
    if (!game_surface) { return; }

    SDL_FRect marker = {.x = x, .y = y, .w = static_cast<float>(Tile::SIZE), .h = static_cast<float>(Tile::SIZE)};
    game_surface->drawRectBorder(&marker, color);
}

// Convierte coordenadas de ventana a coordenadas de juego y tile
void MapEditor::updateMousePosition() {
    float mouse_x = 0.0F;
    float mouse_y = 0.0F;
    SDL_GetMouseState(&mouse_x, &mouse_y);

    float render_x = 0.0F;
    float render_y = 0.0F;
    SDL_RenderCoordinatesFromWindow(Screen::get()->getRenderer(), mouse_x, mouse_y, &render_x, &render_y);

    SDL_FRect dst_rect = Screen::get()->getGameSurfaceDstRect();
    mouse_game_x_ = render_x - dst_rect.x;
    mouse_game_y_ = render_y - dst_rect.y;

    // Convertir a coordenadas de tile (clampeadas al área de juego)
    mouse_tile_x_ = static_cast<int>(mouse_game_x_) / Tile::SIZE;
    mouse_tile_y_ = static_cast<int>(mouse_game_y_) / Tile::SIZE;

    // Clampear a los límites del mapa
    mouse_tile_x_ = std::max(mouse_tile_x_, 0);
    if (mouse_tile_x_ >= PlayArea::WIDTH / Tile::SIZE) { mouse_tile_x_ = PlayArea::WIDTH / Tile::SIZE - 1; }
    mouse_tile_y_ = std::max(mouse_tile_y_, 0);
    if (mouse_tile_y_ >= PlayArea::HEIGHT / Tile::SIZE) { mouse_tile_y_ = PlayArea::HEIGHT / Tile::SIZE - 1; }
}

// Actualiza la información de la barra de estado
void MapEditor::updateStatusBarInfo() {  // NOLINT(readability-function-cognitive-complexity)
    if (!statusbar_) { return; }

    statusbar_->setMouseTile(mouse_tile_x_, mouse_tile_y_);

    std::string line2;
    std::string line3;
    std::string line5;

    // Helper para conexiones compactas
    auto conn = [](const std::string& r) -> std::string {
        if (r == "0" || r.empty()) { return "-"; }
        return r.substr(0, r.find('.'));
    };

    // Info de drag activo (línea 5, junto a tile coords)
    if (drag_.target != DragTarget::NONE && drag_.moved) {
        switch (drag_.target) {
            case DragTarget::PLAYER:
                line5 = "dragging: player";
                break;
            case DragTarget::ENTITY_INITIAL:
                line5 = std::string("dragging: ") + entityLabel(drag_.entity_type) + " " + std::to_string(drag_.index);
                break;
            case DragTarget::ENTITY_BOUND1:
                line5 = std::string("dragging: ") + entityLabel(drag_.entity_type)[0] + std::to_string(drag_.index) + " bound1";
                break;
            case DragTarget::ENTITY_BOUND2:
                line5 = std::string("dragging: ") + entityLabel(drag_.entity_type)[0] + std::to_string(drag_.index) + " bound2";
                break;
            case DragTarget::NONE:
                break;
        }
    }

    // Líneas 2-3 según selección
    switch (selection_.type) {
        case EntityType::ENEMY:
            if (selection_.index < static_cast<int>(room_data_.enemies.size())) {
                const auto& e = room_data_.enemies[selection_.index];
                std::string anim = e.animation_path;
                auto dot = anim.rfind('.');
                if (dot != std::string::npos) { anim.resize(dot); }

                line2 = "enemy " + std::to_string(selection_.index) + ": " + anim;
                line3 = "vx:" + std::to_string(static_cast<int>(e.vx)) +
                    " vy:" + std::to_string(static_cast<int>(e.vy));
                if (e.flip) { line3 += " flip"; }
                if (e.mirror) { line3 += " mirror"; }
            }
            break;

        case EntityType::ITEM:
            if (selection_.index < static_cast<int>(room_data_.items.size())) {
                const auto& it = room_data_.items[selection_.index];
                line2 = "item " + std::to_string(selection_.index) + ": tile=" + std::to_string(it.tile) +
                    "  counter=" + std::to_string(it.counter);
                std::string c1_marker = it.color1_overridden ? "*" : "";
                std::string c2_marker = it.color2_overridden ? "*" : "";
                line3 = "col:" + std::to_string(it.color1) + c1_marker + "/" + std::to_string(it.color2) + c2_marker;
            }
            break;

        case EntityType::PLATFORM:
            if (selection_.index < static_cast<int>(room_data_.platforms.size())) {
                const auto& p = room_data_.platforms[selection_.index];
                std::string anim = p.animation_path;
                if (anim.ends_with(".yaml")) { anim.resize(anim.size() - 5); }
                line2 = "platform " + std::to_string(selection_.index) + ": " + anim;
                line3 = "speed:" + std::to_string(static_cast<int>(p.speed)) + " " + (p.loop == LoopMode::CIRCULAR ? "circular" : "pingpong");
                if (p.easing != "linear") { line3 += " " + p.easing; }
            }
            break;

        case EntityType::KEY:
            if (selection_.index < static_cast<int>(room_data_.keys.size())) {
                const auto& k = room_data_.keys[selection_.index];
                std::string anim = k.animation_path;
                auto dot = anim.rfind('.');
                if (dot != std::string::npos) { anim.resize(dot); }
                line2 = "key " + std::to_string(selection_.index) + ": " + anim;
                line3 = "id: " + k.id;
            }
            break;

        case EntityType::DOOR:
            if (selection_.index < static_cast<int>(room_data_.doors.size())) {
                const auto& d = room_data_.doors[selection_.index];
                std::string anim = d.animation_path;
                auto dot = anim.rfind('.');
                if (dot != std::string::npos) { anim.resize(dot); }
                line2 = "door " + std::to_string(selection_.index) + ": " + anim;
                line3 = "id: " + d.id;
            }
            break;

        case EntityType::NONE: {
            // Propiedades de la habitación
            std::string ts_marker = room_data_.tile_set_overridden ? " (ts*)" : "";
            std::string mu_marker = room_data_.music_overridden ? " (mu*)" : "";
            std::string bg_marker = room_data_.bg_color_overridden ? "*" : "";
            line2 = "zone:" + room_data_.zone + ts_marker + mu_marker + " bg:" + std::to_string(room_data_.bg_color) + bg_marker;
            line3 = "u:" + conn(room_data_.upper_room) + " d:" + conn(room_data_.lower_room) +
                " l:" + conn(room_data_.left_room) + " r:" + conn(room_data_.right_room);
            break;
        }
    }

    // Línea 4: brush activo + auto-collision
    std::string line4;
    if (!brush_.isEmpty()) {
        if (brush_.width == 1 && brush_.height == 1) {
            int v = brush_.tiles[0];
            line4 = (v == BrushPattern::ERASE) ? "brush: eraser (e)" : ("brush: tile " + std::to_string(v));
        } else {
            line4 = "brush: " + std::to_string(brush_.width) + "x" + std::to_string(brush_.height);
        }
    }
    if (!editing_collision_ && auto_collision_ >= 0) {
        const char* col_name = (auto_collision_ == 1) ? "wall" : "pass";
        line4 += (line4.empty() ? "" : " ") + std::string("[col: ") + col_name + "]";
    }

    statusbar_->setLine2(line2);
    statusbar_->setLine3(line3);
    statusbar_->setLine4(line4);
    statusbar_->setLine5(line5);

    // Actualizar el prompt de la consola según la selección
    if (selection_.isNone()) {
        Console::get()->setPrompt("room> ");
    } else {
        Console::get()->setPrompt(std::string(entityLabel(selection_.type)) + " " + std::to_string(selection_.index) + "> ");
    }
}

// Devuelve las propiedades válidas de SET según la selección actual
auto MapEditor::getSetCompletions() const -> std::vector<std::string> {
    switch (selection_.type) {
        case EntityType::ENEMY:
            return {"ANIMATION", "VX", "VY", "FLIP", "MIRROR"};
        case EntityType::ITEM:
            return {"TILE", "COUNTER", "COLOR1", "COLOR2"};
        case EntityType::PLATFORM:
            return {"ANIMATION", "SPEED", "LOOP", "EASING"};
        case EntityType::KEY:
        case EntityType::DOOR:
            return {"ID", "ANIMATION"};
        default:
            return {"ZONE", "TILESET", "MUSIC", "BGCOLOR", "UP", "DOWN", "LEFT", "RIGHT"};
    }
}

// Devuelve la lista de animaciones disponibles para la entidad seleccionada,
// escaneando la carpeta correspondiente en data/. Cada tipo de entidad solo
// puede usar animaciones de su propio directorio. Para entidades sin animación
// (Item, Room/none) devuelve lista vacía.
auto MapEditor::getAnimationCompletions() const -> std::vector<std::string> {
    const char* folder = nullptr;
    switch (selection_.type) {
        case EntityType::ENEMY:
            folder = "data/enemies";
            break;
        case EntityType::PLATFORM:
            folder = "data/platforms";
            break;
        case EntityType::KEY:
            folder = "data/keys";
            break;
        case EntityType::DOOR:
            folder = "data/doors";
            break;
        default:
            return {};
    }
    std::vector<std::string> result;
    std::error_code ec;
    for (const auto& entry : std::filesystem::directory_iterator(folder, ec)) {
        if (ec) { break; }
        if (!entry.is_regular_file()) { continue; }
        const auto& path = entry.path();
        if (path.extension() != ".yaml") { continue; }
        result.push_back(toUpper(path.stem().string()));
    }
    std::ranges::sort(result);
    return result;
}

// Modifica una propiedad del enemigo seleccionado
auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedEnemy()) { return "No enemy selected"; }

    auto& enemy = room_data_.enemies[selection_.index];

    if (property == "ANIMATION") {
        std::string anim = toLower(value);
        if (anim.find('.') == std::string::npos) { anim += ".yaml"; }

        // Intentar recrear el enemigo con la nueva animación
        std::string old_anim = enemy.animation_path;
        enemy.animation_path = anim;
        try {
            auto* enemy_mgr = room_->getEnemyManager();
            enemy_mgr->getEnemy(selection_.index) = Enemy::create(enemy);
        } catch (const std::exception& e) {
            enemy.animation_path = old_anim;  // Restaurar si falla
            return std::string("Error: ") + e.what();
        }

        autosave();
        return "animation: " + anim;
    }

    if (property == "COLOR") {
        return "color property removed (legacy)";
    }

    if (property == "VX") {
        try {
            enemy.vx = std::stof(value);
        } catch (...) { return "Invalid value: " + value; }
        enemy.vy = 0.0F;  // No se permiten velocidades en los dos ejes

        room_->getEnemyManager()->getEnemy(selection_.index)->resetToInitialPosition(enemy);
        autosave();
        return "vx: " + std::to_string(static_cast<int>(enemy.vx)) + " vy: 0";
    }

    if (property == "VY") {
        try {
            enemy.vy = std::stof(value);
        } catch (...) { return "Invalid value: " + value; }
        enemy.vx = 0.0F;  // No se permiten velocidades en los dos ejes

        room_->getEnemyManager()->getEnemy(selection_.index)->resetToInitialPosition(enemy);
        autosave();
        return "vy: " + std::to_string(static_cast<int>(enemy.vy)) + " vx: 0";
    }

    if (property == "FLIP") {
        enemy.flip = (value == "ON" || value == "TRUE" || value == "1");
        if (enemy.flip) { enemy.mirror = false; }  // Mutuamente excluyentes

        // Recrear el enemigo (flip/mirror se aplican en el constructor)
        try {
            auto* enemy_mgr = room_->getEnemyManager();
            enemy_mgr->getEnemy(selection_.index) = Enemy::create(enemy);
        } catch (const std::exception& e) {
            return std::string("Error: ") + e.what();
        }

        autosave();
        return std::string("flip: ") + (enemy.flip ? "on" : "off");
    }

    if (property == "MIRROR") {
        enemy.mirror = (value == "ON" || value == "TRUE" || value == "1");
        if (enemy.mirror) { enemy.flip = false; }  // Mutuamente excluyentes

        // Recrear el enemigo (flip/mirror se aplican en el constructor)
        try {
            auto* enemy_mgr = room_->getEnemyManager();
            enemy_mgr->getEnemy(selection_.index) = Enemy::create(enemy);
        } catch (const std::exception& e) {
            return std::string("Error: ") + e.what();
        }

        autosave();
        return std::string("mirror: ") + (enemy.mirror ? "on" : "off");
    }

    return "Unknown property: " + property + " (use: animation, vx, vy, flip, mirror)";
}

// Crea un nuevo enemigo con valores por defecto, centrado en la habitación
auto MapEditor::addEnemy() -> std::string {
    if (!active_) { return "Editor not active"; }

    constexpr float CENTER_X = PlayArea::CENTER_X;
    constexpr float CENTER_Y = PlayArea::CENTER_Y;
    constexpr float ROUTE_HALF = 2.0F * Tile::SIZE;  // 2 tiles a cada lado (5 tiles total con el centro)

    Enemy::Data new_enemy;
    new_enemy.animation_path = "spider.yaml";
    new_enemy.x = CENTER_X;
    new_enemy.y = CENTER_Y;
    new_enemy.vx = 24.0F;
    new_enemy.vy = 0.0F;
    new_enemy.x1 = static_cast<int>(CENTER_X - ROUTE_HALF);
    new_enemy.y1 = static_cast<int>(CENTER_Y);
    new_enemy.x2 = static_cast<int>(CENTER_X + ROUTE_HALF);
    new_enemy.y2 = static_cast<int>(CENTER_Y);
    new_enemy.flip = true;
    new_enemy.frame = -1;

    // Añadir a los datos y crear el sprite vivo
    room_data_.enemies.push_back(new_enemy);
    room_->getEnemyManager()->addEnemy(Enemy::create(new_enemy));

    // Seleccionar el nuevo enemigo
    int new_index = static_cast<int>(room_data_.enemies.size()) - 1;
    selection_ = {.type = EntityType::ENEMY, .index = new_index};

    autosave();
    return "Added enemy " + std::to_string(new_index);
}

// Elimina el enemigo seleccionado
auto MapEditor::deleteEnemy() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedEnemy()) { return "No enemy selected"; }

    const int IDX = selection_.index;

    // Eliminar de los datos
    room_data_.enemies.erase(room_data_.enemies.begin() + IDX);

    // Recrear todos los enemigos vivos (los índices cambian al borrar)
    auto* enemy_mgr = room_->getEnemyManager();
    enemy_mgr->clear();
    for (const auto& enemy_data : room_data_.enemies) {
        enemy_mgr->addEnemy(Enemy::create(enemy_data));
    }

    selection_.clear();
    autosave();
    return "Deleted enemy " + std::to_string(IDX);
}

// Duplica el enemigo seleccionado (lo pone un tile a la derecha)
auto MapEditor::duplicateEnemy() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedEnemy()) { return "No enemy selected"; }

    // Copiar datos del enemigo seleccionado
    Enemy::Data copy = room_data_.enemies[selection_.index];

    // Desplazar un tile a la derecha
    copy.x += Tile::SIZE;
    copy.x1 += Tile::SIZE;
    copy.x2 += Tile::SIZE;

    // Añadir y crear sprite vivo
    room_data_.enemies.push_back(copy);
    room_->getEnemyManager()->addEnemy(Enemy::create(copy));

    // Seleccionar el nuevo enemigo
    int new_index = static_cast<int>(room_data_.enemies.size()) - 1;
    selection_ = {.type = EntityType::ENEMY, .index = new_index};

    autosave();
    return "Duplicated as enemy " + std::to_string(new_index);
}

// Modifica una propiedad de la habitación
auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    if (!active_) { return "Editor not active"; }

    std::string val = toLower(value);

    if (property == "ZONE") {
        const Zone::Data* zone = ZoneManager::get()->getZone(val);
        if (zone == nullptr) { return "Unknown zone: " + val; }
        room_data_.zone = val;
        // Si no hay overrides, propagar el tileset y la música de la nueva zona
        if (!room_data_.tile_set_overridden) {
            room_data_.tile_set_file = zone->tile_set_file;
            room_->setTileSet(zone->tile_set_file);
        }
        if (!room_data_.music_overridden) {
            room_data_.music = zone->music;
        }
        if (!room_data_.bg_color_overridden) {
            room_data_.bg_color = zone->bg_color;
            room_->setBgColor(zone->bg_color);
        }
        autosave();
        return "zone: " + val;
    }

    if (property == "TILESET") {
        // "reset" / "none" limpia el override y vuelve a heredar de la zona
        if (val == "reset" || val == "none") {
            room_data_.tile_set_overridden = false;
            const Zone::Data* zone = ZoneManager::get()->getZone(room_data_.zone);
            if (zone != nullptr) {
                room_data_.tile_set_file = zone->tile_set_file;
                room_->setTileSet(zone->tile_set_file);
            }
            autosave();
            return "tileset: (inherit from zone)";
        }
        std::string tileset = val;
        if (tileset.find('.') == std::string::npos) { tileset += ".gif"; }
        room_data_.tile_set_file = tileset;
        room_data_.tile_set_overridden = true;
        room_->setTileSet(tileset);
        autosave();
        return "tileset: " + tileset;
    }

    if (property == "BGCOLOR") {
        // "reset" / "none" limpia el override y vuelve a heredar de la zona
        if (val == "reset" || val == "none") {
            room_data_.bg_color_overridden = false;
            const Zone::Data* zone = ZoneManager::get()->getZone(room_data_.zone);
            if (zone != nullptr) {
                room_data_.bg_color = zone->bg_color;
                room_->setBgColor(zone->bg_color);
            }
            autosave();
            return "bgcolor: (inherit from zone)";
        }
        int color = 0;
        try {
            color = std::stoi(val);
        } catch (...) { return "usage: set bgcolor <0-255|reset>"; }
        if (color < 0 || color > 255) { return "bgcolor must be 0-255"; }
        room_data_.bg_color = static_cast<Uint8>(color);
        room_data_.bg_color_overridden = true;
        room_->setBgColor(static_cast<Uint8>(color));
        autosave();
        return "bgcolor: " + std::to_string(color);
    }

    if (property == "MUSIC") {
        // "reset" / "none" limpia el override y vuelve a heredar de la zona
        if (val == "reset" || val == "none") {
            room_data_.music_overridden = false;
            const Zone::Data* zone = ZoneManager::get()->getZone(room_data_.zone);
            if (zone != nullptr) {
                room_data_.music = zone->music;
            }
            autosave();
            return "music: (inherit from zone)";
        }
        // Nota: el valor se guarda tal cual (case-sensitive). val ya está en lower.
        // Usamos el value original para respetar mayúsculas del nombre del fichero.
        std::string music = value;
        if (music.find('.') == std::string::npos) { music += ".ogg"; }
        room_data_.music = music;
        room_data_.music_overridden = true;
        autosave();
        return "music: " + music;
    }

    // Conexiones: UP, DOWN, LEFT, RIGHT
    if (property == "UP" || property == "DOWN" || property == "LEFT" || property == "RIGHT") {
        std::string connection = "0";
        if (val != "0" && val != "null" && val != "none") {
            try {
                int num = std::stoi(val);
                char buf[16];
                std::snprintf(buf, sizeof(buf), "%03d.yaml", num);
                connection = buf;
            } catch (...) {
                connection = val;
                if (connection.find('.') == std::string::npos) { connection += ".yaml"; }
            }
        }

        // Dirección opuesta para la conexión recíproca
        std::string opposite;
        std::string* our_field = nullptr;
        if (property == "UP") {
            opposite = "lower_room";
            our_field = &room_data_.upper_room;
        }
        if (property == "DOWN") {
            opposite = "upper_room";
            our_field = &room_data_.lower_room;
        }
        if (property == "LEFT") {
            opposite = "right_room";
            our_field = &room_data_.left_room;
        }
        if (property == "RIGHT") {
            opposite = "left_room";
            our_field = &room_data_.right_room;
        }

        // Si había una conexión anterior, romper la recíproca de la otra room
        if (our_field != nullptr && *our_field != "0" && !our_field->empty()) {
            auto old_other = Resource::Cache::get()->getRoom(*our_field);
            if (old_other) {
                if (opposite == "upper_room") {
                    old_other->upper_room = "0";
                } else if (opposite == "lower_room") {
                    old_other->lower_room = "0";
                } else if (opposite == "left_room") {
                    old_other->left_room = "0";
                } else if (opposite == "right_room") {
                    old_other->right_room = "0";
                }
                // Guardar la otra room
                std::string other_path = Resource::List::get()->get(*our_field);
                if (!other_path.empty()) {
                    RoomFormat::saveYAML(other_path, *old_other);
                }
            }
        }

        // Aplicar la nueva conexión
        if (our_field != nullptr) { *our_field = connection; }

        // Sincronizar la conexión al Room vivo
        Room::Border border = Room::Border::NONE;
        if (property == "UP") {
            border = Room::Border::TOP;
        } else if (property == "DOWN") {
            border = Room::Border::BOTTOM;
        } else if (property == "LEFT") {
            border = Room::Border::LEFT;
        } else if (property == "RIGHT") {
            border = Room::Border::RIGHT;
        }
        room_->setConnection(border, connection);

        // Crear la conexión recíproca en la otra room
        if (connection != "0") {
            auto other = Resource::Cache::get()->getRoom(connection);
            if (other) {
                if (opposite == "upper_room") {
                    other->upper_room = room_path_;
                } else if (opposite == "lower_room") {
                    other->lower_room = room_path_;
                } else if (opposite == "left_room") {
                    other->left_room = room_path_;
                } else if (opposite == "right_room") {
                    other->right_room = room_path_;
                }
                std::string other_path = Resource::List::get()->get(connection);
                if (!other_path.empty()) {
                    RoomFormat::saveYAML(other_path, *other);
                }
            }
        }

        autosave();
        return toLower(property) + ": " + connection;
    }

    return "Unknown property: " + property + " (use: zone, itemcolor1, itemcolor2, conveyor, tileset, music, bgcolor, up, down, left, right)";
}

// Crea una nueva habitación
auto MapEditor::createNewRoom(const std::string& direction) -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    if (!active_) { return "Editor not active"; }

    // Validar dirección si se proporcionó
    if (!direction.empty() && direction != "LEFT" && direction != "RIGHT" && direction != "UP" && direction != "DOWN") {
        return "usage: room new [left|right|up|down]";
    }

    // Comprobar que no hay ya una room en esa dirección
    if (!direction.empty()) {
        const std::string* existing = nullptr;
        if (direction == "UP") {
            existing = &room_data_.upper_room;
        } else if (direction == "DOWN") {
            existing = &room_data_.lower_room;
        } else if (direction == "LEFT") {
            existing = &room_data_.left_room;
        } else if (direction == "RIGHT") {
            existing = &room_data_.right_room;
        }
        if (existing != nullptr && *existing != "0" && !existing->empty()) {
            return "Already connected " + toLower(direction) + ": " + *existing;
        }
    }

    // Encontrar el primer número libre (reutiliza huecos)
    const auto& rooms = Resource::Cache::get()->getRooms();
    std::set<int> used;
    for (const auto& r : rooms) {
        try {
            used.insert(std::stoi(r.name.substr(0, r.name.find('.'))));
        } catch (...) {
            // @INTENTIONAL: nombre de room no es numérico → saltar
        }
    }
    int new_num = 1;
    while (used.contains(new_num)) { ++new_num; }
    char name_buf[16];
    std::snprintf(name_buf, sizeof(name_buf), "%03d.yaml", new_num);
    std::string new_name = name_buf;

    // Derivar la ruta de la carpeta de rooms
    std::string ref_path = Resource::List::get()->get(room_path_);
    if (ref_path.empty()) { return "Error: cannot resolve room path"; }
    std::string room_dir = ref_path.substr(0, ref_path.find_last_of("\\/") + 1);
    std::string new_path = room_dir + new_name;

    // Construir Room::Data por defecto desde la autoridad del formato
    Room::Data new_room = RoomFormat::createDefault();
    new_room.number = std::string(name_buf).substr(0, std::string(name_buf).find('.'));

    // Heredar la zona de la room actual (y sus tileset/música resueltos), para
    // que el usuario no tenga que asignarla manualmente al expandir un nivel.
    // No marcamos los overrides: si la room actual tenía overrides explícitos,
    // la nueva se queda con los valores resueltos pero como heredados de zona.
    new_room.zone = room_data_.zone;
    const Zone::Data* zone = ZoneManager::get()->getZone(new_room.zone);
    if (zone != nullptr) {
        new_room.tile_set_file = zone->tile_set_file;
        new_room.music = zone->music;
    }

    // Conexión recíproca: la nueva room conecta de vuelta a la actual
    if (direction == "UP") {
        new_room.lower_room = room_path_;
    } else if (direction == "DOWN") {
        new_room.upper_room = room_path_;
    } else if (direction == "LEFT") {
        new_room.right_room = room_path_;
    } else if (direction == "RIGHT") {
        new_room.left_room = room_path_;
    }

    // Persistir vía la autoridad del formato (no más std::ofstream a pelo)
    auto save_result = RoomFormat::saveYAML(new_path, new_room);
    if (save_result.starts_with("Error")) { return save_result; }

    // Registrar en Resource::List (mapa + assets.yaml) y cache
    Resource::List::get()->addAsset(new_path, Resource::List::Type::ROOM);
    Resource::Cache::get()->getRooms().emplace_back(
        RoomResource{.name = new_name, .room = std::make_shared<Room::Data>(new_room)});

    // Conectar la room actual con la nueva (recíproco: ya hecho arriba para la nueva).
    // Actualizamos tres sitios para que la conexión sea visible inmediatamente:
    //   1. room_data_ (la copia del editor) → será autosaveada al yaml
    //   2. la Room viva del juego (room_) → para que el navigation funcione sin reload
    //   3. la Room::Data cacheada en Resource::Cache → para que adjacent rooms y
    //      futuros reloads vean la conexión nueva
    Room::Border border = Room::Border::NONE;
    if (direction == "UP") {
        room_data_.upper_room = new_name;
        border = Room::Border::TOP;
    } else if (direction == "DOWN") {
        room_data_.lower_room = new_name;
        border = Room::Border::BOTTOM;
    } else if (direction == "LEFT") {
        room_data_.left_room = new_name;
        border = Room::Border::LEFT;
    } else if (direction == "RIGHT") {
        room_data_.right_room = new_name;
        border = Room::Border::RIGHT;
    }

    if (border != Room::Border::NONE) {
        // Sincronizar con la Room viva (Game::room_cache_ apunta a este shared_ptr)
        room_->setConnection(border, new_name);

        // Sincronizar con Resource::Cache::rooms_ (datos crudos)
        auto cached = Resource::Cache::get()->getRoom(room_path_);
        if (cached) {
            if (direction == "UP") {
                cached->upper_room = new_name;
            } else if (direction == "DOWN") {
                cached->lower_room = new_name;
            } else if (direction == "LEFT") {
                cached->left_room = new_name;
            } else if (direction == "RIGHT") {
                cached->right_room = new_name;
            }
        }
    }

    if (!direction.empty()) { autosave(); }

    // Navegar a la nueva room
    reenter_ = true;
    if (GameControl::exit_editor) { GameControl::exit_editor(); }
    if (GameControl::change_room && GameControl::change_room(new_name)) {
        if (GameControl::enter_editor) { GameControl::enter_editor(); }
    }

    return "Created room " + new_name + (direction.empty() ? "" : " (" + toLower(direction) + ")");
}

// Elimina la habitación actual
auto MapEditor::deleteRoom() -> std::string {  // NOLINT(readability-function-cognitive-complexity)
    if (!active_) { return "Editor not active"; }

    std::string deleted_name = room_path_;

    // Buscar una room vecina a la que navegar después de borrar
    std::string target = "0";
    if (room_data_.upper_room != "0" && !room_data_.upper_room.empty()) {
        target = room_data_.upper_room;
    } else if (room_data_.lower_room != "0" && !room_data_.lower_room.empty()) {
        target = room_data_.lower_room;
    } else if (room_data_.left_room != "0" && !room_data_.left_room.empty()) {
        target = room_data_.left_room;
    } else if (room_data_.right_room != "0" && !room_data_.right_room.empty()) {
        target = room_data_.right_room;
    }

    if (target == "0") {
        // Buscar la primera room que no sea esta
        for (const auto& r : Resource::Cache::get()->getRooms()) {
            if (r.name != deleted_name) {
                target = r.name;
                break;
            }
        }
    }
    if (target == "0") { return "Cannot delete: no other room to navigate to"; }

    // Desenlazar todas las conexiones recíprocas
    auto unlink_reciprocal = [&](const std::string& neighbor, const std::string& field_name) {
        if (neighbor == "0" || neighbor.empty()) { return; }
        auto other = Resource::Cache::get()->getRoom(neighbor);
        if (!other) { return; }
        if (field_name == "upper_room") {
            other->upper_room = "0";
        } else if (field_name == "lower_room") {
            other->lower_room = "0";
        } else if (field_name == "left_room") {
            other->left_room = "0";
        } else if (field_name == "right_room") {
            other->right_room = "0";
        }
        // Guardar la otra room
        std::string other_path = Resource::List::get()->get(neighbor);
        if (!other_path.empty()) {
            RoomFormat::saveYAML(other_path, *other);
        }
    };

    unlink_reciprocal(room_data_.upper_room, "lower_room");  // Si nosotros somos su lower
    unlink_reciprocal(room_data_.lower_room, "upper_room");  // Si nosotros somos su upper
    unlink_reciprocal(room_data_.left_room, "right_room");   // Si nosotros somos su right
    unlink_reciprocal(room_data_.right_room, "left_room");   // Si nosotros somos su left

    // Navegar a la room destino antes de borrar
    reenter_ = true;
    if (GameControl::exit_editor) { GameControl::exit_editor(); }
    if (GameControl::change_room) { GameControl::change_room(target); }

    // Borrar el YAML del disco
    std::string yaml_path = Resource::List::get()->get(deleted_name);
    if (!yaml_path.empty()) {
        std::remove(yaml_path.c_str());
    }

    // Quitar del cache
    auto& cache_rooms = Resource::Cache::get()->getRooms();
    std::erase_if(cache_rooms, [&](const RoomResource& r) { return r.name == deleted_name; });

    // Quitar de Resource::List (mapa + assets.yaml)
    Resource::List::get()->removeAsset(deleted_name);

    // Re-entrar al editor en la room destino
    if (GameControl::enter_editor) { GameControl::enter_editor(); }

    return "Deleted room " + deleted_name + ", moved to " + target;
}

// Modifica una propiedad del item seleccionado
auto MapEditor::setItemProperty(const std::string& property, const std::string& value) -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedItem()) { return "No item selected"; }

    auto& item = room_data_.items[selection_.index];

    if (property == "TILE") {
        // Abrir el tile picker visual
        openTilePicker(item.tile_set_file, item.tile);
        return "Select tile...";
    }

    if (property == "COUNTER") {
        try {
            item.counter = std::stoi(value);
        } catch (...) { return "Invalid value: " + value; }
        autosave();
        return "counter: " + std::to_string(item.counter);
    }

    if (property == "COLOR1") {
        // "reset" / "none" limpia el override y vuelve al default
        const std::string LOWER = toLower(value);
        if (LOWER == "reset" || LOWER == "none") {
            item.color1 = Item::DEFAULT_COLOR1;
            item.color1_overridden = false;
            // Recrear el sprite vivo con los colores nuevos
            room_->getItemManager()->getItem(selection_.index) = std::make_shared<Item>(item);
            autosave();
            return "color1: (default)";
        }
        try {
            item.color1 = static_cast<Uint8>(std::stoi(value));
        } catch (...) { return "Invalid value: " + value; }
        item.color1_overridden = true;
        room_->getItemManager()->getItem(selection_.index) = std::make_shared<Item>(item);
        autosave();
        return "color1: " + std::to_string(item.color1);
    }

    if (property == "COLOR2") {
        const std::string LOWER = toLower(value);
        if (LOWER == "reset" || LOWER == "none") {
            item.color2 = Item::DEFAULT_COLOR2;
            item.color2_overridden = false;
            room_->getItemManager()->getItem(selection_.index) = std::make_shared<Item>(item);
            autosave();
            return "color2: (default)";
        }
        try {
            item.color2 = static_cast<Uint8>(std::stoi(value));
        } catch (...) { return "Invalid value: " + value; }
        item.color2_overridden = true;
        room_->getItemManager()->getItem(selection_.index) = std::make_shared<Item>(item);
        autosave();
        return "color2: " + std::to_string(item.color2);
    }

    return "Unknown property: " + property + " (use: tile, counter, color1, color2)";
}

// Abre el tile picker para seleccionar un tile
void MapEditor::openTilePicker(const std::string& tileset_name, int current_tile) {
    // Cerrar la consola si está abierta (para que el primer click vaya al picker)
    if (Console::get()->isActive()) {
        Console::get()->toggle();
    }

    tile_picker_.on_select = [this, tileset_name](int col, int row, int /*width*/, int /*height*/) {
        if (!hasSelectedItem()) { return; }
        // Items solo usan un tile: tomamos siempre la celda superior-izquierda del rect
        auto surface = Resource::Cache::get()->getSurface(tileset_name);
        if (!surface) { return; }
        int cols = static_cast<int>(surface->getWidth()) / Tile::SIZE;
        if (cols <= 0) { return; }
        int tile = (row * cols) + col;
        room_data_.items[selection_.index].tile = tile;
        room_->getItemManager()->getItem(selection_.index)->setTile(tile);
        autosave();
    };
    // Pasar color de fondo de la habitación + color del item seleccionado para previsualizarlo
    Uint8 preview_color = hasSelectedItem()
        ? room_data_.items[selection_.index].color1
        : Item::DEFAULT_COLOR1;
    tile_picker_.open(tileset_name, current_tile, room_data_.bg_color, 1, preview_color, 0, 1, true);
}

// Crea un nuevo item con valores por defecto, centrado en la habitación
auto MapEditor::addItem() -> std::string {
    if (!active_) { return "Editor not active"; }

    Item::Data new_item;
    new_item.tile_set_file = "items.gif";
    new_item.tile = 42;  // Tile por defecto
    new_item.x = PlayArea::CENTER_X;
    new_item.y = PlayArea::CENTER_Y;
    new_item.counter = 0;
    // Los colores quedan en sus defaults (Item::DEFAULT_COLOR1/2)

    room_data_.items.push_back(new_item);
    room_->getItemManager()->addItem(std::make_shared<Item>(new_item));

    int new_index = static_cast<int>(room_data_.items.size()) - 1;
    selection_ = {.type = EntityType::ITEM, .index = new_index};

    autosave();
    return "Added item " + std::to_string(new_index);
}

// Elimina el item seleccionado
auto MapEditor::deleteItem() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedItem()) { return "No item selected"; }

    const int IDX = selection_.index;
    room_data_.items.erase(room_data_.items.begin() + IDX);

    // Recrear todos los items (los índices cambian al borrar)
    auto* item_mgr = room_->getItemManager();
    item_mgr->clear();
    for (const auto& item_data : room_data_.items) {
        item_mgr->addItem(std::make_shared<Item>(item_data));
    }

    selection_.clear();
    autosave();
    return "Deleted item " + std::to_string(IDX);
}

// Duplica el item seleccionado (lo pone un tile a la derecha)
auto MapEditor::duplicateItem() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedItem()) { return "No item selected"; }

    Item::Data copy = room_data_.items[selection_.index];
    copy.x += Tile::SIZE;

    room_data_.items.push_back(copy);
    room_->getItemManager()->addItem(std::make_shared<Item>(copy));

    int new_index = static_cast<int>(room_data_.items.size()) - 1;
    selection_ = {.type = EntityType::ITEM, .index = new_index};

    autosave();
    return "Duplicated as item " + std::to_string(new_index);
}

// Modifica una propiedad de la plataforma seleccionada
auto MapEditor::setPlatformProperty(const std::string& property, const std::string& value) -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedPlatform()) { return "No platform selected"; }

    auto& platform = room_data_.platforms[selection_.index];

    if (property == "ANIMATION") {
        std::string anim = toLower(value);
        if (anim.find('.') == std::string::npos) { anim += ".yaml"; }

        std::string old_anim = platform.animation_path;
        platform.animation_path = anim;
        try {
            auto* platform_mgr = room_->getPlatformManager();
            platform_mgr->getPlatform(selection_.index) = std::make_shared<MovingPlatform>(platform);
        } catch (const std::exception& e) {
            platform.animation_path = old_anim;
            return std::string("Error: ") + e.what();
        }

        autosave();
        return "animation: " + anim;
    }

    if (property == "SPEED") {
        try {
            platform.speed = std::stof(value);
        } catch (...) { return "Invalid value: " + value; }

        room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform);
        autosave();
        return "speed: " + std::to_string(static_cast<int>(platform.speed));
    }

    if (property == "LOOP") {
        std::string val = toLower(value);
        if (val == "circular") {
            platform.loop = LoopMode::CIRCULAR;
        } else {
            platform.loop = LoopMode::PINGPONG;
            val = "pingpong";
        }

        room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform);
        autosave();
        return "loop: " + val;
    }

    if (property == "EASING") {
        platform.easing = toLower(value);

        room_->getPlatformManager()->getPlatform(selection_.index)->resetToInitialPosition(platform);
        autosave();
        return "easing: " + platform.easing;
    }

    return "Unknown property: " + property + " (use: animation, speed, loop, easing)";
}

// Crea una nueva plataforma con valores por defecto, centrada en la habitación
auto MapEditor::addPlatform() -> std::string {
    if (!active_) { return "Editor not active"; }

    MovingPlatform::Data new_platform;
    new_platform.animation_path = "bin.yaml";
    new_platform.speed = 24.0F;
    new_platform.frame = -1;
    constexpr float CENTER_X = PlayArea::CENTER_X;
    constexpr float CENTER_Y = PlayArea::CENTER_Y;
    constexpr float ROUTE_HALF = 2.0F * Tile::SIZE;
    new_platform.path = {
        {.x = CENTER_X - ROUTE_HALF, .y = CENTER_Y, .wait = 0.0F},
        {.x = CENTER_X + ROUTE_HALF, .y = CENTER_Y, .wait = 0.0F}};

    room_data_.platforms.push_back(new_platform);
    room_->getPlatformManager()->addPlatform(std::make_shared<MovingPlatform>(new_platform));

    int new_index = static_cast<int>(room_data_.platforms.size()) - 1;
    selection_ = {.type = EntityType::PLATFORM, .index = new_index};

    autosave();
    return "Added platform " + std::to_string(new_index);
}

// Elimina la plataforma seleccionada
auto MapEditor::deletePlatform() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedPlatform()) { return "No platform selected"; }

    const int IDX = selection_.index;
    room_data_.platforms.erase(room_data_.platforms.begin() + IDX);

    // Recrear todas las plataformas (los índices cambian al borrar)
    auto* platform_mgr = room_->getPlatformManager();
    platform_mgr->clear();
    for (const auto& platform_data : room_data_.platforms) {
        platform_mgr->addPlatform(std::make_shared<MovingPlatform>(platform_data));
    }

    selection_.clear();
    autosave();
    return "Deleted platform " + std::to_string(IDX);
}

// Duplica la plataforma seleccionada (la pone un tile a la derecha)
auto MapEditor::duplicatePlatform() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedPlatform()) { return "No platform selected"; }

    MovingPlatform::Data copy = room_data_.platforms[selection_.index];
    for (auto& wp : copy.path) {
        wp.x += Tile::SIZE;
    }

    room_data_.platforms.push_back(copy);
    room_->getPlatformManager()->addPlatform(std::make_shared<MovingPlatform>(copy));

    int new_index = static_cast<int>(room_data_.platforms.size()) - 1;
    selection_ = {.type = EntityType::PLATFORM, .index = new_index};

    autosave();
    return "Duplicated as platform " + std::to_string(new_index);
}

// ============================================================================
// LLAVES
// ============================================================================

// Modifica una propiedad de la llave seleccionada
auto MapEditor::setKeyProperty(const std::string& property, const std::string& value) -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedKey()) { return "No key selected"; }

    auto& key = room_data_.keys[selection_.index];

    if (property == "ID") {
        key.id = value;
        // Recrear el Key (el id se pasa al constructor)
        try {
            room_->getKeyManager()->getKey(selection_.index) = std::make_shared<Key>(key);
        } catch (const std::exception& e) {
            return std::string("Error: ") + e.what();
        }
        autosave();
        return "id: " + value;
    }

    if (property == "ANIMATION") {
        std::string anim = toLower(value);
        if (anim.find('.') == std::string::npos) { anim += ".yaml"; }
        std::string old_anim = key.animation_path;
        key.animation_path = anim;
        try {
            room_->getKeyManager()->getKey(selection_.index) = std::make_shared<Key>(key);
        } catch (const std::exception& e) {
            key.animation_path = old_anim;
            return std::string("Error: ") + e.what();
        }
        autosave();
        return "animation: " + anim;
    }

    return "Unknown property: " + property + " (use: id, animation)";
}

// Crea una nueva llave centrada en la habitación
auto MapEditor::addKey() -> std::string {
    if (!active_) { return "Editor not active"; }

    Key::Data new_key;
    new_key.animation_path = "key1.yaml";
    new_key.id = "1";
    new_key.x = static_cast<float>(PlayArea::CENTER_X);
    new_key.y = static_cast<float>(PlayArea::CENTER_Y);

    room_data_.keys.push_back(new_key);
    try {
        room_->getKeyManager()->addKey(std::make_shared<Key>(new_key));
    } catch (const std::exception& e) {
        room_data_.keys.pop_back();
        return std::string("Error: ") + e.what();
    }

    int new_index = static_cast<int>(room_data_.keys.size()) - 1;
    selection_ = {.type = EntityType::KEY, .index = new_index};

    autosave();
    return "Added key " + std::to_string(new_index);
}

// Elimina la llave seleccionada
auto MapEditor::deleteKey() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedKey()) { return "No key selected"; }

    const int IDX = selection_.index;
    room_data_.keys.erase(room_data_.keys.begin() + IDX);

    // Recrear todas las llaves (los índices cambian al borrar)
    auto* key_mgr = room_->getKeyManager();
    key_mgr->clear();
    for (const auto& key_data : room_data_.keys) {
        key_mgr->addKey(std::make_shared<Key>(key_data));
    }

    selection_.clear();
    autosave();
    return "Deleted key " + std::to_string(IDX);
}

// Duplica la llave seleccionada (la pone un tile a la derecha)
auto MapEditor::duplicateKey() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedKey()) { return "No key selected"; }

    Key::Data copy = room_data_.keys[selection_.index];
    copy.x += Tile::SIZE;

    room_data_.keys.push_back(copy);
    try {
        room_->getKeyManager()->addKey(std::make_shared<Key>(copy));
    } catch (const std::exception& e) {
        room_data_.keys.pop_back();
        return std::string("Error: ") + e.what();
    }

    int new_index = static_cast<int>(room_data_.keys.size()) - 1;
    selection_ = {.type = EntityType::KEY, .index = new_index};

    autosave();
    return "Duplicated as key " + std::to_string(new_index);
}

// ============================================================================
// PUERTAS
// ============================================================================

// Reconstruye todas las puertas vivas desde room_data_, limpiando primero los
// WALLs antiguos del CollisionMap. Lo usa setDoorProperty cuando un cambio
// (id, animation) requiere recrear el Door y mantener los tiles sincronizados.
void MapEditor::rebuildDoors() {
    auto* door_mgr = room_->getDoorManager();
    // Borrar una a una desde el principio: cada removeDoor desregistra el
    // Door del SolidActorManager.
    while (door_mgr->getCount() > 0) {
        door_mgr->removeDoor(0);
    }
    // Re-añadir desde room_data_; addDoor las registra como SolidActor.
    for (const auto& d : room_data_.doors) {
        door_mgr->addDoor(std::make_shared<Door>(d, /*start_opened=*/false));
    }
}

// Modifica una propiedad de la puerta seleccionada
auto MapEditor::setDoorProperty(const std::string& property, const std::string& value) -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedDoor()) { return "No door selected"; }

    auto& door = room_data_.doors[selection_.index];

    if (property == "ID") {
        door.id = value;
        // Recrear preserva el id pero pasa por el constructor → rebuild completo
        // para mantener los tiles del CollisionMap sincronizados.
        rebuildDoors();
        autosave();
        return "id: " + value;
    }

    if (property == "ANIMATION") {
        std::string anim = toLower(value);
        if (anim.find('.') == std::string::npos) { anim += ".yaml"; }
        std::string old_anim = door.animation_path;
        door.animation_path = anim;
        try {
            rebuildDoors();
        } catch (const std::exception& e) {
            door.animation_path = old_anim;
            // Reconstruir con los datos viejos para dejar el manager coherente
            rebuildDoors();
            return std::string("Error: ") + e.what();
        }
        autosave();
        return "animation: " + anim;
    }

    return "Unknown property: " + property + " (use: id, animation)";
}

// Crea una nueva puerta centrada en la habitación, alineada al grid de 8 px
auto MapEditor::addDoor() -> std::string {
    if (!active_) { return "Editor not active"; }

    Door::Data new_door;
    new_door.animation_path = "door1.yaml";
    new_door.id = "1";
    // Centrar y alinear al grid (la puerta ocupa 1×4 tiles)
    new_door.x = static_cast<float>((PlayArea::CENTER_X / Tile::SIZE) * Tile::SIZE);
    new_door.y = static_cast<float>((PlayArea::CENTER_Y / Tile::SIZE) * Tile::SIZE);

    room_data_.doors.push_back(new_door);
    try {
        // addDoor del manager ya escribe los WALLs si la puerta es bloqueante
        // (lo es por defecto, porque pasamos start_opened=false).
        room_->getDoorManager()->addDoor(std::make_shared<Door>(new_door, /*start_opened=*/false));
    } catch (const std::exception& e) {
        room_data_.doors.pop_back();
        return std::string("Error: ") + e.what();
    }

    int new_index = static_cast<int>(room_data_.doors.size()) - 1;
    selection_ = {.type = EntityType::DOOR, .index = new_index};

    autosave();
    return "Added door " + std::to_string(new_index);
}

// Elimina la puerta seleccionada (limpia los WALLs del CollisionMap)
auto MapEditor::deleteDoor() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedDoor()) { return "No door selected"; }

    const int IDX = selection_.index;

    // Importante: usar removeDoor del manager (desregistra del SolidActorManager)
    room_->getDoorManager()->removeDoor(IDX);
    room_data_.doors.erase(room_data_.doors.begin() + IDX);

    selection_.clear();
    autosave();
    return "Deleted door " + std::to_string(IDX);
}

// Duplica la puerta seleccionada (la pone una altura entera abajo, 4 tiles,
// para no solapar visualmente)
auto MapEditor::duplicateDoor() -> std::string {
    if (!active_) { return "Editor not active"; }
    if (!hasSelectedDoor()) { return "No door selected"; }

    Door::Data copy = room_data_.doors[selection_.index];
    copy.y += 4 * Tile::SIZE;  // 4 tiles = altura completa de una puerta

    room_data_.doors.push_back(copy);
    try {
        room_->getDoorManager()->addDoor(std::make_shared<Door>(copy, /*start_opened=*/false));
    } catch (const std::exception& e) {
        room_data_.doors.pop_back();
        return std::string("Error: ") + e.what();
    }

    int new_index = static_cast<int>(room_data_.doors.size()) - 1;
    selection_ = {.type = EntityType::DOOR, .index = new_index};

    autosave();
    return "Duplicated as door " + std::to_string(new_index);
}

// Elige un color de grid que contraste con el fondo
// Empieza con bright_black (1), si coincide con el bg en la paleta activa, sube índices
static auto pickGridColor(Uint8 bg, const std::shared_ptr<Surface>& surface) -> Uint8 {
    Uint8 grid = 1;
    Uint32 bg_argb = surface->getPaletteColor(bg);

    // Si bright_black es igual al bg, buscar el siguiente color distinto
    while (grid < 15 && surface->getPaletteColor(grid) == bg_argb) {
        grid += 2;  // Saltar de 2 en 2 para mantenerse en tonos discretos
    }

    return grid;
}

// Dibuja la cuadrícula de tiles (líneas de puntos, 1 pixel sí / 1 no)
void MapEditor::renderGrid() {
    auto game_surface = Screen::get()->getRendererSurface();
    if (!game_surface) { return; }

    const Uint8 COLOR = pickGridColor(0, game_surface);

    // Líneas verticales (cada 8 pixels)
    for (int x = Tile::SIZE; x < PlayArea::WIDTH; x += Tile::SIZE) {
        for (int y = 0; y < PlayArea::HEIGHT; y += 2) {
            game_surface->putPixel(x, y, COLOR);
        }
    }

    // Líneas horizontales (cada 8 pixels)
    for (int y = Tile::SIZE; y < PlayArea::HEIGHT; y += Tile::SIZE) {
        for (int x = 0; x < PlayArea::WIDTH; x += 2) {
            game_surface->putPixel(x, y, COLOR);
        }
    }
}

#endif  // _DEBUG
