#include "views.h"
#include "entity_ops.h"
#include "project_manager.h"

#include "viz/graph_types.h"
#include "viz/graph_viewport.h"
#include "viz/graph_sources.h"

#include "../../../../cpp/vendor/sqlite3/sqlite3.h"

#include "core/button.h"
#include "core/icon_button.h"
#include "core/toolbar.h"
#include "core/select.h"
#include "core/modal_dialog.h"
#include "core/text_input.h"
#include "core/tokens.h"
#include "core/icons_tabler.h"

// data_table — issue 0081-J: migracion panel Table a data_table::render.
#include "viz/data_table.h"

#include "imgui.h"

#include <algorithm>
#include <cctype>
#include <cfloat>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unordered_set>

namespace ge {

namespace {

const char* k_layout_names[] = {
    "force", "grid", "circular", "radial", "hierarchical", "fixed",
};
constexpr int k_layout_count = (int)(sizeof(k_layout_names) / sizeof(k_layout_names[0]));

ImVec4 abgr_to_imvec4(uint32_t c) {
    uint8_t r = (uint8_t)( c        & 0xFF);
    uint8_t g = (uint8_t)((c >>  8) & 0xFF);
    uint8_t b = (uint8_t)((c >> 16) & 0xFF);
    uint8_t a = (uint8_t)((c >> 24) & 0xFF);
    return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);
}

void color_swatch(uint32_t color, float size = 12.0f) {
    ImVec2 p = ImGui::GetCursorScreenPos();
    ImDrawList* dl = ImGui::GetWindowDrawList();
    dl->AddRectFilled(p, ImVec2(p.x + size, p.y + size),
                      ImGui::ColorConvertFloat4ToU32(abgr_to_imvec4(color)),
                      3.0f);
    ImGui::Dummy(ImVec2(size, size));
    ImGui::SameLine();
}

} // namespace

void views_reset_visibility(AppState& app) {
    if (!app.graph) return;
    int nt = app.graph->type_count;
    int nr = app.graph->rel_type_count;
    if (nt > 256) nt = 256;
    if (nr > 256) nr = 256;
    for (int i = 0; i < nt; ++i) app.type_visible[i]     = true;
    for (int i = 0; i < nr; ++i) app.rel_type_visible[i] = true;
    app.type_visible_n     = nt;
    app.rel_type_visible_n = nr;
}

// FNV1a-64 — debe coincidir con graph_sources.cpp (escribe el hash de id en
// node.user_data). Usado para mapear ids del FTS a nodos.
static uint64_t fnv1a64_id(const char* s) {
    uint64_t h = 1469598103934665603ULL;
    for (; s && *s; ++s) {
        h ^= (uint8_t)*s;
        h *= 1099511628211ULL;
    }
    return h;
}

void views_apply_visibility(AppState& app) {
    // Toda la mascara (tipos + filter) se computa en views_filter_apply.
    // Mantenemos esta firma por compatibilidad; basta con marcar dirty y
    // dejar que el reapply integre todo de una pasada.
    app.filter_dirty = true;
    views_filter_apply(app);
}

bool views_filter_active(const AppState& app) {
    return app.filter_query_buf[0] != 0 || !app.filter_tags.empty();
}

void views_filter_add_tag(AppState& app, const char* tag) {
    if (!tag || !*tag) return;
    for (const auto& t : app.filter_tags) if (t == tag) return;
    app.filter_tags.emplace_back(tag);
    app.filter_dirty = true;
}

void views_filter_clear(AppState& app) {
    app.filter_query_buf[0] = 0;
    app.filter_tags.clear();
    app.filter_hits.clear();
    app.filter_dropdown_open = false;
    app.filter_dirty = true;
}

void views_filter_apply(AppState& app) {
    if (!app.graph) { app.filter_dirty = false; return; }
    GraphData& g = *app.graph;
    const bool has_filter = views_filter_active(app);

    // 1) Construir el set de ids matching (FTS query AND tags). Si el filtro
    //    esta inactivo, no hay set: todos los nodos son "match".
    std::unordered_set<uint64_t> match_hashes;
    bool have_match_set = false;

    if (has_filter && !app.input_db_path.empty()) {
        std::vector<std::string> ids_query;
        std::vector<std::string> ids_tags;
        bool have_q = (app.filter_query_buf[0] != 0);
        bool have_t = !app.filter_tags.empty();

        if (have_q) {
            std::vector<EntityHit> hits;
            // Limit alto para que el filtro no recorte por encima del dropdown.
            entity_search_fts(app.input_db_path.c_str(),
                              app.filter_query_buf, 200, &hits);
            ids_query.reserve(hits.size());
            for (auto& h : hits) ids_query.emplace_back(std::move(h.id));
        }
        if (have_t) {
            entity_list_by_tags(app.input_db_path.c_str(),
                                app.filter_tags, &ids_tags);
        }

        std::vector<std::string>* primary = nullptr;
        const std::vector<std::string>* secondary = nullptr;
        if (have_q && have_t) {
            primary   = &ids_query;
            secondary = &ids_tags;
        } else if (have_q) {
            primary = &ids_query;
        } else {
            primary = &ids_tags;
        }

        std::unordered_set<std::string> sec_set;
        if (secondary) sec_set.insert(secondary->begin(), secondary->end());

        match_hashes.reserve(primary->size());
        for (const auto& id : *primary) {
            if (secondary && !sec_set.count(id)) continue;
            match_hashes.insert(fnv1a64_id(id.c_str()));
        }
        have_match_set = true;
    }

    // 2) Refrescar dropdown (max 20). Solo cuando hay query — los chips de
    //    tags no contribuyen al dropdown (su feedback visual son los chips).
    if (app.filter_query_buf[0]) {
        app.filter_hits.clear();
        if (!app.input_db_path.empty()) {
            entity_search_fts(app.input_db_path.c_str(),
                              app.filter_query_buf, 20, &app.filter_hits);
        }
    } else {
        app.filter_hits.clear();
    }

    // 3) Aplicar mascara a nodos: type-filter + filter (query/tags) + modo.
    const bool hide_mode = (app.filter_mode == AppState::FM_HIDE);
    for (int i = 0; i < g.node_count; ++i) {
        GraphNode& n = g.nodes[i];
        uint16_t t   = n.type_id;
        bool type_vis = (t < (uint16_t)app.type_visible_n) ? app.type_visible[t] : true;

        bool match = true;
        if (have_match_set) match = match_hashes.count(n.user_data) > 0;

        if (!type_vis) {
            n.flags &= ~NF_VISIBLE;
            n.color_override = 0u;
            continue;
        }

        if (!have_match_set || match) {
            n.flags |= NF_VISIBLE;
            n.color_override = 0u;             // restaurar color del tipo
        } else if (hide_mode) {
            n.flags &= ~NF_VISIBLE;
            n.color_override = 0u;
        } else {
            // Highlight: dim al ~25% de alpha conservando RGB del tipo.
            uint32_t base = resolve_node_color(n, g.types, g.type_count);
            n.color_override = (base & 0x00FFFFFFu) | (0x40u << 24);
            n.flags |= NF_VISIBLE;
        }
    }

    // 4) Edges: visibles si ambos endpoints son visibles y el rel_type lo
    //    permite. Mantiene el comportamiento previo.
    for (int i = 0; i < g.edge_count; ++i) {
        const GraphEdge& e = g.edges[i];
        bool rel_vis = (e.type_id < (uint16_t)app.rel_type_visible_n)
                           ? app.rel_type_visible[e.type_id] : true;
        bool src_vis = (e.source < (uint32_t)g.node_count) &&
                       (g.nodes[e.source].flags & NF_VISIBLE);
        bool tgt_vis = (e.target < (uint32_t)g.node_count) &&
                       (g.nodes[e.target].flags & NF_VISIBLE);
        bool vis = rel_vis && src_vis && tgt_vis;
        if (vis) g.edges[i].flags |=  EF_VISIBLE;
        else     g.edges[i].flags &= ~EF_VISIBLE;
    }

    app.filter_dirty = false;
}

// ----------------------------------------------------------------------------
// Toolbar
// ----------------------------------------------------------------------------

void views_toolbar(AppState& app) {
    using namespace fn_ui;
    toolbar_begin();
        // Project switcher — etiqueta = proyecto activo. Click abre popup con
        // New / Open / Recent / Reveal. Si no hay proyecto activo (modo
        // legacy con --input directo), muestra "(no project)".
        {
            char btn_label[128];
            const char* p = app.active_project.empty()
                                ? "(no project)" : app.active_project.c_str();
            std::snprintf(btn_label, sizeof(btn_label), TI_FOLDER " Project: %s", p);
            if (button(btn_label, ButtonVariant::Secondary)) {
                // Refresca caches al abrir
                project_list(&app.project_list_cache);
                ProjectSettings ps;
                project_settings_load(&ps);
                app.project_recent_cache = ps.recent;
                ImGui::OpenPopup("##project_menu");
            }
            if (ImGui::BeginPopup("##project_menu")) {
                if (ImGui::MenuItem(TI_PLUS " New project...")) {
                    app.show_new_project_modal = true;
                    app.new_project_buf[0] = 0;
                    app.new_project_error.clear();
                }
                ImGui::Separator();

                // Recent submenu
                if (!app.project_recent_cache.empty()) {
                    if (ImGui::BeginMenu("Recent")) {
                        for (const auto& slug : app.project_recent_cache) {
                            bool is_active = (slug == app.active_project);
                            if (ImGui::MenuItem(slug.c_str(), nullptr, is_active)
                                && !is_active) {
                                app.want_switch_project = true;
                                app.switch_project_target = slug;
                            }
                        }
                        ImGui::EndMenu();
                    }
                }

                // Open submenu — todos los detectados
                if (ImGui::BeginMenu("Open")) {
                    if (app.project_list_cache.empty()) {
                        ImGui::TextDisabled("(no projects yet)");
                    } else {
                        for (const auto& slug : app.project_list_cache) {
                            bool is_active = (slug == app.active_project);
                            if (ImGui::MenuItem(slug.c_str(), nullptr, is_active)
                                && !is_active) {
                                app.want_switch_project = true;
                                app.switch_project_target = slug;
                            }
                        }
                    }
                    ImGui::EndMenu();
                }

                ImGui::Separator();
                bool can_reveal = !app.active_project.empty();
                if (ImGui::MenuItem(TI_FOLDER_OPEN " Reveal in explorer",
                                     nullptr, false, can_reveal)) {
                    project_reveal_in_explorer(app.active_project.c_str());
                }
                ImGui::EndPopup();
            }
        }
        ImGui::SameLine();
        toolbar_separator();

        if (button(TI_FOLDER " Open file...", ButtonVariant::Secondary)) {
            app.show_open_modal = true;
        }
        ImGui::SameLine();
        if (button(TI_TABLE " Import dataset...", ButtonVariant::Secondary)) {
            app.show_import_modal = true;
            app.import_error.clear();
        }
        ImGui::SameLine();
        // Dropdown "Tables ▾" — toggle visibilidad de las ventanas Table
        // expandidas. Desmarcar = colapsar (cerrar ventana + expanded=false).
        {
            char btn[64];
            std::snprintf(btn, sizeof(btn), TI_TABLE " Tables (%zu)",
                           app.node_groups_windows.size());
            if (button(btn, ButtonVariant::Subtle)) {
                ImGui::OpenPopup("##tables_menu");
            }
            if (ImGui::BeginPopup("##tables_menu")) {
                if (app.node_groups_windows.empty()) {
                    ImGui::TextDisabled("(no open NodeGroups)");
                } else {
                    for (auto& kv : app.node_groups_windows) {
                        bool checked = kv.second.open;
                        char lbl[160];
                        std::snprintf(lbl, sizeof(lbl), "%s  (%lld rows)",
                                       kv.second.meta.name.c_str(),
                                       (long long)kv.second.total_rows);
                        if (ImGui::MenuItem(lbl, nullptr, checked)) {
                            kv.second.open = !checked;
                        }
                    }
                    ImGui::Separator();
                    if (ImGui::MenuItem(TI_X " Collapse all")) {
                        for (auto& kv : app.node_groups_windows) kv.second.open = false;
                    }
                }
                ImGui::EndPopup();
            }
        }
        ImGui::SameLine();

        // Add node — input + auto-deteccion de tipo. Enter o boton "Add" lo
        // confirman; main.cpp inserta en operations.db y dispara reload.
        ImGui::SetNextItemWidth(220);
        DetectedType dt = detect_type(app.add_buf);
        ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue;
        char hint[64];
        std::snprintf(hint, sizeof(hint), "Add node (%s)...", detected_type_name(dt));
        if (ImGui::InputTextWithHint("##addnode", hint, app.add_buf, sizeof(app.add_buf), flags)) {
            app.want_add_node = true;
        }
        ImGui::SameLine();
        if (button(TI_PLUS " Add", ButtonVariant::Primary)) {
            app.want_add_node = true;
        }
        ImGui::SameLine();
        ImGui::TextDisabled("[%s]", detected_type_name(dt));
        toolbar_separator();

        // ---- Filtro / busqueda FTS5 (issue 0009) -----------------------
        {
            ImGui::SetNextItemWidth(220);
            ImGuiInputTextFlags ff = ImGuiInputTextFlags_EnterReturnsTrue;
            char prev[128];
            std::snprintf(prev, sizeof(prev), "%s", app.filter_query_buf);
            bool committed = ImGui::InputTextWithHint("##filterq",
                                TI_SEARCH " Search name/desc/tags...",
                                app.filter_query_buf,
                                sizeof(app.filter_query_buf), ff);
            bool typing_changed = std::strcmp(prev, app.filter_query_buf) != 0;
            if (typing_changed || committed) {
                app.filter_dirty = true;
                app.filter_dropdown_open = (app.filter_query_buf[0] != 0);
            }
            if (ImGui::IsItemActivated()) {
                app.filter_dropdown_open = (app.filter_query_buf[0] != 0);
            }

            // Dropdown bajo el input mientras hay texto.
            if (app.filter_dropdown_open && !app.filter_hits.empty()) {
                ImVec2 anchor = ImGui::GetItemRectMin();
                anchor.y      = ImGui::GetItemRectMax().y + 2.0f;
                ImGui::SetNextWindowPos(anchor);
                ImGui::SetNextWindowSize(ImVec2(360.0f, 0.0f));
                ImGui::SetNextWindowBgAlpha(0.97f);
                ImGuiWindowFlags wf = ImGuiWindowFlags_NoTitleBar
                                    | ImGuiWindowFlags_NoResize
                                    | ImGuiWindowFlags_NoMove
                                    | ImGuiWindowFlags_NoSavedSettings
                                    | ImGuiWindowFlags_AlwaysAutoResize
                                    | ImGuiWindowFlags_NoFocusOnAppearing;
                if (ImGui::Begin("##filter_dropdown", nullptr, wf)) {
                    for (const auto& h : app.filter_hits) {
                        char label[256];
                        std::snprintf(label, sizeof(label), "%s  [%s]##h_%s",
                                      h.name.c_str(),
                                      h.type_ref.empty() ? "?" : h.type_ref.c_str(),
                                      h.id.c_str());
                        if (ImGui::Selectable(label)) {
                            // Resolver a node_idx y pedir centrado.
                            uint64_t hh = fnv1a64_id(h.id.c_str());
                            int idx = app.graph
                                       ? app.graph->find_node_by_user_data(hh) : -1;
                            if (idx >= 0) {
                                app.filter_focus_target = idx;
                                if (app.viewport) {
                                    graph_viewport_clear_selection(*app.graph,
                                                                    *app.viewport);
                                    graph_viewport_add_to_selection(*app.graph,
                                                                     *app.viewport, idx);
                                }
                            }
                            app.filter_dropdown_open = false;
                        }
                    }
                }
                ImGui::End();
            }

            // Chips de tags activos.
            for (size_t i = 0; i < app.filter_tags.size();) {
                ImGui::SameLine();
                char chip[96];
                std::snprintf(chip, sizeof(chip), TI_TAG " %s " TI_X "##chip_%zu",
                              app.filter_tags[i].c_str(), i);
                if (button(chip, ButtonVariant::Subtle)) {
                    app.filter_tags.erase(app.filter_tags.begin() + i);
                    app.filter_dirty = true;
                    continue;
                }
                ++i;
            }

            ImGui::SameLine();
            ImGui::SetNextItemWidth(120);
            ImGuiInputTextFlags tf = ImGuiInputTextFlags_EnterReturnsTrue;
            if (ImGui::InputTextWithHint("##filtertag", "+ tag",
                                         app.filter_tag_input,
                                         sizeof(app.filter_tag_input), tf)) {
                if (app.filter_tag_input[0]) {
                    views_filter_add_tag(app, app.filter_tag_input);
                    app.filter_tag_input[0] = 0;
                }
            }

            ImGui::SameLine();
            ImGui::SetNextItemWidth(96);
            const char* mode_items[] = { "Highlight", "Hide" };
            int m = app.filter_mode;
            if (ImGui::Combo("##fmode", &m, mode_items, 2)) {
                if (m != app.filter_mode) {
                    app.filter_mode = m;
                    app.filter_dirty = true;
                }
            }
            ImGui::SameLine();
            bool can_clear = views_filter_active(app);
            if (!can_clear) ImGui::BeginDisabled();
            if (button(TI_FILTER_OFF " Clear", ButtonVariant::Subtle)) {
                views_filter_clear(app);
            }
            if (!can_clear) ImGui::EndDisabled();
        }
        toolbar_separator();

        // Layout: <name> ▾ — dropdown con todos los layouts + acciones.
        {
            char btn[96];
            std::snprintf(btn, sizeof(btn), TI_LAYOUT_GRID " Layout: %s",
                           k_layout_names[app.layout_mode % k_layout_count]);
            if (button(btn, ButtonVariant::Secondary)) {
                ImGui::OpenPopup("##layout_menu");
            }
            if (ImGui::BeginPopup("##layout_menu")) {
                ImGui::TextDisabled("Apply layout");
                ImGui::Separator();
                for (int i = 0; i < k_layout_count; ++i) {
                    bool is_cur = (i == app.layout_mode);
                    if (ImGui::MenuItem(k_layout_names[i], nullptr, is_cur)) {
                        app.layout_mode = i;
                        ++app.apply_layout_tick;
                    }
                }
                ImGui::Separator();
                if (ImGui::MenuItem(TI_LAYOUT_GRID " Reset positions (unpin + restart)")) {
                    app.want_unpin_all = true;
                    ++app.apply_layout_tick;
                }
                if (ImGui::MenuItem(TI_DEVICE_FLOPPY " Save current layout")) {
                    app.want_save_layout = true;
                }
                ImGui::EndPopup();
            }
        }
        ImGui::SameLine();

        // Physics ▶ / ⏸ — toggle visible. Solo afecta a layout 'force'.
        if (app.viewport) {
            const bool running = app.viewport->layout_running;
            const char* lbl = running ? TI_PLAYER_PAUSE " Physics: ON"
                                       : TI_PLAYER_PLAY  " Physics: OFF";
            ButtonVariant var = running ? ButtonVariant::Primary
                                          : ButtonVariant::Subtle;
            if (button(lbl, var)) {
                app.viewport->layout_running = !running;
            }
        }
        toolbar_separator();

        if (button(TI_FILTER " Filters...", ButtonVariant::Subtle)) {
            app.show_filters_modal = true;
        }
        if (button(TI_ARROWS_MAXIMIZE " Fit view", ButtonVariant::Subtle)) {
            app.want_fit = true;
        }
        if (button(TI_REFRESH " Reload", ButtonVariant::Subtle)) {
            app.want_reload = true;
        }
        toolbar_separator();

        ImGui::Checkbox("GPU layout", &app.use_gpu);
        ImGui::SameLine();
        ImGui::Checkbox("Labels", &app.labels_enabled);
    toolbar_end();
}

// ----------------------------------------------------------------------------
// Legend
// ----------------------------------------------------------------------------

void views_legend(AppState& app) {
    if (!app.panel_legend) return;
    if (!ImGui::Begin("Legend", &app.panel_legend)) {
        ImGui::End();
        return;
    }
    if (!app.graph) {
        ImGui::TextUnformatted("(no graph loaded)");
        ImGui::End();
        return;
    }

    GraphData& g = *app.graph;
    bool changed = false;

    ImGui::TextUnformatted("Entity types");
    ImGui::Separator();
    for (int i = 0; i < g.type_count && i < app.type_visible_n; ++i) {
        const EntityType& et = g.types[i];
        color_swatch(et.color);
        char id[32];
        std::snprintf(id, sizeof(id), "##t%d", i);
        bool v = app.type_visible[i];
        if (ImGui::Checkbox(id, &v)) {
            app.type_visible[i] = v;
            changed = true;
        }
        ImGui::SameLine();
        ImGui::TextUnformatted(et.name ? et.name : "(unnamed)");
    }

    if (g.rel_type_count > 0) {
        ImGui::Spacing();
        ImGui::TextUnformatted("Relation types");
        ImGui::Separator();
        for (int i = 0; i < g.rel_type_count && i < app.rel_type_visible_n; ++i) {
            const RelationType& rt = g.rel_types[i];
            color_swatch(rt.color);
            char id[32];
            std::snprintf(id, sizeof(id), "##r%d", i);
            bool v = app.rel_type_visible[i];
            if (ImGui::Checkbox(id, &v)) {
                app.rel_type_visible[i] = v;
                changed = true;
            }
            ImGui::SameLine();
            ImGui::TextUnformatted(rt.name ? rt.name : "(unnamed)");
        }
    }

    if (changed) views_apply_visibility(app);

    ImGui::End();
}

// ----------------------------------------------------------------------------
// Inspector helpers (issue 0008)
// ----------------------------------------------------------------------------

namespace {

const char* k_status_options[]  = { "active", "stale", "corrupted", "archived" };
constexpr int k_status_count    = 4;

int status_to_idx(const std::string& s) {
    for (int i = 0; i < k_status_count; ++i) {
        if (s == k_status_options[i]) return i;
    }
    return 0;
}

const EntitySpec* find_entity_spec(const ParsedTypes& pt, const char* name) {
    if (!name || !*name) return nullptr;
    auto eq_ci = [&](const std::string& a, const char* b) {
        if (a.size() != std::strlen(b)) return false;
        for (size_t i = 0; i < a.size(); ++i) {
            if (std::tolower((unsigned char)a[i]) !=
                std::tolower((unsigned char)b[i])) return false;
        }
        return true;
    };
    for (const auto& e : pt.entities) {
        if (eq_ci(e.name, name)) return &e;
    }
    return nullptr;
}

// Asegura que el buffer de descripcion tiene al menos `need` bytes (con NUL).
void ensure_desc_buf(std::vector<char>& buf, size_t need) {
    if (need < 4096) need = 4096;
    if (buf.size() < need) buf.assign(need, 0);
}

void copy_to_buf(char* buf, size_t n, const std::string& s) {
    if (n == 0) return;
    size_t k = std::min(n - 1, s.size());
    std::memcpy(buf, s.data(), k);
    buf[k] = 0;
}

} // namespace

void views_inspector_clear_draft(AppState& app) {
    app.insp_node_idx = -1;
    app.insp_entity_id.clear();
    app.insp_name_buf[0] = 0;
    app.insp_type_buf[0] = 0;
    app.insp_desc_buf.clear();
    app.insp_status_idx = 0;
    app.insp_field_keys.clear();
    app.insp_field_values.clear();
    app.insp_is_extra.clear();
    app.insp_tags.clear();
    app.insp_tag_input[0] = 0;
    app.insp_extra_key[0] = 0;
    app.insp_dirty = false;
    app.insp_show_unsaved = false;
    app.insp_pending_target = -1;
}

void views_inspector_refresh_caches(AppState& app) {
    app.insp_tag_suggestions.clear();
    if (!app.input_db_path.empty()) {
        entity_list_distinct_tags(app.input_db_path.c_str(),
                                   &app.insp_tag_suggestions);
    }
    app.insp_type_options.clear();
    for (const auto& e : app.parsed_types.entities) {
        if (!e.name.empty()) app.insp_type_options.push_back(e.name);
    }
    // Si el grafo trae tipos no presentes en el yaml, anadirlos para que el
    // combo no pierda opciones.
    if (app.graph) {
        for (int i = 0; i < app.graph->type_count; ++i) {
            const char* nm = app.graph->types[i].name;
            if (!nm || !*nm) continue;
            bool dup = false;
            for (const auto& s : app.insp_type_options) {
                if (s == nm) { dup = true; break; }
            }
            if (!dup) app.insp_type_options.emplace_back(nm);
        }
    }
}

void views_inspector_load_draft(AppState& app, int node_idx,
                                 const char* entity_id) {
    views_inspector_clear_draft(app);
    if (!entity_id || !*entity_id) return;
    EntityRecord rec;
    if (!entity_load_full(app.input_db_path.c_str(), entity_id, &rec)) return;

    app.insp_node_idx = node_idx;
    app.insp_entity_id = entity_id;
    copy_to_buf(app.insp_name_buf, sizeof(app.insp_name_buf), rec.name);
    copy_to_buf(app.insp_type_buf, sizeof(app.insp_type_buf), rec.type_ref);
    ensure_desc_buf(app.insp_desc_buf, rec.description.size() + 4096);
    std::memcpy(app.insp_desc_buf.data(), rec.description.data(),
                 rec.description.size());
    app.insp_status_idx = status_to_idx(rec.status);

    // Construye lista de fields: primero los del schema (en orden), luego
    // las extras (claves en metadata que no estan en el schema).
    const EntitySpec* spec = find_entity_spec(app.parsed_types, rec.type_ref.c_str());
    auto find_meta = [&](const std::string& key) -> const MetadataField* {
        for (const auto& m : rec.metadata) if (m.key == key) return &m;
        return nullptr;
    };
    if (spec) {
        for (const auto& f : spec->fields) {
            std::string val;
            if (auto m = find_meta(f.name)) val = m->value_str;
            app.insp_field_keys.push_back(f.name);
            app.insp_field_values.push_back(std::move(val));
            app.insp_is_extra.push_back(0);
        }
    }
    // Extras: claves en metadata que no estan en el schema.
    for (const auto& m : rec.metadata) {
        bool in_schema = false;
        if (spec) {
            for (const auto& f : spec->fields) {
                if (f.name == m.key) { in_schema = true; break; }
            }
        }
        if (!in_schema) {
            app.insp_field_keys.push_back(m.key);
            app.insp_field_values.push_back(m.value_str);
            app.insp_is_extra.push_back(1);
        }
    }
    app.insp_tags = std::move(rec.tags);
    app.insp_dirty = false;
}

EntityRecord views_inspector_build_record(const AppState& app) {
    EntityRecord r;
    r.id = app.insp_entity_id;
    r.name = app.insp_name_buf;
    r.type_ref = app.insp_type_buf;
    r.description = app.insp_desc_buf.empty()
                         ? std::string()
                         : std::string(app.insp_desc_buf.data());
    int sidx = app.insp_status_idx;
    if (sidx < 0 || sidx >= k_status_count) sidx = 0;
    r.status = k_status_options[sidx];
    r.tags = app.insp_tags;

    const EntitySpec* spec = find_entity_spec(app.parsed_types,
                                                app.insp_type_buf);
    auto kind_for_key = [&](const std::string& k) -> FieldKind {
        if (!spec) return FK_STRING;
        for (const auto& f : spec->fields) if (f.name == k) return f.kind;
        return FK_STRING;
    };
    for (size_t i = 0; i < app.insp_field_keys.size(); ++i) {
        const std::string& key = app.insp_field_keys[i];
        const std::string& val = app.insp_field_values[i];
        if (key.empty() || val.empty()) continue;
        bool is_extra = app.insp_is_extra[i] != 0;
        FieldKind kind = is_extra ? FK_STRING : kind_for_key(key);
        MetadataField mf;
        mf.key = key;
        mf.value_str = val;
        mf.is_string = (kind == FK_STRING || kind == FK_DATE
                          || kind == FK_URL || kind == FK_ENUM);
        // Para int/float/bool, value_str debe ser literal valido.
        if (kind == FK_BOOL) {
            std::string lv = val;
            std::transform(lv.begin(), lv.end(), lv.begin(),
                            [](unsigned char c){ return std::tolower(c); });
            mf.value_str = (lv == "true" || lv == "1" || lv == "yes")
                              ? "true" : "false";
        }
        r.metadata.push_back(std::move(mf));
    }
    return r;
}

// ----------------------------------------------------------------------------
// Inspector — render del panel editable
// ----------------------------------------------------------------------------

void views_inspector(AppState& app) {
    if (!app.panel_inspector) return;
    if (!ImGui::Begin("Inspector", &app.panel_inspector)) {
        ImGui::End();
        return;
    }
    if (!app.graph || !app.viewport) {
        ImGui::TextUnformatted("(no graph)");
        ImGui::End();
        return;
    }

    GraphData& g = *app.graph;
    const auto& sel = app.viewport->selection;

    if (sel.empty()) {
        if (app.insp_node_idx != -1 && !app.insp_dirty) {
            views_inspector_clear_draft(app);
        }
        ImGui::TextUnformatted("No selection.");
        ImGui::TextWrapped("Click a node, or shift+drag to lasso a region.");
        if (app.insp_dirty) {
            ImGui::Spacing();
            ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.7f, 0.3f, 1.0f));
            ImGui::TextWrapped("Cambios sin guardar en %s",
                               app.insp_entity_id.c_str());
            ImGui::PopStyleColor();
            if (fn_ui::button("Save", fn_ui::ButtonVariant::Primary)) {
                app.want_inspector_save = true;
            }
            ImGui::SameLine();
            if (fn_ui::button("Discard", fn_ui::ButtonVariant::Subtle)) {
                app.want_inspector_discard = true;
            }
        }
        ImGui::End();
        return;
    }

    if (sel.size() > 1) {
        if (app.insp_node_idx != -1 && !app.insp_dirty) {
            views_inspector_clear_draft(app);
        }
        ImGui::Text("%zu nodes selected", sel.size());
        ImGui::TextDisabled("(edicion multi-seleccion no soportada)");
        ImGui::Separator();
        for (size_t i = 0; i < sel.size() && i < 32; ++i) {
            int idx = sel[i];
            if (idx < 0 || idx >= g.node_count) continue;
            const GraphNode& n = g.nodes[idx];
            const char* lbl = graph::graph_label(&g, n.label_idx);
            ImGui::BulletText("[%d] %s", idx, lbl && *lbl ? lbl : "(unnamed)");
        }
        if (sel.size() > 32) ImGui::TextDisabled("(...%zu more)", sel.size() - 32);
        ImGui::End();
        return;
    }

    int idx = sel.front();
    if (idx < 0 || idx >= g.node_count) {
        ImGui::TextUnformatted("(invalid selection)");
        ImGui::End();
        return;
    }

    // Sincroniza draft con seleccion actual. Si hay cambios pendientes y la
    // seleccion cambio, mostramos un banner pidiendo Save/Discard antes de
    // cargar el nodo nuevo. Mientras tanto el draft sigue siendo el del
    // nodo anterior.
    if (app.insp_node_idx != idx) {
        if (app.insp_dirty && app.insp_node_idx >= 0
            && app.insp_node_idx < g.node_count) {
            // No cargar — esperar decision del usuario.
            ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.7f, 0.3f, 1.0f));
            ImGui::TextWrapped("Cambios sin guardar (nodo cambio en el viewport)."
                               " Save / Discard primero.");
            ImGui::PopStyleColor();
            ImGui::Separator();
        } else {
            // Resolver el sql id desde el viewport callback no esta aqui,
            // asi que main.cpp debe haber rellenado insp_pending_target +
            // input_db_path; pero la ruta normal es: main.cpp detecta cambio
            // de seleccion y llama views_inspector_load_draft. Aqui solo
            // limpiamos si el user_data no resuelve.
            // Simplemente intentamos cargar via insp_pending_target.
        }
    }

    // Si el draft no esta cargado para este idx, mostramos un placeholder.
    // main.cpp es responsable de llamar load_draft cuando seleccion cambia
    // y no hay dirty.
    if (app.insp_node_idx != idx) {
        ImGui::TextDisabled("Cargando nodo %d...", idx);
        ImGui::End();
        return;
    }

    const GraphNode& n = g.nodes[idx];
    ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
    ImGui::Text("id=%s   user_data=%llx   pos=(%.1f, %.1f)",
                app.insp_entity_id.c_str(),
                (unsigned long long)n.user_data, n.x, n.y);
    ImGui::PopStyleColor();
    ImGui::Separator();

    bool any_change = false;

    // ---- Identidad ----
    // Layout label-izquierda / input-derecha via 2-col table. El label
    // alineado al frame del input y el input estirado al ancho restante.
    ImGui::TextUnformatted("Identity");
    ImGui::Separator();

    if (ImGui::BeginTable("##insp_id", 2,
                           ImGuiTableFlags_SizingStretchProp |
                           ImGuiTableFlags_NoBordersInBody)) {
        ImGui::TableSetupColumn("k", ImGuiTableColumnFlags_WidthFixed, 90.0f);
        ImGui::TableSetupColumn("v", ImGuiTableColumnFlags_WidthStretch);

        // name
        ImGui::TableNextRow(); ImGui::TableNextColumn();
        ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("name");
        ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-FLT_MIN);
        if (ImGui::InputText("##name", app.insp_name_buf,
                              sizeof(app.insp_name_buf)))
            any_change = true;

        // type combo
        ImGui::TableNextRow(); ImGui::TableNextColumn();
        ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("type");
        ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-FLT_MIN);
        {
            int cur = -1;
            for (size_t i = 0; i < app.insp_type_options.size(); ++i) {
                if (app.insp_type_options[i] == app.insp_type_buf) {
                    cur = (int)i; break;
                }
            }
            if (ImGui::BeginCombo("##type", app.insp_type_buf)) {
                for (size_t i = 0; i < app.insp_type_options.size(); ++i) {
                    bool is_sel = (int)i == cur;
                    if (ImGui::Selectable(app.insp_type_options[i].c_str(), is_sel)) {
                        copy_to_buf(app.insp_type_buf, sizeof(app.insp_type_buf),
                                      app.insp_type_options[i]);
                        any_change = true;
                    }
                    if (is_sel) ImGui::SetItemDefaultFocus();
                }
                ImGui::EndCombo();
            }
        }

        // status combo
        ImGui::TableNextRow(); ImGui::TableNextColumn();
        ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("status");
        ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-FLT_MIN);
        if (ImGui::Combo("##status", &app.insp_status_idx,
                           k_status_options, k_status_count))
            any_change = true;

        ImGui::EndTable();
    }

    // description — multiline va debajo de su label, ocupando todo el
    // ancho. Con 60 px de alto entra ~3 lineas; el usuario hace scroll
    // dentro del input para textos mas largos.
    ImGui::Spacing();
    ImGui::TextUnformatted("description");
    if (app.insp_desc_buf.empty()) ensure_desc_buf(app.insp_desc_buf, 4096);
    if (ImGui::InputTextMultiline("##desc",
                                    app.insp_desc_buf.data(),
                                    app.insp_desc_buf.size(),
                                    ImVec2(-FLT_MIN, 60.0f)))
        any_change = true;

    // ---- Schema fields + Extras ----
    // Misma idea que Identity: 2-col table con label izquierda, input
    // derecha. Para extras añadimos un boton trash inline; para URLs un
    // boton Open. Ambos son SmallButton tras un input mas estrecho.
    if (!app.insp_field_keys.empty()) {
        ImGui::Spacing();
        ImGui::TextUnformatted("Fields");
        ImGui::Separator();
        const EntitySpec* spec = find_entity_spec(app.parsed_types,
                                                    app.insp_type_buf);

        if (ImGui::BeginTable("##insp_fields", 2,
                               ImGuiTableFlags_SizingStretchProp |
                               ImGuiTableFlags_NoBordersInBody)) {
            ImGui::TableSetupColumn("k", ImGuiTableColumnFlags_WidthFixed, 90.0f);
            ImGui::TableSetupColumn("v", ImGuiTableColumnFlags_WidthStretch);

            for (size_t i = 0; i < app.insp_field_keys.size(); ++i) {
                const std::string& key = app.insp_field_keys[i];
                std::string& val       = app.insp_field_values[i];
                bool is_extra          = app.insp_is_extra[i] != 0;
                ImGui::PushID((int)i);

                // Encuentra la FieldSpec si es del schema.
                const FieldSpec* fs = nullptr;
                if (!is_extra && spec) {
                    for (const auto& f : spec->fields) {
                        if (f.name == key) { fs = &f; break; }
                    }
                }

                FieldKind kind = fs ? fs->kind : FK_STRING;

                // Label izquierdo. Marca `*` si es required, prefijo
                // [extra] si es campo libre añadido por el usuario.
                ImGui::TableNextRow(); ImGui::TableNextColumn();
                ImGui::AlignTextToFramePadding();
                if (is_extra) {
                    ImGui::PushStyleColor(ImGuiCol_Text,
                        ImVec4(0.65f, 0.65f, 0.50f, 1.0f));
                    ImGui::Text("%s", key.c_str());
                    ImGui::PopStyleColor();
                } else if (fs && fs->required) {
                    ImGui::Text("%s *", key.c_str());
                } else {
                    ImGui::TextUnformatted(key.c_str());
                }

                // Input derecha. Reserva espacio para el trailing button
                // cuando aplique (URL Open, extras trash).
                ImGui::TableNextColumn();
                bool needs_trail_btn = is_extra ||
                    (kind == FK_URL && !val.empty() &&
                     (val.rfind("http://", 0) == 0 ||
                      val.rfind("https://", 0) == 0));
                ImGui::SetNextItemWidth(needs_trail_btn ? -32.0f : -FLT_MIN);

                char buf[1024];
                size_t k = std::min(sizeof(buf) - 1, val.size());
                std::memcpy(buf, val.data(), k);
                buf[k] = 0;

                bool changed = false;
                switch (kind) {
                    case FK_BOOL: {
                        bool b = (val == "true" || val == "1");
                        if (ImGui::Checkbox("##v", &b)) {
                            val = b ? "true" : "false";
                            changed = true;
                        }
                        break;
                    }
                    case FK_INT: {
                        int n = std::atoi(val.c_str());
                        if (ImGui::InputInt("##v", &n)) {
                            char nb[32]; std::snprintf(nb, sizeof(nb), "%d", n);
                            val = nb;
                            changed = true;
                        }
                        break;
                    }
                    case FK_FLOAT: {
                        double d = std::atof(val.c_str());
                        if (ImGui::InputDouble("##v", &d, 0.0, 0.0, "%.6g")) {
                            char nb[64]; std::snprintf(nb, sizeof(nb), "%.10g", d);
                            val = nb;
                            changed = true;
                        }
                        break;
                    }
                    case FK_ENUM: {
                        if (fs && !fs->enum_values.empty()) {
                            int cur = -1;
                            for (size_t e = 0; e < fs->enum_values.size(); ++e) {
                                if (fs->enum_values[e] == val) { cur = (int)e; break; }
                            }
                            if (ImGui::BeginCombo("##v", val.c_str())) {
                                for (size_t e = 0; e < fs->enum_values.size(); ++e) {
                                    bool is_sel = (int)e == cur;
                                    if (ImGui::Selectable(fs->enum_values[e].c_str(), is_sel)) {
                                        val = fs->enum_values[e];
                                        changed = true;
                                    }
                                    if (is_sel) ImGui::SetItemDefaultFocus();
                                }
                                ImGui::EndCombo();
                            }
                        } else {
                            if (ImGui::InputText("##v", buf, sizeof(buf))) {
                                val = buf;
                                changed = true;
                            }
                        }
                        break;
                    }
                    case FK_URL:
                        if (ImGui::InputText("##v", buf, sizeof(buf))) {
                            val = buf;
                            changed = true;
                        }
                        if (!val.empty() &&
                            (val.rfind("http://", 0) == 0 || val.rfind("https://", 0) == 0)) {
                            ImGui::SameLine();
                            if (ImGui::SmallButton(TI_EXTERNAL_LINK "##url")) {
#if defined(_WIN32)
                                std::string cmd = "start \"\" \"" + val + "\"";
#else
                                std::string cmd = "xdg-open '" + val + "' >/dev/null 2>&1 &";
#endif
                                int rc = std::system(cmd.c_str()); (void)rc;
                            }
                        }
                        break;
                    case FK_DATE:
                    case FK_STRING:
                    default:
                        if (ImGui::InputTextWithHint("##v",
                                                       kind == FK_DATE ? "YYYY-MM-DD" : "",
                                                       buf, sizeof(buf))) {
                            val = buf;
                            changed = true;
                        }
                        break;
                }

                if (is_extra) {
                    ImGui::SameLine();
                    if (ImGui::SmallButton(TI_TRASH "##rm")) {
                        app.insp_field_keys.erase(app.insp_field_keys.begin() + i);
                        app.insp_field_values.erase(app.insp_field_values.begin() + i);
                        app.insp_is_extra.erase(app.insp_is_extra.begin() + i);
                        ImGui::PopID();
                        any_change = true;
                        --i;
                        continue;
                    }
                }
                if (changed) any_change = true;
                ImGui::PopID();
            }
            ImGui::EndTable();
        }
    }

    // ---- Add extra ----
    ImGui::Spacing();
    ImGui::TextUnformatted("Extra field");
    ImGui::Separator();
    ImGui::SetNextItemWidth(160);
    ImGui::InputTextWithHint("##xkey", "key", app.insp_extra_key,
                              sizeof(app.insp_extra_key));
    ImGui::SameLine();
    if (fn_ui::button(TI_PLUS " Add", fn_ui::ButtonVariant::Subtle)) {
        if (app.insp_extra_key[0]) {
            // Evitar colision con keys ya presentes
            std::string k = app.insp_extra_key;
            bool dup = false;
            for (const auto& x : app.insp_field_keys) {
                if (x == k) { dup = true; break; }
            }
            if (!dup) {
                app.insp_field_keys.push_back(k);
                app.insp_field_values.emplace_back("");
                app.insp_is_extra.push_back(1);
                any_change = true;
            }
            app.insp_extra_key[0] = 0;
        }
    }

    // ---- Tags ----
    ImGui::Spacing();
    ImGui::TextUnformatted("Tags");
    ImGui::Separator();
    for (size_t i = 0; i < app.insp_tags.size(); ++i) {
        ImGui::PushID((int)i);
        ImGui::PushStyleColor(ImGuiCol_Button,        ImVec4(0.18f, 0.30f, 0.50f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.40f, 0.65f, 1.0f));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,  ImVec4(0.35f, 0.50f, 0.75f, 1.0f));
        std::string lbl = app.insp_tags[i] + "  x";
        bool removed = false;
        if (ImGui::SmallButton(lbl.c_str())) {
            app.insp_tags.erase(app.insp_tags.begin() + i);
            removed = true;
            any_change = true;
        } else if (ImGui::IsItemHovered()
                   && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
            // Right-click anade el tag como chip del filtro (issue 0009).
            views_filter_add_tag(app, app.insp_tags[i].c_str());
        }
        if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip("click: quitar  |  right-click: filtrar por tag");
        }
        ImGui::PopStyleColor(3);
        ImGui::PopID();
        if (removed) { --i; continue; }
        if ((i + 1) % 4 != 0) ImGui::SameLine();
    }
    if (!app.insp_tags.empty()) ImGui::NewLine();

    ImGui::SetNextItemWidth(160);
    ImGuiInputTextFlags tflags = ImGuiInputTextFlags_EnterReturnsTrue;
    bool commit_tag = ImGui::InputTextWithHint("##taginput", "add tag, Enter",
                                                  app.insp_tag_input,
                                                  sizeof(app.insp_tag_input),
                                                  tflags);
    ImGui::SameLine();
    if (fn_ui::button("Add tag", fn_ui::ButtonVariant::Subtle)) commit_tag = true;
    if (commit_tag && app.insp_tag_input[0]) {
        std::string t = app.insp_tag_input;
        bool dup = false;
        for (const auto& x : app.insp_tags) if (x == t) { dup = true; break; }
        if (!dup) {
            app.insp_tags.push_back(t);
            any_change = true;
        }
        app.insp_tag_input[0] = 0;
    }
    if (!app.insp_tag_suggestions.empty()) {
        ImGui::TextDisabled("(in db: %zu tags distintas)", app.insp_tag_suggestions.size());
        // Lista compacta clickable de las primeras 12 sugerencias.
        int shown = 0;
        for (const auto& s : app.insp_tag_suggestions) {
            if (shown >= 12) { ImGui::TextDisabled("..."); break; }
            // Ocultar las ya presentes
            bool already = false;
            for (const auto& x : app.insp_tags) if (x == s) { already = true; break; }
            if (already) continue;
            ImGui::SmallButton(s.c_str());
            if (ImGui::IsItemClicked()) {
                app.insp_tags.push_back(s);
                any_change = true;
            }
            ImGui::SameLine();
            ++shown;
        }
        if (shown > 0) ImGui::NewLine();
    }

    // ---- Footer: Save / Discard / Notes / Neighbors ----
    ImGui::Spacing();
    ImGui::Separator();
    if (any_change) app.insp_dirty = true;

    if (app.insp_dirty) {
        ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.7f, 0.3f, 1.0f));
        ImGui::TextUnformatted("(modified)");
        ImGui::PopStyleColor();
    }
    if (fn_ui::button(TI_DEVICE_FLOPPY " Save",
                       app.insp_dirty ? fn_ui::ButtonVariant::Primary
                                      : fn_ui::ButtonVariant::Subtle)) {
        app.want_inspector_save = true;
    }
    ImGui::SameLine();
    if (fn_ui::button("Discard", fn_ui::ButtonVariant::Subtle)) {
        app.want_inspector_discard = true;
    }
    ImGui::SameLine();
    if (fn_ui::button(TI_FILE_TEXT " Open notes", fn_ui::ButtonVariant::Subtle)) {
        app.want_open_note = true;
        app.open_note_target = idx;
    }

    // ---- Neighbors (read-only) ----
    ImGui::Spacing();
    ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
    ImGui::TextUnformatted("Neighbors:");
    ImGui::PopStyleColor();

    int neighbor_count = 0;
    for (int e = 0; e < g.edge_count && neighbor_count < 64; ++e) {
        const GraphEdge& edge = g.edges[e];
        int other = -1;
        const char* arrow = " ";
        if (edge.source == (uint32_t)idx)        { other = (int)edge.target; arrow = "->"; }
        else if (edge.target == (uint32_t)idx)   { other = (int)edge.source; arrow = "<-"; }
        if (other < 0 || other >= g.node_count) continue;
        const char* olbl = graph::graph_label(&g, g.nodes[other].label_idx);
        const char* rname = (edge.type_id < (uint16_t)g.rel_type_count &&
                              g.rel_types[edge.type_id].name)
                                ? g.rel_types[edge.type_id].name
                                : k_default_relation_name;
        char buf[256];
        std::snprintf(buf, sizeof(buf), "%s %s [%d] %s", arrow, rname, other,
                      olbl && *olbl ? olbl : "(unnamed)");
        if (ImGui::Selectable(buf)) {
            graph_viewport_clear_selection(g, *app.viewport);
            graph_viewport_add_to_selection(g, *app.viewport, other);
        }
        ++neighbor_count;
    }
    if (neighbor_count == 0)
        ImGui::TextDisabled("(none)");

    ImGui::End();
}

// ----------------------------------------------------------------------------
// Stats
// ----------------------------------------------------------------------------

void views_stats(AppState& app) {
    if (!app.panel_stats) return;
    if (!ImGui::Begin("Stats", &app.panel_stats)) {
        ImGui::End();
        return;
    }
    if (!app.graph || !app.viewport) {
        ImGui::TextUnformatted("(no graph)");
        ImGui::End();
        return;
    }
    int sel = (int)app.viewport->selection.size();
    ImGui::Text("nodes=%d  edges=%d  types=%d  rel_types=%d",
                app.graph->node_count, app.graph->edge_count,
                app.graph->type_count, app.graph->rel_type_count);
    ImGui::Text("fps=%d  energy=%.4f  selection=%d",
                app.fps_estimate, app.viewport->layout_energy, sel);
    ImGui::Text("layout=%s  mode=%s",
                k_layout_names[app.layout_mode % k_layout_count],
                app.use_gpu ? "GPU" : "CPU");
    ImGui::End();
}

// ----------------------------------------------------------------------------
// Note editor (markdown)
// ----------------------------------------------------------------------------

void views_note(AppState& app) {
    if (!app.panel_note) return;
    if (!ImGui::Begin(TI_FILE_TEXT " Note", &app.panel_note,
                       ImGuiWindowFlags_MenuBar)) {
        ImGui::End();
        return;
    }

    if (ImGui::BeginMenuBar()) {
        if (ImGui::MenuItem(TI_DEVICE_FLOPPY " Save", "Ctrl+S",
                             /*selected=*/false, app.note_dirty)) {
            app.want_save_note = true;
        }
        if (app.note_dirty) {
            ImGui::TextDisabled("(modified)");
        }
        ImGui::EndMenuBar();
    }

    if (app.note_node < 0) {
        ImGui::TextDisabled("Doble click sobre un nodo para abrir su nota.");
        ImGui::End();
        return;
    }

    ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
    ImGui::TextUnformatted("entity:");
    ImGui::PopStyleColor();
    ImGui::SameLine();
    ImGui::TextUnformatted(app.note_entity_label.empty()
                            ? "(unnamed)" : app.note_entity_label.c_str());

    ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
    ImGui::TextUnformatted("type:");
    ImGui::PopStyleColor();
    ImGui::SameLine();
    ImGui::TextUnformatted(app.note_entity_type.empty()
                            ? "(no-type)" : app.note_entity_type.c_str());

    ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
    ImGui::TextUnformatted("id:");
    ImGui::PopStyleColor();
    ImGui::SameLine();
    ImGui::TextUnformatted(app.note_entity_id.c_str());

    ImGui::Separator();

    if (app.note_buf.empty()) app.note_buf.resize(1024, 0);

    // Crece el buffer si esta cerca del limite.
    if (std::strlen(app.note_buf.data()) + 64 >= app.note_buf.size()) {
        app.note_buf.resize(app.note_buf.size() * 2, 0);
    }

    ImVec2 size = ImGui::GetContentRegionAvail();
    if (ImGui::InputTextMultiline("##note_md", app.note_buf.data(),
                                    app.note_buf.size(), size,
                                    ImGuiInputTextFlags_AllowTabInput)) {
        app.note_dirty = true;
    }
    if (ImGui::IsItemHovered() &&
        ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_S)) {
        app.want_save_note = true;
    }

    ImGui::End();
}

// ----------------------------------------------------------------------------
// Modals
// ----------------------------------------------------------------------------

bool views_filters_modal(AppState& app) {
    if (!app.show_filters_modal) return false;
    bool changed = false;
    if (fn_ui::modal_dialog_begin("Filters", &app.show_filters_modal,
                                   ImVec2(520, 0))) {
        if (!app.graph) {
            ImGui::TextUnformatted("(no graph)");
        } else {
            ImGui::TextUnformatted("Entity types");
            ImGui::Separator();
            int nt = app.type_visible_n;
            ImGui::Columns(2, "##fent", false);
            for (int i = 0; i < nt; ++i) {
                color_swatch(app.graph->types[i].color);
                char id[32];
                std::snprintf(id, sizeof(id), "##fe%d", i);
                bool v = app.type_visible[i];
                if (ImGui::Checkbox(id, &v)) {
                    app.type_visible[i] = v;
                    changed = true;
                }
                ImGui::SameLine();
                ImGui::TextUnformatted(app.graph->types[i].name ? app.graph->types[i].name : "?");
                ImGui::NextColumn();
            }
            ImGui::Columns(1);

            if (app.graph->rel_type_count > 0) {
                ImGui::Spacing();
                ImGui::TextUnformatted("Relation types");
                ImGui::Separator();
                int nr = app.rel_type_visible_n;
                ImGui::Columns(2, "##frel", false);
                for (int i = 0; i < nr; ++i) {
                    color_swatch(app.graph->rel_types[i].color);
                    char id[32];
                    std::snprintf(id, sizeof(id), "##fr%d", i);
                    bool v = app.rel_type_visible[i];
                    if (ImGui::Checkbox(id, &v)) {
                        app.rel_type_visible[i] = v;
                        changed = true;
                    }
                    ImGui::SameLine();
                    ImGui::TextUnformatted(app.graph->rel_types[i].name ? app.graph->rel_types[i].name : "?");
                    ImGui::NextColumn();
                }
                ImGui::Columns(1);
            }

            ImGui::Spacing();
            if (fn_ui::button("Show all", fn_ui::ButtonVariant::Subtle)) {
                for (int i = 0; i < app.type_visible_n; ++i)     app.type_visible[i]     = true;
                for (int i = 0; i < app.rel_type_visible_n; ++i) app.rel_type_visible[i] = true;
                changed = true;
            }
            ImGui::SameLine();
            if (fn_ui::button("Hide all", fn_ui::ButtonVariant::Subtle)) {
                for (int i = 0; i < app.type_visible_n; ++i)     app.type_visible[i]     = false;
                for (int i = 0; i < app.rel_type_visible_n; ++i) app.rel_type_visible[i] = false;
                changed = true;
            }
            ImGui::SameLine();
            if (fn_ui::button("Close", fn_ui::ButtonVariant::Primary)) {
                app.show_filters_modal = false;
            }
        }
    }
    fn_ui::modal_dialog_end();
    if (changed) views_apply_visibility(app);
    return changed;
}

// ----------------------------------------------------------------------------
// Modal: New project
// ----------------------------------------------------------------------------

bool views_new_project_modal(AppState& app) {
    if (!app.show_new_project_modal) return false;
    bool created = false;
    if (fn_ui::modal_dialog_begin("New project", &app.show_new_project_modal,
                                   ImVec2(480, 0))) {
        ImGui::TextWrapped(
            "Crea una subcarpeta en projects/ con su propio operations.db,"
            " types.yaml y graph_explorer.db.");
        ImGui::Spacing();
        fn_ui::text_input("Slug", app.new_project_buf, sizeof(app.new_project_buf),
                          "caso_aurgi");
        ImGui::TextDisabled("a-z, 0-9, '_' y '-' (max 64)");

        if (!app.new_project_error.empty()) {
            ImGui::Spacing();
            ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.4f, 1.0f));
            ImGui::TextWrapped("%s", app.new_project_error.c_str());
            ImGui::PopStyleColor();
        }

        ImGui::Spacing();
        if (fn_ui::button("Create", fn_ui::ButtonVariant::Primary)) {
            std::string err;
            if (!project_validate_slug(app.new_project_buf, &err)) {
                app.new_project_error = err;
            } else if (project_exists(app.new_project_buf)) {
                app.new_project_error = "ya existe un proyecto con ese slug";
            } else if (!project_create(app.new_project_buf, &err)) {
                app.new_project_error = err;
            } else {
                // Switch al recien creado
                app.want_switch_project = true;
                app.switch_project_target = app.new_project_buf;
                app.show_new_project_modal = false;
                app.new_project_error.clear();
                created = true;
            }
        }
        ImGui::SameLine();
        if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
            app.show_new_project_modal = false;
            app.new_project_error.clear();
        }
    }
    fn_ui::modal_dialog_end();
    return created;
}

bool views_open_modal(AppState& app) {
    if (!app.show_open_modal) return false;
    bool opened = false;
    if (fn_ui::modal_dialog_begin("Open file", &app.show_open_modal,
                                   ImVec2(520, 0))) {
        ImGui::TextWrapped("Path to operations.db (or any supported source).");
        ImGui::Spacing();
        fn_ui::text_input("Path", app.open_buf, sizeof(app.open_buf),
                          "apps/registry_dashboard/operations.db");
        ImGui::Spacing();
        if (fn_ui::button("Open", fn_ui::ButtonVariant::Primary)) {
            if (app.open_buf[0]) {
                app.want_open_file  = true;
                app.show_open_modal = false;
                opened = true;
            }
        }
        ImGui::SameLine();
        if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
            app.show_open_modal = false;
        }
    }
    fn_ui::modal_dialog_end();
    return opened;
}

// ----------------------------------------------------------------------------
// Table view (issue 0004)
// ----------------------------------------------------------------------------

void views_table_refresh_indices(AppState& app) {
    if (!app.graph) return;
    GraphData& g = *app.graph;

    // Degree map: user_data -> count.
    std::unordered_map<uint64_t, int> deg;
    deg.reserve((size_t)g.node_count * 2);
    for (int i = 0; i < g.edge_count; ++i) {
        const GraphEdge& e = g.edges[i];
        if (e.source < (uint32_t)g.node_count) deg[g.nodes[e.source].user_data]++;
        if (e.target < (uint32_t)g.node_count) deg[g.nodes[e.target].user_data]++;
    }

    for (auto& r : app.table_rows) {
        uint64_t h = fnv1a64_id(r.id.c_str());
        r.node_idx = g.find_node_by_user_data(h);
        auto it = deg.find(h);
        r.neighbors = (it == deg.end()) ? 0 : it->second;
    }
}

// ----------------------------------------------------------------------------
// Table view (issue 0004) — migrated to data_table::render (issue 0081-J).
//
// OLD: ImGui::BeginTable("##tablev", 6, ...) with manual sort/filter/clipper.
//      Per-column filter popups + chips toolbar + TabBar per type.
//      Click on row selected node in graph viewport.
//
// NEW: data_table::render() provides sort + filter + viz + stages.
//      AppState::table_dt_state persists the UI state between frames.
//      Viewport selection via Inspector (not via table row click).
//
// Removed helpers: render_one_table, render_table_headers_with_filters,
//   table_row_lt, table_row_field, table_col_name_by_id, k_table_cols,
//   TableColMeta, TableSortCtx, ci_contains, build_visible lambda.
//
// TODO(future): if viewport selection from table is wanted again, expose
//   a read-only "last hovered row id" from data_table::State or add a
//   post-render row-id lookup via ImGui::IsItemHovered on a named child.
// ----------------------------------------------------------------------------

void views_table(AppState& app) {
    if (!app.panel_table) return;
    if (!ImGui::Begin("Table", &app.panel_table)) {
        ImGui::End();
        return;
    }

    if (app.table_rows.empty()) {
        ImGui::TextDisabled("(no entities loaded)");
        ImGui::End();
        return;
    }

    // Construir TableInput a partir de app.table_rows.
    // Columnas: id, name, type, status, updated_at, neighbors.
    // Los datos se copian en backing strings cada frame (barato para N<50k
    // ya que son strings cortos y el vector se reutiliza por move).
    // data_table::render gestiona sort, filtros, busqueda y viz internamente.
    static const std::vector<std::string> k_headers = {
        "id", "name", "type", "status", "updated_at", "neighbors"
    };
    constexpr int k_ncols = 6;

    // cell_backing: propietario de los strings de la tabla.
    // Se reconstruye solo cuando table_cache_dirty (tras reload).
    // Para mantenerlo entre frames: miembro de AppState seria lo optimo,
    // pero para simplicidad lo hacemos static local (unica instancia OK
    // porque views_table se llama desde un unico lugar del render loop).
    static std::vector<std::string> s_cell_backing;
    static std::vector<const char*> s_cells;
    static int s_rows_cached = -1;

    // Rebuilding the cache on dirty flag or size change.
    if (app.table_cache_dirty || (int)app.table_rows.size() != s_rows_cached) {
        const int nrows = (int)app.table_rows.size();
        s_cell_backing.resize((size_t)nrows * k_ncols);
        s_cells.resize((size_t)nrows * k_ncols);
        for (int i = 0; i < nrows; ++i) {
            const auto& r = app.table_rows[i];
            s_cell_backing[(size_t)i * k_ncols + 0] = r.id;
            s_cell_backing[(size_t)i * k_ncols + 1] = r.name;
            s_cell_backing[(size_t)i * k_ncols + 2] = r.type_ref;
            s_cell_backing[(size_t)i * k_ncols + 3] = r.status;
            s_cell_backing[(size_t)i * k_ncols + 4] = r.updated_at;
            s_cell_backing[(size_t)i * k_ncols + 5] = std::to_string(r.neighbors);
        }
        for (size_t k = 0; k < s_cell_backing.size(); ++k)
            s_cells[k] = s_cell_backing[k].c_str();
        s_rows_cached = nrows;
        app.table_cache_dirty = false;
    }

    data_table::TableInput tbl;
    tbl.name    = "entities";
    tbl.headers = k_headers;
    tbl.types   = {
        data_table::ColumnType::String,  // id
        data_table::ColumnType::String,  // name
        data_table::ColumnType::String,  // type
        data_table::ColumnType::String,  // status
        data_table::ColumnType::String,  // updated_at
        data_table::ColumnType::Int,     // neighbors
    };
    tbl.cells = s_cells.empty() ? nullptr : s_cells.data();
    tbl.rows  = s_rows_cached;
    tbl.cols  = k_ncols;

    // Render con chrome completo (barra de chips + breadcrumb).
    // app.table_dt_state persiste entre frames.
    data_table::render("##tablev_dt", {tbl}, app.table_dt_state);

    ImGui::End();
}

// ----------------------------------------------------------------------------
// Table node UI fase 2 (issue 0011) — ventana expandida + import
// ----------------------------------------------------------------------------

AppState::NodeGroupsWindowState*
    views_node_groups_open(AppState& app,
                            const std::string& container_id,
                            NodeGroupsKind kind,
                            const char* ops_db)
{
    if (container_id.empty()) return nullptr;

    auto it = app.node_groups_windows.find(container_id);
    if (it != app.node_groups_windows.end()) {
        // Ya existe — no recargar metadata, solo pedir focus. El kind se
        // respeta tal como estaba (mover entre kinds para el mismo id no
        // tiene sentido en la UI actual).
        it->second.open          = true;
        it->second.focus_request = true;
        return &it->second;
    }

    auto& w = app.node_groups_windows[container_id];
    w.kind          = kind;
    w.open          = true;
    w.focus_request = true;
    w.page_dirty    = true;
    w.offset        = 0;
    w.page.clear();
    w.total_rows    = 0;
    w.last_error.clear();

    // Pre-popular meta segun el kind. Para kind=Group, las columnas son
    // fijas y conocidas — no hace falta tocar BD para descubrirlas, y
    // tampoco hay un nodo type='Table' que leer.
    w.meta = NodeGroupsMeta{};
    w.meta.entity_id = container_id;

    if (kind == NodeGroupsKind::Group) {
        w.meta.columns = {"id", "name", "type_ref", "status", "updated_at"};
        w.meta.id_column    = "id";
        w.meta.label_column = "name";
        // Best effort: leer el name del Group desde operations.db para que
        // el titulo de la ventana sea informativo. Si falla no bloquea.
        if (ops_db && *ops_db) {
            sqlite3* db = nullptr;
            if (sqlite3_open_v2(ops_db, &db, SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK) {
                sqlite3_stmt* st = nullptr;
                if (sqlite3_prepare_v2(db,
                        "SELECT name FROM entities WHERE id = ?",
                        -1, &st, nullptr) == SQLITE_OK) {
                    sqlite3_bind_text(st, 1, container_id.c_str(), -1, SQLITE_TRANSIENT);
                    if (sqlite3_step(st) == SQLITE_ROW) {
                        const unsigned char* p = sqlite3_column_text(st, 0);
                        if (p) w.meta.name = (const char*)p;
                    }
                    sqlite3_finalize(st);
                }
                sqlite3_close(db);
            }
        }
    } else {
        // kind=Table: cargar metadata real del nodo Table-typed. El path
        // tipico para entries creadas por views_node_groups_windows_sync
        // ya hace esto, pero si llaman a views_node_groups_open directo
        // queremos comportamiento equivalente.
        if (ops_db && *ops_db) {
            NodeGroupsMeta meta;
            if (node_groups_get_metadata(ops_db, container_id.c_str(), &meta)) {
                w.meta = std::move(meta);
            }
        }
    }
    return &w;
}

void views_node_groups_windows_sync(AppState& app, const char* ops_db) {
    if (!app.graph || !ops_db) return;
    GraphData& g = *app.graph;

    // Construir set de Tables expandidas con su metadata fresca.
    std::unordered_map<std::string, NodeGroupsMeta> live;
    for (int i = 0; i < g.node_count; ++i) {
        const GraphNode& n = g.nodes[i];
        if (n.type_id >= (uint16_t)g.type_count) continue;
        const EntityType& t = g.types[n.type_id];
        if (!t.name || std::strcmp(t.name, "Table") != 0) continue;
        // Resolver entity_id via SQL inverso por user_data hash es caro;
        // hacemos una pasada SQL para todas las Table entities.
    }
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(ops_db, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return;
    }
    // json_extract devuelve INTEGER 1 para JSON true; comparamos contra 1
    // (json('true') no es comparable directo — devuelve TEXT 'true').
    const char* sql =
        "SELECT id FROM entities "
        "WHERE type_ref = 'Table' AND json_extract(metadata,'$.expanded') = 1";
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return;
    }
    while (sqlite3_step(st) == SQLITE_ROW) {
        const unsigned char* p = sqlite3_column_text(st, 0);
        if (!p) continue;
        std::string id = (const char*)p;
        NodeGroupsMeta meta;
        if (node_groups_get_metadata(ops_db, id.c_str(), &meta)) {
            live.emplace(id, std::move(meta));
        }
    }
    sqlite3_finalize(st);
    sqlite3_close(db);

    // Quitar las que ya no estan expanded — pero solo las kind=Table.
    // Las kind=Group viven en operations.db con su propia condicion de
    // existencia (entity con type_ref='Group') y no deben tocarse aqui.
    for (auto it = app.node_groups_windows.begin(); it != app.node_groups_windows.end(); ) {
        if (it->second.kind == NodeGroupsKind::Group) { ++it; continue; }
        if (live.find(it->first) == live.end()) it = app.node_groups_windows.erase(it);
        else                                    ++it;
    }
    // Anadir las nuevas o refrescar metadata. Tras cualquier sync forzamos
    // page_dirty = true para que la siguiente iteracion del render relea
    // la pagina contra DuckDB (se evita asi mostrar pages obsoletas tras
    // promote/demote/import — donde el flag promoted de cada fila puede
    // haber cambiado).
    for (auto& kv : live) {
        auto& w = app.node_groups_windows[kv.first];
        bool was_present = !w.meta.entity_id.empty();
        w.kind = NodeGroupsKind::Table;  // expanded -> siempre Table
        w.meta = std::move(kv.second);
        w.open = true;
        w.page_dirty = true;
        if (!was_present) {
            w.offset = 0;
            w.page.clear();
            w.total_rows = 0;
        }
    }
}

void views_node_groups_window(AppState& app) {
    if (app.node_groups_windows.empty()) return;
    GraphData* g = app.graph;
    GraphViewportState* vp = app.viewport;

    for (auto& kv : app.node_groups_windows) {
        NodeGroupsMeta& m = kv.second.meta;
        AppState::NodeGroupsWindowState& w = kv.second;

        const bool is_group = (w.kind == NodeGroupsKind::Group);

        char title[160];
        if (is_group) {
            std::snprintf(title, sizeof(title), TI_TABLE " Group: %s##te_%s",
                           m.name.empty() ? "(unnamed)" : m.name.c_str(),
                           m.entity_id.c_str());
        } else {
            std::snprintf(title, sizeof(title), TI_TABLE " NodeGroups: %s##te_%s",
                           m.name.empty() ? "(unnamed)" : m.name.c_str(),
                           m.entity_id.c_str());
        }
        ImGui::SetNextWindowSize(ImVec2(640, 460), ImGuiCond_FirstUseEver);
        const bool focus_now = w.focus_request;
        if (focus_now) {
            // 0036c: forzar foco antes de Begin para que la window quede al
            // frente al abrirse o reabrirse desde doble click.
            ImGui::SetNextWindowFocus();
            w.focus_request = false;
        }
        if (!ImGui::Begin(title, &w.open)) { ImGui::End(); continue; }
        if (focus_now) {
            // Doble seguridad: tambien lo llamamos dentro del Begin/End por
            // si la window estaba ya abierta y ImGui ignora SetNextWindowFocus
            // en ese caso (raro, pero barato).
            ImGui::SetWindowFocus();
        }

        // Header de info (varia por kind)
        if (is_group) {
            ImGui::TextDisabled("group_id=%s · %lld rows",
                                  m.entity_id.c_str(),
                                  (long long)w.total_rows);
        } else {
            ImGui::TextDisabled("%s · %s · %lld rows",
                                  m.duckdb_path.c_str(), m.table_name.c_str(),
                                  (long long)w.total_rows);
        }
        // Issue 0036d: tooltip suave que explica el promote segun kind.
        if (is_group) {
            ImGui::TextDisabled("Promote: saca el nodo del grupo");
        } else {
            ImGui::TextDisabled(
                "Promote: convierte fila DuckDB en entidad del grafo");
        }
        if (!w.last_error.empty()) {
            ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
                                "ERROR: %s", w.last_error.c_str());
        }
        ImGui::Separator();

        // Tabla — layout depende del kind:
        //   Table: [id_column] + columns[] + [promoted]   (col_count = N+2)
        //   Group: columns[] + [promote]                  (col_count = N+1)
        //         (issue 0036d: ultima columna lleva un boton TI_ARROW_UP
        //          para sacar la entidad del grupo.)
        const int col_count = is_group
                                ? (int)m.columns.size() + 1
                                : (int)m.columns.size() + 2;
        ImGuiTableFlags tflags =
            ImGuiTableFlags_Borders   | ImGuiTableFlags_RowBg    |
            ImGuiTableFlags_ScrollY   | ImGuiTableFlags_Resizable |
            ImGuiTableFlags_SizingStretchProp;
        if (col_count > 0 && ImGui::BeginTable("##te_rows", col_count, tflags,
                                ImVec2(0, -ImGui::GetFrameHeightWithSpacing()))) {
            ImGui::TableSetupScrollFreeze(0, 1);
            if (is_group) {
                for (size_t i = 0; i < m.columns.size(); ++i) {
                    bool is_id = (i == 0);
                    ImGui::TableSetupColumn(m.columns[i].c_str(),
                        is_id ? ImGuiTableColumnFlags_WidthFixed
                              : ImGuiTableColumnFlags_WidthStretch,
                        is_id ? 160.0f : 0.0f);
                }
                // 0036d: columna extra para el boton Promote-out-of-group.
                ImGui::TableSetupColumn("promote",
                                         ImGuiTableColumnFlags_WidthFixed, 60.0f);
            } else {
                ImGui::TableSetupColumn(m.id_column.empty() ? "id" : m.id_column.c_str(),
                                         ImGuiTableColumnFlags_WidthFixed, 100.0f);
                for (const auto& c : m.columns) {
                    ImGui::TableSetupColumn(c.c_str(), ImGuiTableColumnFlags_WidthStretch);
                }
                ImGui::TableSetupColumn("promoted",
                                         ImGuiTableColumnFlags_WidthFixed, 80.0f);
            }
            ImGui::TableHeadersRow();

            for (int64_t i = 0; i < (int64_t)w.page.size(); ++i) {
                const NodeGroupsRow& row = w.page[i];
                ImGui::TableNextRow();
                ImGui::PushID((int)(w.offset + i));

                ImGui::TableSetColumnIndex(0);

                // Selectable spanning para que el doble-click y el right-click
                // funcionen sobre toda la fila, no solo el texto.
                // AllowOverlap obligatorio para que botones colocados despues
                // (Promote en kind=Group, ver issue 0036d) reciban sus clicks
                // en lugar de que el Selectable se los trague.
                ImGuiSelectableFlags sf = ImGuiSelectableFlags_SpanAllColumns
                                         | ImGuiSelectableFlags_AllowDoubleClick
                                         | ImGuiSelectableFlags_AllowOverlap;
                ImGui::Selectable(row.id.c_str(), false, sf);

                if (is_group) {
                    // 0036e: en kind=Group la fila YA es una entidad real del
                    // grafo. Single click → focus + select en viewport.
                    // Doble click tambien dispara focus (mismo efecto).
                    // Right click → menu contextual con focus.
                    if (ImGui::IsItemHovered()
                        && (ImGui::IsMouseClicked(0) || ImGui::IsMouseDoubleClicked(0))) {
                        app.want_focus_entity = true;
                        app.focus_entity_id   = row.id;
                    }
                    if (ImGui::IsItemHovered()) {
                        ImGui::SetTooltip("Click to focus entity in viewport");
                    }
                    if (ImGui::BeginPopupContextItem()) {
                        if (ImGui::MenuItem(TI_FOCUS " Focus in Inspector")) {
                            app.want_focus_entity = true;
                            app.focus_entity_id   = row.id;
                        }
                        ImGui::EndPopup();
                    }

                    // Render de las columnas (la 0 ya tiene el Selectable;
                    // el texto del id se ve en el propio Selectable).
                    for (size_t c = 1; c < m.columns.size(); ++c) {
                        ImGui::TableSetColumnIndex((int)c);
                        if (c < row.values.size())
                            ImGui::TextUnformatted(row.values[c].c_str());
                    }
                    // 0036d: boton Promote-out-of-group en la ultima columna.
                    ImGui::TableSetColumnIndex(col_count - 1);
                    if (ImGui::SmallButton(TI_ARROW_UP "##promote_grp")) {
                        app.want_clear_group_id_entity = true;
                        app.clear_group_id_entity_id   = row.id;
                    }
                    if (ImGui::IsItemHovered()) {
                        ImGui::SetTooltip("Promote out of group (move to canvas)");
                    }
                } else {
                    // kind=Table (DuckDB-backed).
                    // 0036e: click ramificado por estado de promocion:
                    //   - promovida → single click = focus en viewport.
                    //   - no promovida → single click = no-op + hint tooltip.
                    // El doble click sobre fila no promovida sigue lanzando
                    // el flujo de promote (legado de 0036c) por convenience.
                    bool is_promoted = !row.promoted_entity_id.empty();
                    if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0)) {
                        if (is_promoted) {
                            app.want_focus_entity = true;
                            app.focus_entity_id   = row.promoted_entity_id;
                        }
                        // else: no-op (hint mostrado via tooltip abajo).
                    }
                    if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
                        if (is_promoted) {
                            app.want_focus_entity = true;
                            app.focus_entity_id   = row.promoted_entity_id;
                        } else {
                            app.want_promote_row  = true;
                            app.promote_table_id  = m.entity_id;
                            app.promote_row_id    = row.id;
                        }
                    }
                    if (ImGui::IsItemHovered()) {
                        if (is_promoted) {
                            ImGui::SetTooltip("Click to focus entity in viewport");
                        } else {
                            ImGui::SetTooltip("promote first to focus\n(double click or right click to promote)");
                        }
                    }
                    if (ImGui::BeginPopupContextItem()) {
                        if (is_promoted) {
                            if (ImGui::MenuItem(TI_FOCUS " Focus in Inspector")) {
                                app.want_focus_entity = true;
                                app.focus_entity_id   = row.promoted_entity_id;
                            }
                            if (ImGui::MenuItem(TI_X " Demote (delete entity)")) {
                                app.want_demote_entity = true;
                                app.demote_entity_id   = row.promoted_entity_id;
                            }
                        } else {
                            if (ImGui::MenuItem(TI_PLUS " Promote to graph node")) {
                                app.want_promote_row  = true;
                                app.promote_table_id  = m.entity_id;
                                app.promote_row_id    = row.id;
                            }
                        }
                        ImGui::EndPopup();
                    }

                    for (size_t c = 0; c < m.columns.size(); ++c) {
                        ImGui::TableSetColumnIndex(1 + (int)c);
                        if (c < row.values.size())
                            ImGui::TextUnformatted(row.values[c].c_str());
                    }
                    ImGui::TableSetColumnIndex(col_count - 1);
                    if (is_promoted) {
                        ImGui::PushStyleColor(ImGuiCol_Text,
                                               ImVec4(0.6f, 0.95f, 0.6f, 1.0f));
                        ImGui::TextUnformatted("yes");
                        ImGui::PopStyleColor();
                    } else {
                        ImGui::TextDisabled("-");
                    }
                }
                ImGui::PopID();
            }
            ImGui::EndTable();
        }

        // Footer: paginacion manual (offset).
        bool has_prev = w.offset > 0;
        bool has_next = w.offset + (int64_t)w.page.size() < w.total_rows;
        if (!has_prev) ImGui::BeginDisabled();
        if (fn_ui::button(TI_ARROW_LEFT " Prev", fn_ui::ButtonVariant::Subtle)) {
            w.offset = std::max<int64_t>(0, w.offset - 200);
            w.page_dirty = true;
        }
        if (!has_prev) ImGui::EndDisabled();
        ImGui::SameLine();
        if (!has_next) ImGui::BeginDisabled();
        if (fn_ui::button("Next " TI_ARROW_RIGHT, fn_ui::ButtonVariant::Subtle)) {
            w.offset = w.offset + 200;
            w.page_dirty = true;
        }
        if (!has_next) ImGui::EndDisabled();
        ImGui::SameLine();
        ImGui::TextDisabled("rows %lld-%lld of %lld",
                              (long long)w.offset + (w.page.empty() ? 0 : 1),
                              (long long)(w.offset + (int64_t)w.page.size()),
                              (long long)w.total_rows);
        ImGui::SameLine();
        if (fn_ui::button(TI_REFRESH " Reload", fn_ui::ButtonVariant::Subtle)) {
            w.page_dirty = true;
        }

        ImGui::End();

        (void)g; (void)vp;
    }

    // Cerrar la ventana = expanded=false. Lo procesa main.cpp leyendo
    // node_groups_windows y comparando `open`.
}

bool views_import_dataset_modal(AppState& app) {
    if (!app.show_import_modal) return false;
    bool submitted = false;
    if (fn_ui::modal_dialog_begin("Import dataset", &app.show_import_modal,
                                    ImVec2(560, 0))) {
        ImGui::TextWrapped(
            "Crea una nueva tabla DuckDB importando un fichero CSV/Parquet/JSON. "
            "Tras el import, se anade un nodo Table apuntando a la nueva tabla.");
        ImGui::Spacing();
        fn_ui::text_input("File path",
                          app.import_path_buf,    sizeof(app.import_path_buf),
                          "tables/people.csv");
        fn_ui::text_input("DuckDB path",
                          app.import_duckdb_buf,  sizeof(app.import_duckdb_buf),
                          "tables/people.duckdb");
        fn_ui::text_input("Dest table",
                          app.import_table_buf,   sizeof(app.import_table_buf),
                          "people");
        fn_ui::text_input("Row type",
                          app.import_row_type_buf,sizeof(app.import_row_type_buf),
                          "Person");
        if (!app.import_error.empty()) {
            ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s",
                                 app.import_error.c_str());
        }
        ImGui::Spacing();
        if (fn_ui::button("Import", fn_ui::ButtonVariant::Primary)) {
            if (app.import_path_buf[0] && app.import_duckdb_buf[0]
                && app.import_table_buf[0]) {
                app.want_import = true;
                submitted = true;
            } else {
                app.import_error = "File path, DuckDB path y dest table son obligatorios.";
            }
        }
        ImGui::SameLine();
        if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
            app.show_import_modal = false;
            app.import_error.clear();
        }
    }
    fn_ui::modal_dialog_end();
    return submitted;
}

// ----------------------------------------------------------------------------
// Table node overlay (issue 0010)
// ----------------------------------------------------------------------------

void views_node_groups_overlay(AppState& app) {
    if (!app.graph || !app.viewport) return;
    GraphData& g = *app.graph;
    if (g.type_count == 0) return;

    const ImVec2 wmin = ImGui::GetItemRectMin();
    const ImVec2 wmax = ImGui::GetItemRectMax();
    const float cx = (wmin.x + wmax.x) * 0.5f;
    const float cy = (wmin.y + wmax.y) * 0.5f;
    ImDrawList* dl = ImGui::GetWindowDrawList();
    if (!dl) return;
    ImFont* font = ImGui::GetFont();

    // El cuadrado lo pinta el GPU (apply_types_yaml fija shape=SQUARE +
    // size=32 para tipos Table). Aqui solo añadimos un contador discreto
    // BAJO el cuadrado: "1000 rows".
    for (int i = 0; i < g.node_count; ++i) {
        const GraphNode& n = g.nodes[i];
        if (!(n.flags & NF_VISIBLE)) continue;
        if (n.type_id >= (uint16_t)g.type_count) continue;
        const EntityType& t = g.types[n.type_id];
        if (!t.name || std::strcmp(t.name, "Table") != 0) continue;

        const float zoom = app.viewport->zoom;
        const float vx = (n.x - app.viewport->cam_x) * zoom + cx;
        const float vy = (n.y - app.viewport->cam_y) * zoom + cy;
        if (vx < wmin.x - 100 || vx > wmax.x + 100) continue;
        if (vy < wmin.y - 100 || vy > wmax.y + 100) continue;

        int64_t count = -1;
        auto it = app.node_groups_counts.find(n.user_data);
        if (it != app.node_groups_counts.end()) count = it->second;
        if (count < 0) continue;

        char buf[64];
        std::snprintf(buf, sizeof(buf), "%lld rows", (long long)count);

        const float font_size = 12.0f;
        if (!font) continue;
        ImVec2 ts = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, buf);

        // Posicion: bajo el cuadrado. La mitad del shape en pixeles depende
        // del default_size del tipo y del zoom.
        const float half_h = (t.default_size * zoom) * 0.5f;
        const float gap    = 4.0f;
        const float tx     = vx - ts.x * 0.5f;
        const float ty     = vy + half_h + gap;

        // Pequeño bg semitransparente para que el texto sea legible sobre
        // grafos densos, sin parecer un chip.
        dl->AddRectFilled(ImVec2(tx - 4, ty - 1),
                           ImVec2(tx + ts.x + 4, ty + ts.y + 1),
                           IM_COL32(20, 25, 35, 180), 3.0f);
        dl->AddText(font, font_size, ImVec2(tx, ty),
                     IM_COL32(200, 220, 240, 230), buf);
    }
}

// ----------------------------------------------------------------------------
// Type Editor (issue 0007)
// ----------------------------------------------------------------------------

namespace {

const char* k_shape_names[] = {
    "(use type)", "circle", "square", "diamond", "hex", "triangle", "rounded_square",
};
constexpr int k_shape_count = (int)(sizeof(k_shape_names) / sizeof(k_shape_names[0]));

const char* k_style_names[] = {
    "(use type)", "solid", "dashed", "dotted",
};
constexpr int k_style_count = (int)(sizeof(k_style_names) / sizeof(k_style_names[0]));

const char* k_field_kind_names[] = {
    "string", "int", "float", "bool", "date", "url", "enum",
};
constexpr int k_field_kind_count = (int)(sizeof(k_field_kind_names) / sizeof(k_field_kind_names[0]));

ImVec4 abgr_to_imvec4_full(uint32_t c) {
    return ImVec4(
        (float)( c        & 0xFF) / 255.0f,
        (float)((c >>  8) & 0xFF) / 255.0f,
        (float)((c >> 16) & 0xFF) / 255.0f,
        (float)((c >> 24) & 0xFF) / 255.0f);
}

uint32_t imvec4_to_abgr(const ImVec4& v) {
    auto clamp01 = [](float x) { return x < 0 ? 0.f : (x > 1 ? 1.f : x); };
    uint8_t r = (uint8_t)(clamp01(v.x) * 255.0f + 0.5f);
    uint8_t g = (uint8_t)(clamp01(v.y) * 255.0f + 0.5f);
    uint8_t b = (uint8_t)(clamp01(v.z) * 255.0f + 0.5f);
    uint8_t a = (uint8_t)(clamp01(v.w) * 255.0f + 0.5f);
    return (uint32_t)r | ((uint32_t)g << 8) | ((uint32_t)b << 16) | ((uint32_t)a << 24);
}

} // namespace

void views_type_editor(AppState& app) {
    if (!app.panel_type_editor) return;
    if (!ImGui::Begin("Types", &app.panel_type_editor)) {
        ImGui::End();
        return;
    }

    if (ImGui::BeginTabBar("##te_tabs")) {
        // ---- Entities tab --------------------------------------------------
        if (ImGui::BeginTabItem("Entities")) {
            app.te_tab_idx = 0;
            ImGui::BeginChild("##te_left", ImVec2(160, 0), true);
            for (int i = 0; i < (int)app.types_draft.entities.size(); ++i) {
                ImGui::PushID(i);
                bool sel = (i == app.te_entity_idx);
                const auto& e = app.types_draft.entities[i];
                if (ImGui::Selectable(e.name.empty() ? "(unnamed)" : e.name.c_str(), sel)) {
                    app.te_entity_idx = i;
                }
                ImGui::PopID();
            }
            ImGui::EndChild();
            ImGui::SameLine();

            ImGui::BeginGroup();
            // +/- buttons
            if (fn_ui::button(TI_PLUS " Add entity type", fn_ui::ButtonVariant::Subtle)) {
                EntitySpec ne;
                ne.name = "NewType";
                app.types_draft.entities.push_back(std::move(ne));
                app.te_entity_idx = (int)app.types_draft.entities.size() - 1;
                app.types_dirty = true;
            }
            ImGui::SameLine();
            bool can_del = (app.te_entity_idx >= 0
                            && app.te_entity_idx < (int)app.types_draft.entities.size());
            if (!can_del) ImGui::BeginDisabled();
            if (fn_ui::button(TI_X " Delete", fn_ui::ButtonVariant::Subtle)) {
                app.te_pending_delete_e = app.te_entity_idx;
                app.te_pending_delete_r = -1;
                app.show_te_delete_modal = true;
            }
            if (!can_del) ImGui::EndDisabled();

            ImGui::Separator();

            if (can_del) {
                EntitySpec& e = app.types_draft.entities[app.te_entity_idx];

                // Name
                char namebuf[80];
                std::snprintf(namebuf, sizeof(namebuf), "%s", e.name.c_str());
                if (ImGui::InputText("Name", namebuf, sizeof(namebuf))) {
                    e.name = namebuf;
                    app.types_dirty = true;
                }

                // Color
                ImVec4 col = abgr_to_imvec4_full(e.color ? e.color : 0xFF888888u);
                if (ImGui::ColorEdit4("Color", (float*)&col,
                                       ImGuiColorEditFlags_NoInputs
                                     | ImGuiColorEditFlags_AlphaBar)) {
                    e.color = imvec4_to_abgr(col);
                    app.types_dirty = true;
                }

                // Shape
                int sh_idx = (e.shape <= SHAPE_ROUNDED_SQUARE) ? (int)e.shape : 0;
                if (ImGui::Combo("Shape", &sh_idx, k_shape_names, k_shape_count)) {
                    e.shape = (uint8_t)sh_idx;
                    app.types_dirty = true;
                }

                // Icon
                char ibuf[64];
                std::snprintf(ibuf, sizeof(ibuf), "%s", e.icon_name.c_str());
                if (ImGui::InputText("Icon (ti-*)", ibuf, sizeof(ibuf))) {
                    e.icon_name = ibuf;
                    e.icon_cp   = tabler_codepoint_by_name(ibuf);
                    app.types_dirty = true;
                }
                ImGui::SameLine();
                ImGui::TextDisabled("cp=0x%04X", e.icon_cp);

                // Principal field (combo entre fields existentes; permite "name" por default)
                std::vector<const char*> pf_opts;
                pf_opts.push_back("(name)");
                for (const auto& fs : e.fields) pf_opts.push_back(fs.name.c_str());
                int pf_idx = 0;
                for (int i = 1; i < (int)pf_opts.size(); ++i) {
                    if (e.principal_field == pf_opts[i]) { pf_idx = i; break; }
                }
                if (e.principal_field.empty()) pf_idx = 0;
                if (ImGui::Combo("Principal field", &pf_idx,
                                  pf_opts.data(), (int)pf_opts.size())) {
                    e.principal_field = (pf_idx == 0) ? std::string()
                                                       : std::string(pf_opts[pf_idx]);
                    app.types_dirty = true;
                }

                // Fields table
                ImGui::Spacing();
                ImGui::TextUnformatted("Fields");
                ImGui::Separator();

                if (ImGui::BeginTable("##te_fields", 5,
                                       ImGuiTableFlags_BordersInnerV
                                     | ImGuiTableFlags_RowBg
                                     | ImGuiTableFlags_SizingStretchProp)) {
                    ImGui::TableSetupColumn("name");
                    ImGui::TableSetupColumn("type",     ImGuiTableColumnFlags_WidthFixed, 90.0f);
                    ImGui::TableSetupColumn("required", ImGuiTableColumnFlags_WidthFixed, 70.0f);
                    ImGui::TableSetupColumn("values",   ImGuiTableColumnFlags_WidthStretch);
                    ImGui::TableSetupColumn("",         ImGuiTableColumnFlags_WidthFixed, 60.0f);
                    ImGui::TableHeadersRow();

                    int del_idx = -1;
                    int up_idx  = -1;
                    int dn_idx  = -1;
                    for (int fi = 0; fi < (int)e.fields.size(); ++fi) {
                        FieldSpec& fs = e.fields[fi];
                        ImGui::PushID(fi);
                        ImGui::TableNextRow();

                        // name
                        ImGui::TableSetColumnIndex(0);
                        ImGui::SetNextItemWidth(-FLT_MIN);
                        char fbuf[64];
                        std::snprintf(fbuf, sizeof(fbuf), "%s", fs.name.c_str());
                        if (ImGui::InputText("##fname", fbuf, sizeof(fbuf))) {
                            fs.name = fbuf;
                            app.types_dirty = true;
                        }

                        // type
                        ImGui::TableSetColumnIndex(1);
                        ImGui::SetNextItemWidth(-FLT_MIN);
                        int kidx = (int)fs.kind;
                        if (kidx < 0 || kidx >= k_field_kind_count) kidx = 0;
                        if (ImGui::Combo("##ftype", &kidx,
                                          k_field_kind_names, k_field_kind_count)) {
                            fs.kind = (FieldKind)kidx;
                            if (fs.kind != FK_ENUM) fs.enum_values.clear();
                            app.types_dirty = true;
                        }

                        // required
                        ImGui::TableSetColumnIndex(2);
                        if (ImGui::Checkbox("##freq", &fs.required)) app.types_dirty = true;

                        // values (CSV editable solo si enum)
                        ImGui::TableSetColumnIndex(3);
                        if (fs.kind == FK_ENUM) {
                            std::string csv;
                            for (size_t i = 0; i < fs.enum_values.size(); ++i) {
                                if (i) csv += ", ";
                                csv += fs.enum_values[i];
                            }
                            char vbuf[256];
                            std::snprintf(vbuf, sizeof(vbuf), "%s", csv.c_str());
                            ImGui::SetNextItemWidth(-FLT_MIN);
                            if (ImGui::InputText("##fval", vbuf, sizeof(vbuf))) {
                                fs.enum_values.clear();
                                std::string s = vbuf;
                                size_t start = 0;
                                while (start < s.size()) {
                                    size_t end = s.find(',', start);
                                    if (end == std::string::npos) end = s.size();
                                    std::string tok = s.substr(start, end - start);
                                    while (!tok.empty() && std::isspace((unsigned char)tok.front())) tok.erase(tok.begin());
                                    while (!tok.empty() && std::isspace((unsigned char)tok.back()))  tok.pop_back();
                                    if (!tok.empty()) fs.enum_values.push_back(std::move(tok));
                                    start = end + 1;
                                }
                                app.types_dirty = true;
                            }
                        } else {
                            ImGui::TextDisabled("-");
                        }

                        // controls
                        ImGui::TableSetColumnIndex(4);
                        if (ImGui::SmallButton(TI_X "##fd")) del_idx = fi;
                        ImGui::SameLine();
                        if (fi > 0 && ImGui::SmallButton("^##fu")) up_idx = fi;
                        ImGui::SameLine();
                        if (fi + 1 < (int)e.fields.size()
                            && ImGui::SmallButton("v##fdn")) dn_idx = fi;

                        ImGui::PopID();
                    }
                    ImGui::EndTable();

                    if (del_idx >= 0) {
                        e.fields.erase(e.fields.begin() + del_idx);
                        app.types_dirty = true;
                    } else if (up_idx > 0) {
                        std::swap(e.fields[up_idx - 1], e.fields[up_idx]);
                        app.types_dirty = true;
                    } else if (dn_idx >= 0 && dn_idx + 1 < (int)e.fields.size()) {
                        std::swap(e.fields[dn_idx], e.fields[dn_idx + 1]);
                        app.types_dirty = true;
                    }
                }

                if (fn_ui::button(TI_PLUS " Add field", fn_ui::ButtonVariant::Subtle)) {
                    FieldSpec ns;
                    ns.name = "field" + std::to_string(e.fields.size() + 1);
                    e.fields.push_back(std::move(ns));
                    app.types_dirty = true;
                }
            } else {
                ImGui::TextDisabled("(no entity selected)");
            }
            ImGui::EndGroup();
            ImGui::EndTabItem();
        }

        // ---- Relations tab -------------------------------------------------
        if (ImGui::BeginTabItem("Relations")) {
            app.te_tab_idx = 1;
            ImGui::BeginChild("##te_left_r", ImVec2(160, 0), true);
            for (int i = 0; i < (int)app.types_draft.relations.size(); ++i) {
                ImGui::PushID(i + 10000);
                bool sel = (i == app.te_relation_idx);
                const auto& r = app.types_draft.relations[i];
                if (ImGui::Selectable(r.name.empty() ? "(unnamed)" : r.name.c_str(), sel)) {
                    app.te_relation_idx = i;
                }
                ImGui::PopID();
            }
            ImGui::EndChild();
            ImGui::SameLine();

            ImGui::BeginGroup();
            if (fn_ui::button(TI_PLUS " Add relation type", fn_ui::ButtonVariant::Subtle)) {
                RelationSpec nr;
                nr.name = "new_relation";
                app.types_draft.relations.push_back(std::move(nr));
                app.te_relation_idx = (int)app.types_draft.relations.size() - 1;
                app.types_dirty = true;
            }
            ImGui::SameLine();
            bool can_del_r = (app.te_relation_idx >= 0
                              && app.te_relation_idx < (int)app.types_draft.relations.size());
            if (!can_del_r) ImGui::BeginDisabled();
            if (fn_ui::button(TI_X " Delete", fn_ui::ButtonVariant::Subtle)) {
                app.te_pending_delete_r = app.te_relation_idx;
                app.te_pending_delete_e = -1;
                app.show_te_delete_modal = true;
            }
            if (!can_del_r) ImGui::EndDisabled();

            ImGui::Separator();

            if (can_del_r) {
                RelationSpec& r = app.types_draft.relations[app.te_relation_idx];

                char rnbuf[80];
                std::snprintf(rnbuf, sizeof(rnbuf), "%s", r.name.c_str());
                if (ImGui::InputText("Name", rnbuf, sizeof(rnbuf))) {
                    r.name = rnbuf;
                    app.types_dirty = true;
                }
                ImVec4 rcol = abgr_to_imvec4_full(r.color ? r.color : 0xFF888888u);
                if (ImGui::ColorEdit4("Color", (float*)&rcol,
                                       ImGuiColorEditFlags_NoInputs
                                     | ImGuiColorEditFlags_AlphaBar)) {
                    r.color = imvec4_to_abgr(rcol);
                    app.types_dirty = true;
                }
                int st_idx = (r.style <= EDGE_DOTTED) ? (int)r.style : 0;
                if (ImGui::Combo("Style", &st_idx, k_style_names, k_style_count)) {
                    r.style = (uint8_t)st_idx;
                    app.types_dirty = true;
                }
            } else {
                ImGui::TextDisabled("(no relation selected)");
            }
            ImGui::EndGroup();
            ImGui::EndTabItem();
        }
        ImGui::EndTabBar();
    }

    // ---- Footer (Save / Reload) -------------------------------------------
    ImGui::Separator();
    if (!app.types_dirty) ImGui::BeginDisabled();
    if (fn_ui::button(TI_DEVICE_FLOPPY " Save to types.yaml",
                       fn_ui::ButtonVariant::Primary)) {
        app.want_types_save = true;
    }
    if (!app.types_dirty) ImGui::EndDisabled();
    ImGui::SameLine();
    if (fn_ui::button(TI_REFRESH " Reload from disk", fn_ui::ButtonVariant::Subtle)) {
        app.want_types_reload = true;
    }
    ImGui::SameLine();
    if (app.types_dirty) {
        ImGui::TextColored(ImVec4(0.95f, 0.6f, 0.2f, 1.0f), "* unsaved changes");
    } else {
        ImGui::TextDisabled("clean");
    }
    if (!app.types_save_error.empty()) {
        ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
                            app.types_save_error.c_str());
    }

    ImGui::End();
}

bool views_type_editor_delete_modal(AppState& app) {
    if (!app.show_te_delete_modal) return false;
    bool confirmed = false;
    if (fn_ui::modal_dialog_begin("Delete type", &app.show_te_delete_modal,
                                   ImVec2(440, 0))) {
        const char* tname = "?";
        const char* tkind = "?";
        if (app.te_pending_delete_e >= 0
            && app.te_pending_delete_e < (int)app.types_draft.entities.size()) {
            tname = app.types_draft.entities[app.te_pending_delete_e].name.c_str();
            tkind = "entity";
        } else if (app.te_pending_delete_r >= 0
                   && app.te_pending_delete_r < (int)app.types_draft.relations.size()) {
            tname = app.types_draft.relations[app.te_pending_delete_r].name.c_str();
            tkind = "relation";
        }
        ImGui::Text("Eliminar %s type \"%s\"?", tkind, tname);
        if (app.te_delete_use_count > 0) {
            ImGui::TextColored(ImVec4(0.95f, 0.6f, 0.2f, 1.0f),
                "Hay %d entidades en uso de este type_ref. Quedaran huerfanas hasta que las cambies.",
                app.te_delete_use_count);
        } else {
            ImGui::TextDisabled("Ninguna entidad usa este tipo actualmente.");
        }
        ImGui::Spacing();
        if (fn_ui::button("Delete", fn_ui::ButtonVariant::Primary)) {
            if (app.te_pending_delete_e >= 0
                && app.te_pending_delete_e < (int)app.types_draft.entities.size()) {
                app.types_draft.entities.erase(
                    app.types_draft.entities.begin() + app.te_pending_delete_e);
                if (app.te_entity_idx >= (int)app.types_draft.entities.size())
                    app.te_entity_idx = (int)app.types_draft.entities.size() - 1;
            } else if (app.te_pending_delete_r >= 0
                       && app.te_pending_delete_r < (int)app.types_draft.relations.size()) {
                app.types_draft.relations.erase(
                    app.types_draft.relations.begin() + app.te_pending_delete_r);
                if (app.te_relation_idx >= (int)app.types_draft.relations.size())
                    app.te_relation_idx = (int)app.types_draft.relations.size() - 1;
            }
            app.te_pending_delete_e = -1;
            app.te_pending_delete_r = -1;
            app.te_delete_use_count = 0;
            app.types_dirty = true;
            app.show_te_delete_modal = false;
            confirmed = true;
        }
        ImGui::SameLine();
        if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
            app.te_pending_delete_e = -1;
            app.te_pending_delete_r = -1;
            app.te_delete_use_count = 0;
            app.show_te_delete_modal = false;
        }
    }
    fn_ui::modal_dialog_end();
    return confirmed;
}

} // namespace ge
