// data_table — render UI completa de tabla TQL.
// Entry-point publica del stack data_table del registry.
// Issue 0081-H. Promovido desde cpp/apps/primitives_gallery/playground/tables/data_table.cpp
//
// Dependencias del registry:
//   - core/data_table_types.h     (tipos compartidos: State, TableInput, Stage, ...)
//   - core/compute_stage.h        (compute_stage_cpp_core)
//   - core/compute_pipeline.h     (compute_pipeline_cpp_core)
//   - core/compute_column_stats.h (compute_column_stats_cpp_core)
//   - core/auto_detect_type.h     (auto_detect_type_cpp_core)
//   - core/tql_emit.h             (tql_emit_cpp_core)
//   - core/tql_apply.h            (tql_apply_cpp_core)
//   - core/lua_engine.h           (lua_engine_cpp_core)
//   - core/join_tables.h          (join_tables_cpp_core)
//   - viz/viz_render.h            (viz_render_cpp_viz)
//
// Notas de deuda tecnica:
//   - tql_apply_cpp_core expone firma reducida; el playground usaba tql::apply
//     con cells/rows/orig_cols. Las llamadas internas de este archivo usan el
//     namespace tql:: del playground via include del tql_apply_cpp_core header.
//     Pendiente: ampliar tql_apply_cpp_core a la firma extendida (Wave 4/5 proposal).
//   - llm_anthropic (Ask AI modal, fase 11): incluido desde el playground (no en registry).
//     Pendiente: promover a cpp/functions/infra/llm_anthropic — deuda Wave 4.
//   - tql_to_sql (SQL transpile): incluido desde el playground. Pendiente: registry Wave 4.
//   - tql_duckdb (FN_TQL_DUCKDB): opcional, sin wrapper en registry todavia.

#include "data_table/data_table.h"

// Framework ImGui (via fn_framework)
#include "imgui.h"

// Registry Wave 1+2 includes (all resolved via fn_table_viz include path).
#include "core/lua_engine.h"
#include "core/tql_apply.h"
#include "core/tql_emit.h"
#include "core/tql_helpers.h"
#include "core/compute_stage.h"
#include "core/compute_pipeline.h"
#include "core/compute_column_stats.h"
#include "core/auto_detect_type.h"
#include "core/join_tables.h"
#include "core/tql_to_sql.h"
#include "viz/viz_render.h"

// llm_anthropic — Ask AI modal. Promoted to registry (cpp/functions/core/) in
// Wave 3.5. Real implementation linked by fn_table_viz; stub kept under
// !FN_LLM_ANTHROPIC for environments that build without the lib.
#ifdef FN_LLM_ANTHROPIC
#  include "core/llm_anthropic.h"
#endif

#ifdef FN_TQL_DUCKDB
#  include "tql_duckdb.h"
#endif

// fn::local_path — from fn_framework (framework/app_base.h).
// Required by the Ask AI modal and TQL save/load paths.
#include "app_base.h"


#include <algorithm>
#include <cfloat>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <fstream>
#include <string>
#include <unordered_map>

// icons_tabler.h: needed by draw_cell_custom icon renderer (issue 0081-N).
#include "core/icons_tabler.h"

// ---------------------------------------------------------------------------
// llm_anthropic stub (Wave 4 TODO: replace with infra/llm_anthropic.h)
// Provides no-op types/functions so fn_table_viz links without the playground.
// When FN_LLM_ANTHROPIC is defined the real header is included above instead.
// ---------------------------------------------------------------------------
#ifndef FN_LLM_ANTHROPIC
namespace llm_anthropic {
    enum class OutputMode { TQL, SQL };
    struct AskInput {
        std::string question;
        std::string tql_current;
        std::vector<std::string> col_names;
        std::vector<data_table::ColumnType> col_types;
        std::vector<std::string> joinable_names;
        OutputMode mode = OutputMode::TQL;
        std::string model;
        int max_tokens = 8192;
    };
    struct AskResult {
        std::string code;
        std::string raw;
        std::string error;
        int tokens_in  = 0;
        int tokens_out = 0;
    };
    inline AskResult ask(const AskInput&, const std::string& = "") {
        AskResult r;
        r.error = "llm_anthropic not available (stub). Build with FN_LLM_ANTHROPIC=1.";
        return r;
    }
} // namespace llm_anthropic
#endif // FN_LLM_ANTHROPIC

namespace data_table {

// ---------------------------------------------------------------------------
// Helpers from playground data_table_logic — declared static so they do not
// leak into the data_table namespace beyond this translation unit.
// Promoted inline to remove dependency on playground headers. Issue 0081-I.
// ---------------------------------------------------------------------------

// column_type_icon: returns a Tabler icon UTF-8 sequence for each ColumnType.
static const char* column_type_icon(ColumnType t) {
    switch (t) {
        case ColumnType::Auto:   return "\xef\xa4\x9d"; // TI_HELP_CIRCLE
        case ColumnType::String: return "\xef\x95\xa7"; // TI_ABC
        case ColumnType::Int:    return "\xef\x95\x94"; // TI_123
        case ColumnType::Float:  return "\xef\xa8\xa6"; // TI_DECIMAL
        case ColumnType::Bool:   return "\xee\xae\xa6"; // TI_CHECKBOX
        case ColumnType::Date:   return "\xee\xa9\x93"; // TI_CALENDAR
        case ColumnType::Json:   return "\xee\xaf\x8c"; // TI_BRACES
    }
    return "?";
}

// ---------------------------------------------------------------------------
// hex_to_imcolor: parses "#rrggbb" or "rrggbb" -> ImVec4 (alpha=1).
// Returns {-1,-1,-1,-1} on failure (caller should skip color push).
// ---------------------------------------------------------------------------
static ImVec4 hex_to_imcolor(const std::string& hex) {
    const char* p = hex.c_str();
    if (*p == '#') ++p;
    unsigned int r = 0, g = 0, b = 0;
    if (std::sscanf(p, "%02x%02x%02x", &r, &g, &b) != 3)
        return ImVec4(-1.f, -1.f, -1.f, -1.f);
    return ImVec4(r / 255.f, g / 255.f, b / 255.f, 1.f);
}

// parse_hex_color: parses "#rrggbb" or "#rrggbbaa" -> ImU32 with explicit alpha.
// Returns IM_COL32(128,128,128,255) on failure (visible fallback).
// v1.4.0 helper for CategoricalChip and ColorScale renderers.
// ---------------------------------------------------------------------------
static ImU32 parse_hex_color(const std::string& hex, float alpha = 1.0f) {
    const char* p = hex.c_str();
    if (*p == '#') ++p;
    unsigned int r = 0, g = 0, b = 0, a = 255;
    int parsed = std::sscanf(p, "%02x%02x%02x%02x", &r, &g, &b, &a);
    if (parsed < 3) return IM_COL32(128, 128, 128, 255);
    if (parsed == 3) {
        // alpha parameter overrides when no alpha in hex string
        a = (unsigned int)(alpha * 255.f + 0.5f);
    }
    return IM_COL32(r, g, b, a);
}

// lerp_color_along_stops: LERP between N color stops based on t in [0,1].
// Stops need not be sorted; function sorts a local copy first.
// If stops is empty, uses default green→amber→red gradient.
// alpha overrides the per-channel alpha of the result.
// v1.4.0 helper for ColorScale renderer.
// ---------------------------------------------------------------------------
static ImU32 lerp_color_along_stops(
    const std::vector<data_table::ColorStop>& stops, float t, float alpha)
{
    // Default green→amber→red when caller provides no stops.
    static const std::vector<data_table::ColorStop> kDefault = {
        {0.0f, "#22c55e"},
        {0.5f, "#f59e0b"},
        {1.0f, "#ef4444"},
    };
    const auto& sv = stops.empty() ? kDefault : stops;

    // Sort by position (copy; usually already sorted).
    std::vector<data_table::ColorStop> sorted_sv = sv;
    std::sort(sorted_sv.begin(), sorted_sv.end(),
              [](const data_table::ColorStop& a, const data_table::ColorStop& b){
                  return a.position < b.position;
              });

    // Clamp t.
    t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);

    // Edge cases: before first stop or after last stop.
    if (t <= sorted_sv.front().position)
        return parse_hex_color(sorted_sv.front().color, alpha);
    if (t >= sorted_sv.back().position)
        return parse_hex_color(sorted_sv.back().color, alpha);

    // Find surrounding stops.
    for (size_t i = 0; i + 1 < sorted_sv.size(); ++i) {
        const auto& lo = sorted_sv[i];
        const auto& hi = sorted_sv[i + 1];
        if (t >= lo.position && t <= hi.position) {
            float span = hi.position - lo.position;
            float f = (span > 1e-6f) ? (t - lo.position) / span : 0.f;
            ImVec4 ca = hex_to_imcolor(lo.color);
            ImVec4 cb = hex_to_imcolor(hi.color);
            if (ca.x < 0.f) ca = ImVec4(0.5f, 0.5f, 0.5f, 1.f);
            if (cb.x < 0.f) cb = ImVec4(0.5f, 0.5f, 0.5f, 1.f);
            float r = ca.x + f * (cb.x - ca.x);
            float g = ca.y + f * (cb.y - ca.y);
            float b = ca.z + f * (cb.z - ca.z);
            unsigned int ri = (unsigned int)(r * 255.f + 0.5f);
            unsigned int gi = (unsigned int)(g * 255.f + 0.5f);
            unsigned int bi = (unsigned int)(b * 255.f + 0.5f);
            unsigned int ai = (unsigned int)(alpha * 255.f + 0.5f);
            return IM_COL32(ri, gi, bi, ai);
        }
    }
    // Fallback (should not reach here).
    return parse_hex_color(sorted_sv.back().color, alpha);
}

// ---------------------------------------------------------------------------
// icon_name_to_glyph: static lookup of icon_name string -> Tabler glyph.
// Covers the ~30 most-used icons. Returns nullptr if not found.
// ---------------------------------------------------------------------------
static const char* icon_name_to_glyph(const std::string& name) {
    static const std::unordered_map<std::string, const char*> kMap = {
        {"TI_CHECK",         TI_CHECK},
        {"TI_X",             TI_X},
        {"TI_ALERT_CIRCLE",  TI_ALERT_CIRCLE},
        {"TI_CIRCLE_DOT",    TI_CIRCLE_DOT},
        {"TI_CLOCK",         TI_CLOCK},
        {"TI_LOADER",        TI_LOADER},
        {"TI_BAN",           TI_BAN},
        {"TI_PLAYER_PLAY",   TI_PLAYER_PLAY},
        {"TI_PLAYER_PAUSE",  TI_PLAYER_PAUSE},
        {"TI_PLAYER_STOP",   TI_PLAYER_STOP},
        {"TI_DATABASE",      TI_DATABASE},
        {"TI_SETTINGS",      TI_SETTINGS},
        {"TI_USER",          TI_USER},
        {"TI_USERS",         TI_USERS},
        {"TI_FILE",          TI_FILE},
        {"TI_FOLDER",        TI_FOLDER},
        {"TI_REFRESH",       TI_REFRESH},
        {"TI_BOLT",          TI_BOLT},
        {"TI_INFO_CIRCLE",   TI_INFO_CIRCLE},
        {"TI_ARROW_UP",      TI_ARROW_UP},
        {"TI_ARROW_DOWN",    TI_ARROW_DOWN},
        {"TI_ARROW_RIGHT",   TI_ARROW_RIGHT},
        {"TI_ARROW_LEFT",    TI_ARROW_LEFT},
        {"TI_DOTS",          TI_DOTS},
        {"TI_EYE",           TI_EYE},
        {"TI_EYE_OFF",       TI_EYE_OFF},
        {"TI_EDIT",          TI_EDIT},
        {"TI_TRASH",         TI_TRASH},
        {"TI_COPY",          TI_COPY},
        {"TI_EXTERNAL_LINK", TI_EXTERNAL_LINK},
    };
    auto it = kMap.find(name);
    return it != kMap.end() ? it->second : nullptr;
}

// ---------------------------------------------------------------------------
// draw_cell_custom: render a cell using the declarative ColumnSpec.
// Called only when spec.renderer != CellRenderer::Text.
// Issue 0081-N, v1.1.0. Phase 2 (v1.2.0): Button renderer + tooltip.
//
// events_out: if non-null and renderer==Button, ButtonClick is pushed on click.
// row_idx / col_idx: logical indices in the TableInput (for event payload).
// ---------------------------------------------------------------------------
static void draw_cell_custom(const ColumnSpec& spec, const char* value,
                              int row_idx, int col_idx,
                              std::vector<TableEvent>* events_out) {
    if (!value) value = "";

    switch (spec.renderer) {
        case CellRenderer::Badge: {
            // Find a matching badge rule (exact match, case-sensitive).
            const BadgeRule* matched = nullptr;
            for (const auto& br : spec.badges) {
                if (br.value == value) { matched = &br; break; }
            }
            if (matched) {
                ImVec4 col = hex_to_imcolor(matched->color_hex);
                const char* label = matched->label.empty() ? value : matched->label.c_str();
                if (col.x >= 0.f) {
                    ImGui::PushStyleColor(ImGuiCol_Header, col);
                    // Slightly brighter on hover to provide feedback.
                    ImVec4 hover_col = ImVec4(
                        std::min(col.x + 0.1f, 1.f),
                        std::min(col.y + 0.1f, 1.f),
                        std::min(col.z + 0.1f, 1.f),
                        col.w);
                    ImGui::PushStyleColor(ImGuiCol_HeaderHovered, hover_col);
                    ImGui::PushStyleColor(ImGuiCol_HeaderActive,  hover_col);
                    // Issue 0081-O.7: removed SpanAllColumns — badge hover must not
                    // illuminate the entire row, only the badge cell.
                    ImGui::Selectable(label, false);
                    ImGui::PopStyleColor(3);
                } else {
                    ImGui::TextUnformatted(label);
                }
            } else {
                ImGui::TextUnformatted(value);
            }
            break;
        }

        case CellRenderer::Progress: {
            double v_raw = 0.0;
            if (!parse_number(value, v_raw)) {
                ImGui::TextUnformatted(value);
                break;
            }
            float fv = (float)v_raw;
            if (spec.progress_scale_100) fv /= 100.f;
            fv = fv < 0.f ? 0.f : (fv > 1.f ? 1.f : fv);
            bool has_color = !spec.progress_color_hex.empty();
            ImVec4 bar_col;
            if (has_color) {
                bar_col = hex_to_imcolor(spec.progress_color_hex);
                if (bar_col.x < 0.f) has_color = false;
            }
            if (has_color) ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bar_col);
            ImGui::ProgressBar(fv, ImVec2(-1.f, 0.f));
            if (has_color) ImGui::PopStyleColor();
            break;
        }

        case CellRenderer::Duration: {
            double v_raw = 0.0;
            if (!parse_number(value, v_raw)) {
                ImGui::TextUnformatted(value);
                break;
            }
            float ms = (float)v_raw;
            ImVec4 text_col;
            if (ms <= spec.duration_warn_ms) {
                text_col = hex_to_imcolor("#22c55e");  // green
            } else if (ms <= spec.duration_error_ms) {
                text_col = hex_to_imcolor("#f59e0b");  // yellow
            } else {
                text_col = hex_to_imcolor("#ef4444");  // red
            }
            char buf[32];
            std::snprintf(buf, sizeof(buf), "%.0f ms", ms);
            ImGui::TextColored(text_col, "%s", buf);
            break;
        }

        case CellRenderer::Icon: {
            const char* glyph = nullptr;
            ImVec4 icon_col(-1.f, -1.f, -1.f, -1.f);
            for (const auto& entry : spec.icon_map) {
                if (entry.value == value) {
                    glyph    = icon_name_to_glyph(entry.icon_name);
                    if (!entry.color_hex.empty())
                        icon_col = hex_to_imcolor(entry.color_hex);
                    break;
                }
            }
            if (glyph) {
                if (icon_col.x >= 0.f)
                    ImGui::TextColored(icon_col, "%s", glyph);
                else
                    ImGui::TextUnformatted(glyph);
            } else {
                ImGui::TextUnformatted(value);
            }
            break;
        }

        case CellRenderer::Button: {
            // Skip empty cell values — app decides when to show a button.
            if (value[0] == '\0') break;
            const char* label = spec.button_label.empty() ? value : spec.button_label.c_str();
            bool has_color = !spec.button_color_hex.empty();
            if (has_color) {
                ImVec4 btn_col = hex_to_imcolor(spec.button_color_hex);
                if (btn_col.x >= 0.f) {
                    ImGui::PushStyleColor(ImGuiCol_Button, btn_col);
                    ImVec4 hov = ImVec4(
                        std::min(btn_col.x + 0.12f, 1.f),
                        std::min(btn_col.y + 0.12f, 1.f),
                        std::min(btn_col.z + 0.12f, 1.f),
                        btn_col.w);
                    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hov);
                    ImGui::PushStyleColor(ImGuiCol_ButtonActive,  hov);
                } else {
                    has_color = false;
                }
            }
            // Unique button ID: combines label + row + col to avoid ImGui ID
            // collisions when the same label appears in multiple rows.
            char btn_id[128];
            std::snprintf(btn_id, sizeof(btn_id), "%s##btn_%d_%d",
                          label, row_idx, col_idx);
            if (ImGui::SmallButton(btn_id) && events_out) {
                TableEvent ev;
                ev.kind      = TableEventKind::ButtonClick;
                ev.row       = row_idx;
                ev.col       = col_idx;
                ev.column_id = spec.id;
                ev.action_id = spec.button_action;
                ev.value     = value;
                events_out->push_back(std::move(ev));
            }
            if (has_color) ImGui::PopStyleColor(3);
            break;
        }

        case CellRenderer::Dots: {
            // Parse cell value as separator-delimited tokens.
            std::string s = value;
            std::vector<std::string> tokens;
            {
                std::string cur;
                char sep = spec.dots_separator ? spec.dots_separator : ',';
                for (char c : s) {
                    if (c == sep) { tokens.push_back(cur); cur.clear(); }
                    else cur.push_back(c);
                }
                if (!cur.empty()) tokens.push_back(cur);
            }
            int limit = (spec.dots_max > 0)
                        ? std::min((int)tokens.size(), spec.dots_max)
                        : (int)tokens.size();

            // Draw filled circles via ImDrawList — font-independent, scales with font size.
            // BadgeRule.label is ignored for Dots (only relevant for Badge renderer).
            float font_h    = ImGui::GetTextLineHeight();
            float radius    = (spec.dots_glyph_size > 0.f ? spec.dots_glyph_size : font_h * 0.32f);
            float spacing   = radius * 2.3f;
            ImVec2 origin   = ImGui::GetCursorScreenPos();
            float dot_y     = origin.y + font_h * 0.5f;
            ImDrawList* dl  = ImGui::GetWindowDrawList();
            ImU32 default_col = IM_COL32(110, 110, 125, 255);  // muted grey

            for (int t = 0; t < limit; ++t) {
                ImU32 col = default_col;
                for (const auto& br : spec.badges) {
                    if (br.value == tokens[t]) {
                        ImVec4 c = hex_to_imcolor(br.color_hex);
                        if (c.x >= 0.f) col = ImGui::ColorConvertFloat4ToU32(c);
                        break;
                    }
                }
                float cx = origin.x + radius + t * spacing;
                dl->AddCircleFilled(ImVec2(cx, dot_y), radius, col, 18);
            }

            // Reserve cursor space so layout flows correctly.
            float total_w = (limit > 0 ? (limit * spacing) : 0.f);
            ImGui::Dummy(ImVec2(total_w, font_h));

            // Hit-test for tooltip per dot.
            if (spec.tooltip_on_hover && ImGui::IsItemHovered()) {
                ImVec2 mp = ImGui::GetMousePos();
                for (int t = 0; t < limit; ++t) {
                    float cx = origin.x + radius + t * spacing;
                    float dx = mp.x - cx, dy = mp.y - dot_y;
                    if (dx*dx + dy*dy <= radius * radius * 1.5f) {
                        ImGui::SetTooltip("%s", tokens[t].c_str());
                        break;
                    }
                }
            }

            if (spec.dots_show_count) {
                ImGui::SameLine(0, 6.0f);
                ImGui::TextDisabled("(%d)", (int)tokens.size());
            }
            break;
        }

        case CellRenderer::CategoricalChip: {
            // Draw a filled circle to the LEFT of the cell text.
            // Color determined by matching value against chips rules.
            // Always visible (not hover-only). If no rule matches, no dot.
            // v1.4.0.
            const ChipRule* matched_chip = nullptr;
            for (const auto& cr : spec.chips) {
                if (cr.match == value) { matched_chip = &cr; break; }
            }
            if (matched_chip) {
                float font_h   = ImGui::GetTextLineHeight();
                ImVec2 cursor  = ImGui::GetCursorScreenPos();
                float radius   = 4.0f;
                float cy       = cursor.y + font_h * 0.5f;
                float cx       = cursor.x + radius;
                ImDrawList* dl = ImGui::GetWindowDrawList();
                ImU32 chip_col = parse_hex_color(matched_chip->color, 1.0f);
                dl->AddCircleFilled(ImVec2(cx, cy), radius, chip_col, 18);
                // Advance cursor past the dot + 4px gap.
                ImGui::Dummy(ImVec2(radius * 2.0f + 4.0f, font_h));
                ImGui::SameLine(0, 0);
            }
            ImGui::TextUnformatted(value);
            break;
        }

        case CellRenderer::ColorScale: {
            // Paint cell background with an interpolated color from N-stop gradient.
            // Numeric value mapped to t = (val - range_min) / (range_max - range_min).
            // Clamped to [0,1]. Non-parseable values render as plain text.
            // v1.4.0.
            double v_raw = 0.0;
            if (!parse_number(value, v_raw)) {
                ImGui::TextUnformatted(value);
                break;
            }
            double span = spec.range_max - spec.range_min;
            float t = 0.f;
            if (span > 1e-12) {
                t = (float)((v_raw - spec.range_min) / span);
            }
            // Clamp.
            t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);

            // Paint background rect covering the full cell area.
            ImU32 bg_col = lerp_color_along_stops(spec.range_stops, t, spec.range_alpha);
            ImVec2 cell_min = ImGui::GetCursorScreenPos();
            // Use cell size: full column width × row height.
            float row_h = ImGui::GetTextLineHeight();
            float col_w = ImGui::GetContentRegionAvail().x;
            ImVec2 cell_max = ImVec2(cell_min.x + col_w, cell_min.y + row_h);
            ImGui::GetWindowDrawList()->AddRectFilled(cell_min, cell_max, bg_col);

            // Draw text on top.
            ImGui::TextUnformatted(value);
            break;
        }

        default:
            // CellRenderer::Text or unknown — plain text.
            ImGui::TextUnformatted(value);
            break;
    }

    // Tooltip: show on hover if tooltip_on_hover is set (non-Dots renderers).
    // For Dots, per-dot tooltips are handled inline above.
    // "auto" shows the raw cell value (useful for truncated text columns).
    if (spec.renderer != CellRenderer::Dots &&
        spec.tooltip_on_hover && ImGui::IsItemHovered()) {
        const char* tip = (spec.tooltip == "auto") ? value : spec.tooltip.c_str();
        if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
    }
}

// compare: cell-level comparison supporting all Op variants.
// Uses parse_number (from auto_detect_type.h) for numeric comparisons.
static bool compare(const char* a, const char* b, Op op) {
    if (!a) a = "";
    if (!b) b = "";
    switch (op) {
        case Op::Contains:    return std::strstr(a, b) != nullptr;
        case Op::NotContains: return std::strstr(a, b) == nullptr;
        case Op::StartsWith: {
            size_t lb = std::strlen(b);
            return std::strncmp(a, b, lb) == 0;
        }
        case Op::EndsWith: {
            size_t la = std::strlen(a), lb = std::strlen(b);
            return lb <= la && std::strcmp(a + la - lb, b) == 0;
        }
        default: break;
    }
    double na, nb;
    bool numeric = parse_number(a, na) && parse_number(b, nb);
    if (numeric) {
        switch (op) {
            case Op::Eq:  return na == nb;
            case Op::Neq: return na != nb;
            case Op::Gt:  return na >  nb;
            case Op::Gte: return na >= nb;
            case Op::Lt:  return na <  nb;
            case Op::Lte: return na <= nb;
            default: break;
        }
    }
    int c = std::strcmp(a, b);
    switch (op) {
        case Op::Eq:  return c == 0;
        case Op::Neq: return c != 0;
        case Op::Gt:  return c >  0;
        case Op::Gte: return c >= 0;
        case Op::Lt:  return c <  0;
        case Op::Lte: return c <= 0;
        default: break;
    }
    return false;
}

// make_drill_filter: creates an Op::Eq filter on col_idx with the given value.
static Filter make_drill_filter(int col_idx, const std::string& value) {
    Filter f;
    f.col   = col_idx;
    f.op    = Op::Eq;
    f.value = value;
    return f;
}

// apply_drill_step: inserts step.added into st at the recorded position.
static bool apply_drill_step(State& st, const DrillStep& step) {
    if (step.target_stage < 0 || step.target_stage >= (int)st.stages.size()) return false;
    Stage& s = st.stages[step.target_stage];
    int pos = step.filter_pos;
    if (pos < 0 || pos > (int)s.filters.size()) return false;
    s.filters.insert(s.filters.begin() + pos, step.added);
    st.active_stage = step.target_stage;
    return true;
}

// undo_drill_step: removes the filter inserted by apply_drill_step.
static bool undo_drill_step(State& st, const DrillStep& step) {
    if (step.target_stage < 0 || step.target_stage >= (int)st.stages.size()) return false;
    Stage& s = st.stages[step.target_stage];
    int pos = step.filter_pos;
    if (pos < 0 || pos >= (int)s.filters.size()) return false;
    s.filters.erase(s.filters.begin() + pos);
    if (step.prev_active_stage >= 0 && step.prev_active_stage < (int)st.stages.size())
        st.active_stage = step.prev_active_stage;
    return true;
}

// drill_up: decrements active_stage by 1 if possible.
static bool drill_up(State& st) {
    if (st.stages.empty()) return false;
    if (st.active_stage <= 0) return false;
    st.active_stage -= 1;
    return true;
}

// row_to_tsv: serializes a single row to a two-line TSV (header + values).
static std::string row_to_tsv(const char* const* cells, int rows, int cols,
                               int row_idx, const std::vector<std::string>& headers) {
    if (row_idx < 0 || row_idx >= rows || cols <= 0) return "";
    std::string out;
    for (int c = 0; c < cols; ++c) {
        if (c > 0) out += '\t';
        if (c < (int)headers.size()) out += headers[c];
    }
    out += "\r\n";
    for (int c = 0; c < cols; ++c) {
        if (c > 0) out += '\t';
        const char* v = cells[row_idx * cols + c];
        if (v) out += v;
    }
    out += "\r\n";
    return out;
}

// compute_visible_rows: applies stage-0 filters + optional sort, returns matching row indices.
static std::vector<int> compute_visible_rows(const char* const* cells,
                                              int rows, int cols,
                                              const State& st)
{
    std::vector<int> out;
    out.reserve(rows);
    const Stage& s = st.raw();
    for (int r = 0; r < rows; ++r) {
        bool keep = true;
        for (const auto& f : s.filters) {
            if (f.col < 0 || f.col >= cols) continue;
            const char* cell = cells[r * cols + f.col];
            if (!compare(cell, f.value.c_str(), f.op)) { keep = false; break; }
        }
        if (keep) out.push_back(r);
    }
    if (!s.sorts.empty()) {
        const SortClause& sc0 = s.sorts.front();
        int sc = -1;
        if (!sc0.col.empty() && sc0.col[0] == '@') {
            sc = std::atoi(sc0.col.c_str() + 1);
        }
        bool desc = sc0.desc;
        if (sc >= 0 && sc < cols) {
            std::sort(out.begin(), out.end(), [&](int a, int b) {
                const char* ca = cells[a * cols + sc];
                const char* cb = cells[b * cols + sc];
                if (!ca) ca = "";
                if (!cb) cb = "";
                double na, nb;
                bool num = parse_number(ca, na) && parse_number(cb, nb);
                int cmp;
                if (num) cmp = (na < nb) ? -1 : (na > nb ? 1 : 0);
                else     cmp = std::strcmp(ca, cb);
                return desc ? (cmp > 0) : (cmp < 0);
            });
        }
    }
    return out;
}

// csv_escape: wraps s in double-quotes if it contains commas, quotes, or newlines.
static std::string csv_escape(const char* s) {
    if (!s) return "";
    bool needs = false;
    for (const char* p = s; *p; ++p) {
        if (*p == ',' || *p == '"' || *p == '\n' || *p == '\r') { needs = true; break; }
    }
    if (!needs) return std::string(s);
    std::string out; out.reserve(std::strlen(s) + 4);
    out += '"';
    for (const char* p = s; *p; ++p) {
        if (*p == '"') out += '"';
        out += *p;
    }
    out += '"';
    return out;
}

// reorder_column: moves col src to position of col dst in st.col_order.
static void reorder_column(State& st, int src, int dst) {
    if (src == dst) return;
    auto it_s = std::find(st.col_order.begin(), st.col_order.end(), src);
    auto it_d = std::find(st.col_order.begin(), st.col_order.end(), dst);
    if (it_s == st.col_order.end() || it_d == st.col_order.end()) return;
    int si = (int)(it_s - st.col_order.begin());
    int di = (int)(it_d - st.col_order.begin());
    int v  = st.col_order[si];
    st.col_order.erase(st.col_order.begin() + si);
    if (di > (int)st.col_order.size()) di = (int)st.col_order.size();
    st.col_order.insert(st.col_order.begin() + di, v);
}

// find_open_bracket: scans buf[0..cursor) backwards for an unmatched '['.
// Returns index of '[' and fills filter_text with content after it, or -1 if none.
static int find_open_bracket(const char* buf, int len, int cursor, std::string& filter_text) {
    filter_text.clear();
    if (!buf || cursor <= 0 || cursor > len) return -1;
    for (int i = cursor - 1; i >= 0; --i) {
        char c = buf[i];
        if (c == ']' || c == '\n') return -1;
        if (c == '[') {
            filter_text.assign(buf + i + 1, cursor - i - 1);
            return i;
        }
    }
    return -1;
}

// insert_column_ref: replaces src[start..cursor) with "[name]", updating new_cursor.
static std::string insert_column_ref(const std::string& src, int start, int cursor,
                                      const std::string& name, int& new_cursor) {
    if (start < 0 || start > (int)src.size() || cursor < start || cursor > (int)src.size()) {
        new_cursor = cursor;
        return src;
    }
    std::string replacement = "[" + name + "]";
    std::string out;
    out.reserve(src.size() - (cursor - start) + replacement.size());
    out.append(src, 0, start);
    out += replacement;
    out.append(src, cursor, std::string::npos);
    new_cursor = start + (int)replacement.size();
    return out;
}

// ---------------------------------------------------------------------------
// Additional helpers from playground data_table_logic — view_mode, joins,
// filter presets, date helpers, effective_type, etc.
// All declared static to stay internal to this translation unit.
// ---------------------------------------------------------------------------

static ColumnType effective_type(ColumnType declared,
                                  const char* const* cells, int rows, int cols, int col) {
    if (declared != ColumnType::Auto) return declared;
    return auto_detect_type(cells, rows, cols, col);
}

static std::vector<Op> ops_for_type(ColumnType t) {
    switch (t) {
        case ColumnType::Int:
        case ColumnType::Float:
        case ColumnType::Date:
            return {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte};
        case ColumnType::Bool:
            return {Op::Eq, Op::Neq};
        case ColumnType::Json:
            return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains};
        case ColumnType::String:
            return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains, Op::StartsWith, Op::EndsWith};
        case ColumnType::Auto:
        default:
            return {Op::Eq, Op::Neq, Op::Contains, Op::NotContains};
    }
}

static int resolve_main_idx(const std::vector<TableInput>& tables, const std::string& main_source) {
    if (tables.empty()) return -1;
    if (main_source.empty()) return 0;
    for (size_t i = 0; i < tables.size(); ++i) {
        if (tables[i].name == main_source) return (int)i;
    }
    return 0;
}

static const char* join_strategy_label(JoinStrategy s) {
    switch (s) {
        case JoinStrategy::Left:  return "left-join";
        case JoinStrategy::Inner: return "inner-join";
        case JoinStrategy::Right: return "right-join";
        case JoinStrategy::Full:  return "full-join";
    }
    return "left-join";
}

struct ViewModeInfo {
    ViewMode     m;
    const char*  token;
    const char*  label;
    int          min_cols;
    bool         needs_num;
    bool         needs_cat;
    bool         needs_agg;
};

static const ViewModeInfo kViewModes[] = {
    { ViewMode::Table,       "table",       "Table",                 1, false, false, false },
    { ViewMode::Bar,         "bar",         "Bar (horizontal)",      2, true,  true,  true  },
    { ViewMode::Column,      "column",      "Column (vertical)",     2, true,  true,  true  },
    { ViewMode::GroupedBar,  "grouped_bar", "Grouped bar",           2, true,  true,  true  },
    { ViewMode::StackedBar,  "stacked_bar", "Stacked bar",           2, true,  true,  true  },
    { ViewMode::Line,        "line",        "Line",                  1, true,  false, false },
    { ViewMode::Area,        "area",        "Area",                  1, true,  false, false },
    { ViewMode::Stairs,      "stairs",      "Stairs",                1, true,  false, false },
    { ViewMode::Scatter,     "scatter",     "Scatter",               2, true,  false, false },
    { ViewMode::Bubble,      "bubble",      "Bubble",                3, true,  false, false },
    { ViewMode::Histogram,   "histogram",   "Histogram",             1, true,  false, false },
    { ViewMode::Histogram2D, "hist2d",      "Histogram 2D",          2, true,  false, false },
    { ViewMode::Heatmap,     "heatmap",     "Heatmap",               1, true,  false, false },
    { ViewMode::BoxPlot,     "boxplot",     "Box plot",              2, true,  true,  false },
    { ViewMode::Stem,        "stem",        "Stem",                  1, true,  false, false },
    { ViewMode::ErrorBars,   "errorbars",   "Error bars",            2, true,  false, false },
    { ViewMode::Pie,         "pie",         "Pie",                   2, true,  true,  true  },
    { ViewMode::Donut,       "donut",       "Donut",                 2, true,  true,  true  },
    { ViewMode::Funnel,      "funnel",      "Funnel",                2, true,  true,  true  },
    { ViewMode::Waterfall,   "waterfall",   "Waterfall",             1, true,  false, true  },
    { ViewMode::KPI,         "kpi",         "KPI (single)",          1, true,  false, true  },
    { ViewMode::KPIGrid,     "kpi_grid",    "KPI grid",              1, true,  false, true  },
    { ViewMode::Candlestick, "candlestick", "Candlestick (OHLC)",    4, true,  false, false },
    { ViewMode::Radar,       "radar",       "Radar",                 2, true,  true,  false },
};
static const int kViewModesN = (int)(sizeof(kViewModes) / sizeof(kViewModes[0]));

static const char* view_mode_label(ViewMode m) {
    for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].label;
    return "Table";
}

static bool view_mode_needs_aggregation(ViewMode m) {
    for (int i = 0; i < kViewModesN; ++i) if (kViewModes[i].m == m) return kViewModes[i].needs_agg;
    return false;
}

static const ViewMode* all_view_modes(int* n_out) {
    static ViewMode arr[64];
    static bool init = false;
    if (!init) {
        for (int i = 0; i < kViewModesN; ++i) arr[i] = kViewModes[i].m;
        init = true;
    }
    if (n_out) *n_out = kViewModesN;
    return arr;
}

// Date helpers (for filter presets and breakout auto-granularity).

namespace {

static bool parse_ymd_local(const std::string& s, int& y, int& m, int& d) {
    if (s.size() < 10) return false;
    for (int i : {0,1,2,3,5,6,8,9}) {
        if (s[(size_t)i] < '0' || s[(size_t)i] > '9') return false;
    }
    if (s[4] != '-' || s[7] != '-') return false;
    y = (s[0]-'0')*1000 + (s[1]-'0')*100 + (s[2]-'0')*10 + (s[3]-'0');
    m = (s[5]-'0')*10 + (s[6]-'0');
    d = (s[8]-'0')*10 + (s[9]-'0');
    if (m < 1 || m > 12 || d < 1 || d > 31) return false;
    return true;
}

static long ymd_to_days_local(int y, int m, int d) {
    if (m <= 2) { y -= 1; m += 12; }
    long era = (y >= 0 ? y : y - 399) / 400;
    unsigned yoe = (unsigned)(y - era * 400);
    unsigned doy = (unsigned)((153 * (m - 3) + 2) / 5 + d - 1);
    unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy;
    return era * 146097 + (long)doe;
}

static void days_to_ymd_local(long days, int& y, int& m, int& d) {
    long era = (days >= 0 ? days : days - 146096) / 146097;
    unsigned doe = (unsigned)(days - era * 146097);
    unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
    int yr = (int)yoe + (int)era * 400;
    unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);
    unsigned mp  = (5*doy + 2)/153;
    unsigned day = doy - (153*mp + 2)/5 + 1;
    unsigned mon = mp < 10 ? mp + 3 : mp - 9;
    if (mon <= 2) yr += 1;
    y = yr; m = (int)mon; d = (int)day;
}

} // anon

static void column_min_max(const char* const* cells, int rows, int cols, int col_idx,
                            std::string& min_out, std::string& max_out) {
    min_out.clear(); max_out.clear();
    if (col_idx < 0 || col_idx >= cols) return;
    bool first = true;
    for (int r = 0; r < rows; ++r) {
        const char* v = cells[r * cols + col_idx];
        if (!v || !*v) continue;
        std::string s(v);
        if (first) { min_out = s; max_out = s; first = false; }
        else { if (s < min_out) min_out = s; if (s > max_out) max_out = s; }
    }
}

static DateGranularity auto_date_granularity(const std::string& min_ymd,
                                              const std::string& max_ymd) {
    int y1,m1,d1, y2,m2,d2;
    if (!parse_ymd_local(min_ymd, y1,m1,d1)) return DateGranularity::Day;
    if (!parse_ymd_local(max_ymd, y2,m2,d2)) return DateGranularity::Day;
    long span = ymd_to_days_local(y2,m2,d2) - ymd_to_days_local(y1,m1,d1);
    if (span < 0) span = -span;
    if (span > 730) return DateGranularity::Year;
    if (span > 60)  return DateGranularity::Month;
    if (span > 14)  return DateGranularity::Week;
    return DateGranularity::Day;
}

static std::string compose_breakout(const std::string& col, DateGranularity g) {
    if (g == DateGranularity::None) return col;
    return col + ":" + date_granularity_token(g);
}

static const char* filter_preset_label(FilterPreset p) {
    switch (p) {
        case FilterPreset::Last7d:       return "Last 7 days";
        case FilterPreset::Last30d:      return "Last 30 days";
        case FilterPreset::Last90d:      return "Last 90 days";
        case FilterPreset::ExcludeNulls: return "Exclude nulls";
        case FilterPreset::NonZero:      return "Non-zero only";
    }
    return "?";
}

static std::vector<Filter> build_preset_filters(FilterPreset preset, int col,
                                                  const std::string& today_ymd) {
    std::vector<Filter> out;
    auto last_n = [&](int n) {
        int y, m, d;
        if (!parse_ymd_local(today_ymd, y, m, d)) return;
        long days = ymd_to_days_local(y, m, d) - n;
        int yy, mm, dd;
        days_to_ymd_local(days, yy, mm, dd);
        char buf[16];
        std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", yy, mm, dd);
        Filter f; f.col = col; f.op = Op::Gte; f.value = buf;
        out.push_back(f);
    };
    switch (preset) {
        case FilterPreset::Last7d:  last_n(7);  break;
        case FilterPreset::Last30d: last_n(30); break;
        case FilterPreset::Last90d: last_n(90); break;
        case FilterPreset::ExcludeNulls: {
            Filter f; f.col = col; f.op = Op::Neq; f.value = "";
            out.push_back(f); break;
        }
        case FilterPreset::NonZero: {
            Filter f1; f1.col = col; f1.op = Op::Neq; f1.value = "";
            Filter f2; f2.col = col; f2.op = Op::Neq; f2.value = "0";
            out.push_back(f1); out.push_back(f2); break;
        }
    }
    return out;
}

// agg_fn_name, op_is_string_only — small helpers not in tql_helpers.h.
// op_label and aggregation_alias are already provided by tql_helpers.h.

static const char* agg_fn_name(AggFn f) {
    switch (f) {
        case AggFn::Count:      return "count";
        case AggFn::Sum:        return "sum";
        case AggFn::Avg:        return "avg";
        case AggFn::Min:        return "min";
        case AggFn::Max:        return "max";
        case AggFn::Distinct:   return "distinct";
        case AggFn::Stddev:     return "stddev";
        case AggFn::Median:     return "median";
        case AggFn::P25:        return "p25";
        case AggFn::P75:        return "p75";
        case AggFn::P90:        return "p90";
        case AggFn::P99:        return "p99";
        case AggFn::Percentile: return "percentile";
    }
    return "?";
}

static bool op_is_string_only(Op o) {
    return o == Op::Contains || o == Op::NotContains ||
           o == Op::StartsWith || o == Op::EndsWith;
}

// UTC date today as ISO YYYY-MM-DD. Para preset filtros Last7/30/90d.
static std::string today_iso() {
    std::time_t t = std::time(nullptr);
    std::tm tm = *std::gmtime(&t);
    char buf[16];
    std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d",
                  tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
    return buf;
}

namespace {

// ---------------------------------------------------------------------------
// UI state global por-instancia (singleton playground).
// ---------------------------------------------------------------------------
struct UiState {
    int         pending_col     = -1;
    std::string pending_value;
    bool        open_cell_popup = false;

    int                                       header_popup_col = -1;
    std::unordered_map<int, std::string>      filter_inputs;
    std::unordered_map<int, std::string>      color_value_inputs;
    std::unordered_map<int, ImVec4>           color_picker_vals;

    int         addf_col   = 0;
    std::string addf_val;
    bool        addf_range = false;
    std::string addf_lo;
    std::string addf_hi;

    int  sel_anchor_row = -1;
    int  sel_anchor_col = -1;
    int  sel_end_row    = -1;
    int  sel_end_col    = -1;
    bool sel_active     = false;
    bool sel_dragging   = false;

    std::string last_export_path;

    // Modal de columna custom (formula).
    bool        cf_open          = false;
    bool        cf_editing       = false;
    int         cf_edit_idx      = -1;
    int         cf_target_stage  = 0;       // stage donde se guarda la formula
    std::string cf_formula;
    std::string cf_name;
    ColumnType  cf_type          = ColumnType::String;
    std::string cf_error;

    bool        cf_ac_open       = false;
    int         cf_ac_start      = -1;
    int         cf_ac_cursor     = -1;
    std::string cf_ac_filter;
    bool        cf_force_cursor  = false;
    int         cf_target_cursor = -1;

    // TQL modales.
    bool        tql_show_open    = false;
    std::string tql_show_text;
    bool        tql_apply_open   = false;
    std::string tql_apply_text;
    std::string tql_apply_error;
    char        tql_file_path[256] = "table.tql";
    std::string tql_io_status;       // mensaje "saved" / "loaded" / error

    // Add-breakout popup (stage > 0).
    int         brk_picker_col   = 0;

    // Add-aggregation popup (stage > 0).
    int         agg_picker_fn    = (int)AggFn::Count;
    int         agg_picker_col   = 0;
    double      agg_picker_arg   = 0.95;

    // Edit chip popups: click der sobre chip.
    //   0=none, 1=filter, 2=breakout, 3=agg, 4=sort
    int         edit_chip_kind   = 0;
    int         edit_chip_idx    = -1;
    int         edit_col_idx     = 0;       // combo idx para col picker
    int         edit_op          = (int)Op::Eq;
    int         edit_agg_fn      = (int)AggFn::Count;
    double      edit_agg_arg     = 0.5;
    bool        edit_sort_desc   = false;
    std::string edit_value;

    // Add-sort popup (any stage).
    int         sort_picker_col  = 0;
    bool        sort_picker_desc = false;

    bool                  stats_mode    = false;
    std::vector<ColStats> stats_cache;
    const char* const*    last_cells    = nullptr;
    int                   last_rows     = -1;
    int                   last_eff_cols = -1;
    size_t                last_filter_h = (size_t)-1;
    int                   last_visible  = -1;

    // Snapshot del active stage output para el config popup.
    std::vector<std::string> active_headers;
    std::vector<ColumnType>  active_types;
    // Snapshot del INPUT del active stage (= output del previo o orig+derived
    // si active==0). Para que el config popup pueda cambiar la categoria del
    // breakout del stage activo eligiendo de las cols upstream.
    std::vector<std::string> input_headers_active;
    std::vector<ColumnType>  input_types_active;

    // Para forzar re-fit en cambio de display/stage/config.
    ViewMode prev_viz_display = ViewMode::Table;
    int      prev_viz_stage   = 0;
    size_t   prev_viz_cfg_h   = 0;

    // (chrome_user_set / chrome_user_visible moved to State — per-table now.)

    // Toggle Table <-> View: remember last non-table display.
    ViewMode last_non_table_main = ViewMode::Bar;

    // Drill history (fase 10). Stacks per-app; no persistido en TQL.
    std::vector<DrillStep> drill_back;
    std::vector<DrillStep> drill_forward;

    // Row inspector (fase 10). -1 cerrado, sino row idx en el output del stage activo.
    int  inspect_row     = -1;
    bool inspect_open    = false;

    // Ask AI modal (fase 11 — issue 0080).
    bool         ask_open       = false;
    bool         ask_busy       = false;
    int          ask_mode       = 0;     // 0 = TQL, 1 = SQL
    char         ask_question[2048] = {0};
    std::string  ask_current_tql;        // emit del state actual al abrir modal
    std::string  ask_response_raw;       // texto del modelo
    std::string  ask_response_code;      // bloque extraido (Lua o SQL)
    std::string  ask_error;
    std::string  ask_status;             // "Sent. Waiting..." / "OK" / error
    char         ask_edit_buf[8192] = {0}; // buffer editable de propuesta
};

UiState& ui() { static UiState s; return s; }

// Row inspector modal (fase 10). Muestra todas cols + valores de la fila
// inspect_row del output del stage activo. Read-only + Copy TSV + Filter
// by this row (anade filters al stage previo si existe).
static void draw_row_inspector_modal(State& st, int active,
                                      const char* const* cells, int rows, int cols,
                                      const std::vector<std::string>& headers,
                                      const std::vector<ColumnType>& types,
                                      const std::vector<std::string>& prev_input_headers) {
    auto& U = ui();
    if (!U.inspect_open) return;
    if (U.inspect_row < 0 || U.inspect_row >= rows) {
        U.inspect_open = false;
        return;
    }
    ImGui::OpenPopup("##row_inspector");
    ImGui::SetNextWindowSize(ImVec2(560, 400), ImGuiCond_Appearing);
    if (ImGui::BeginPopupModal("##row_inspector", &U.inspect_open,
                                ImGuiWindowFlags_NoSavedSettings)) {
        ImGui::Text("Row %d", U.inspect_row);
        ImGui::SameLine(0, 20);
        if (ImGui::SmallButton("Copy TSV")) {
            std::string tsv = row_to_tsv(cells, rows, cols, U.inspect_row, headers);
            ImGui::SetClipboardText(tsv.c_str());
        }
        ImGui::SameLine();
        bool can_filter = (active > 0 && !prev_input_headers.empty());
        ImGui::BeginDisabled(!can_filter);
        if (ImGui::SmallButton("Filter prev stage by this row")) {
            int target = active - 1;
            for (int c = 0; c < cols; ++c) {
                const char* v = cells[U.inspect_row * cols + c];
                if (!v || !*v) continue;
                const std::string& h = headers[c];
                std::string h_clean;
                parse_breakout_granularity(h, h_clean);
                int ci = -1;
                for (size_t i = 0; i < prev_input_headers.size(); ++i) {
                    if (prev_input_headers[i] == h_clean) { ci = (int)i; break; }
                }
                if (ci < 0) continue;
                DrillStep step;
                step.target_stage      = target;
                step.filter_pos        = (int)st.stages[target].filters.size();
                step.prev_active_stage = st.active_stage;
                step.added             = make_drill_filter(ci, v);
                if (apply_drill_step(st, step)) {
                    U.drill_back.push_back(step);
                }
            }
            U.drill_forward.clear();
            U.inspect_open = false;
        }
        ImGui::EndDisabled();
        ImGui::Separator();
        ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
                              | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable;
        if (ImGui::BeginTable("##inspector_tbl", 2, flags, ImVec2(-1, -1))) {
            ImGui::TableSetupColumn("col");
            ImGui::TableSetupColumn("value");
            ImGui::TableHeadersRow();
            for (int c = 0; c < cols; ++c) {
                ImGui::TableNextRow();
                ImGui::TableSetColumnIndex(0);
                ColumnType t = (c < (int)types.size()) ? types[c] : ColumnType::String;
                ImGui::Text("%s %s", column_type_icon(t),
                            (c < (int)headers.size()) ? headers[c].c_str() : "?");
                ImGui::TableSetColumnIndex(1);
                const char* v = cells[U.inspect_row * cols + c];
                ImGui::TextWrapped("%s", v ? v : "");
            }
            ImGui::EndTable();
        }
        ImGui::EndPopup();
    }
}

int autocomplete_cb(ImGuiInputTextCallbackData* data) {
    UiState* U = (UiState*)data->UserData;
    if (data->EventFlag == ImGuiInputTextFlags_CallbackAlways) {
        if (U->cf_force_cursor) {
            data->CursorPos = U->cf_target_cursor;
            U->cf_force_cursor = false;
        }
    }
    if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) {
        std::string filter;
        int idx = find_open_bracket(data->Buf, data->BufTextLen,
                                     data->CursorPos, filter);
        if (idx >= 0) {
            U->cf_ac_open    = true;
            U->cf_ac_start   = idx;
            U->cf_ac_cursor  = data->CursorPos;
            U->cf_ac_filter  = filter;
        } else {
            U->cf_ac_open = false;
        }
    }
    return 0;
}

size_t filters_hash(const std::vector<Filter>& f) {
    size_t h = 0xcbf29ce484222325ULL;
    for (const auto& x : f) {
        h ^= (size_t)x.col;       h *= 0x100000001b3ULL;
        h ^= (size_t)x.op;        h *= 0x100000001b3ULL;
        for (char ch : x.value) { h ^= (size_t)(unsigned char)ch; h *= 0x100000001b3ULL; }
    }
    return h;
}

void ensure_init(State& st, int eff_cols) {
    if ((int)st.col_visible.size() < eff_cols) st.col_visible.resize(eff_cols, true);
    if ((int)st.col_order.size() != eff_cols) {
        std::vector<int> next;
        next.reserve(eff_cols);
        for (int x : st.col_order) if (x >= 0 && x < eff_cols) next.push_back(x);
        std::vector<bool> seen(eff_cols, false);
        for (int x : next) seen[x] = true;
        for (int i = 0; i < eff_cols; ++i) if (!seen[i]) next.push_back(i);
        st.col_order = std::move(next);
    }
    if ((int)st.col_visible.size() < (int)st.col_order.size())
        st.col_visible.resize(st.col_order.size(), true);
}

// ---------------------------------------------------------------------------
// Breadcrumb stages: fila de botones Raw > Stage 1 > Stage 2 ... [+ Stage]
// ---------------------------------------------------------------------------
void draw_stage_breadcrumb(State& st) {
    st.ensure_stage0();

    // Drill history back/forward (fase 10). Botones al inicio.
    auto& U = ui();
    {
        bool can_back = !U.drill_back.empty();
        ImGui::BeginDisabled(!can_back);
        if (ImGui::SmallButton("<##drill_back")) {
            DrillStep s = U.drill_back.back();
            U.drill_back.pop_back();
            if (undo_drill_step(st, s)) {
                U.drill_forward.push_back(s);
            }
        }
        ImGui::EndDisabled();
        if (can_back && ImGui::IsItemHovered())
            ImGui::SetTooltip("Drill back (%zu)", U.drill_back.size());
        ImGui::SameLine();
        bool can_fwd = !U.drill_forward.empty();
        ImGui::BeginDisabled(!can_fwd);
        if (ImGui::SmallButton(">##drill_fwd")) {
            DrillStep s = U.drill_forward.back();
            U.drill_forward.pop_back();
            if (apply_drill_step(st, s)) {
                U.drill_back.push_back(s);
            }
        }
        ImGui::EndDisabled();
        if (can_fwd && ImGui::IsItemHovered())
            ImGui::SetTooltip("Drill forward (%zu)", U.drill_forward.size());
        ImGui::SameLine();
        bool can_up = (st.active_stage > 0);
        ImGui::BeginDisabled(!can_up);
        if (ImGui::SmallButton("^##drill_up")) drill_up(st);
        ImGui::EndDisabled();
        if (can_up && ImGui::IsItemHovered())
            ImGui::SetTooltip("Drill up (stage previo, sin perder filters)");
        ImGui::SameLine();
        ImGui::TextDisabled("|");
        ImGui::SameLine();
    }

    for (int si = 0; si < (int)st.stages.size(); ++si) {
        if (si > 0) { ImGui::SameLine(); ImGui::TextDisabled(">"); ImGui::SameLine(); }

        bool active = (si == st.active_stage);
        if (active) {
            ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32( 80, 140, 200, 240));
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 220, 240));
            ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 60, 120, 180, 240));
        } else {
            ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32( 70,  70,  90, 200));
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 90,  90, 120, 220));
            ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 55,  55,  75, 220));
        }

        char label[256];
        if (si == 0) {
            std::snprintf(label, sizeof(label), "Raw##stage%d", si);
        } else {
            const Stage& s = st.stages[si];
            std::string desc;
            for (size_t i = 0; i < s.breakouts.size() && i < 2; ++i) {
                if (i > 0) desc += ", ";
                desc += s.breakouts[i];
            }
            if (s.breakouts.size() > 2) desc += "...";
            if (desc.empty())
                std::snprintf(label, sizeof(label), "Stage %d##s%d", si, si);
            else
                std::snprintf(label, sizeof(label), "Stage %d: by %s##s%d",
                              si, desc.c_str(), si);
        }
        if (ImGui::Button(label)) st.active_stage = si;
        ImGui::PopStyleColor(3);

        if (si > 0) {
            ImGui::SameLine();
            char xlbl[32];
            std::snprintf(xlbl, sizeof(xlbl), "x##rm_s%d", si);
            if (ImGui::SmallButton(xlbl)) {
                // borra ese stage y sucesores
                while ((int)st.stages.size() > si) st.stages.pop_back();
                if (st.active_stage >= (int)st.stages.size())
                    st.active_stage = (int)st.stages.size() - 1;
                if (st.active_stage < 0) st.active_stage = 0;
                break;
            }
        }
    }
    ImGui::SameLine();
    ImGui::TextDisabled(">");
    ImGui::SameLine();
    if (ImGui::SmallButton("+ Stage##add_stage")) {
        st.stages.push_back(Stage{});
        st.active_stage = (int)st.stages.size() - 1;
    }
}

struct ColInfo { std::string name; ColumnType type; };
std::vector<ColInfo> collect_active_col_info(const State& st);

// Auto-promote: si user en stage 0 elige una viz que necesita agrupacion,
// crea stage 1 con breakout=primera cat + agg=sum(primera num) o count.
void auto_promote_aggregated(State& st) {
    auto& U = ui();
    if (st.active_stage != 0)        return;
    if (st.stages.size() != 1)       return;

    std::string cat_name;
    std::string num_name;
    for (size_t i = 0; i < U.active_headers.size() && i < U.active_types.size(); ++i) {
        ColumnType t = U.active_types[i];
        if (cat_name.empty() &&
            (t == ColumnType::String || t == ColumnType::Date ||
             t == ColumnType::Bool   || t == ColumnType::Json)) {
            cat_name = U.active_headers[i];
        }
        if (num_name.empty() &&
            (t == ColumnType::Int || t == ColumnType::Float)) {
            num_name = U.active_headers[i];
        }
    }

    Stage s1;
    if (!cat_name.empty()) s1.breakouts.push_back(cat_name);
    Aggregation a;
    if (!num_name.empty()) {
        a.fn  = AggFn::Sum;
        a.col = num_name;
    } else {
        a.fn  = AggFn::Count;
    }
    s1.aggregations.push_back(a);
    st.stages.push_back(std::move(s1));
    st.active_stage = (int)st.stages.size() - 1;
}

// Toggle simple: un solo boton que alterna entre Table y el last_non_table.
// Para el main pasa st (para poder auto-promote a stage agregado si la viz
// destino lo requiere). Para extras usar overload sin State.
void draw_table_toggle(ViewMode& display, ViewMode& last_non_table,
                        const char* id_suffix, State* st_opt = nullptr) {
    bool is_table = (display == ViewMode::Table);
    char b[64];
    std::snprintf(b, sizeof(b), "%s##tbl_%s",
                  is_table ? "Show chart" : "Show table", id_suffix);
    ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(80, 140, 200, 240));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 220, 240));
    if (ImGui::SmallButton(b)) {
        if (is_table) {
            ViewMode tgt = (last_non_table == ViewMode::Table)
                            ? ViewMode::Bar : last_non_table;
            display = tgt;
            if (st_opt && view_mode_needs_aggregation(tgt)) {
                auto_promote_aggregated(*st_opt);
            }
        } else {
            last_non_table = display;
            display        = ViewMode::Table;
        }
    }
    ImGui::PopStyleColor(2);
}

// Render extra viz panel: child window con toolbar mini + chart.
// Devuelve true si user pidio cerrar.
bool draw_extra_panel(State& st, VizPanel& p, int idx, const StageOutput& so,
                      const std::vector<ColumnSpec>* col_specs = nullptr) {
    bool close_req = false;
    char child_id[64]; std::snprintf(child_id, sizeof(child_id), "##extra_viz_%d", idx);
    ImGui::BeginChild(child_id, ImVec2(0, 320), true);

    // Toolbar
    int n_modes = 0;
    const ViewMode* modes = all_view_modes(&n_modes);
    ImGui::TextDisabled("View:");
    ImGui::SameLine();
    ImGui::SetNextItemWidth(180);
    char combo_id[64]; std::snprintf(combo_id, sizeof(combo_id), "##ev_mode_%d", idx);
    if (ImGui::BeginCombo(combo_id, view_mode_label(p.display))) {
        for (int i = 0; i < n_modes; ++i) {
            bool sel = (modes[i] == p.display);
            if (ImGui::Selectable(view_mode_label(modes[i]), sel)) {
                p.display = modes[i];
                p.config.fit_request = true;
            }
            if (sel) ImGui::SetItemDefaultFocus();
        }
        ImGui::EndCombo();
    }
    ImGui::SameLine();
    char fit_id[32]; std::snprintf(fit_id, sizeof(fit_id), "Fit##ev_fit_%d", idx);
    if (ImGui::SmallButton(fit_id)) p.config.fit_request = true;
    ImGui::SameLine();
    char lock_id[32]; std::snprintf(lock_id, sizeof(lock_id), "%s##ev_lock_%d",
                                     p.config.locked ? "Locked" : "Lock", idx);
    if (p.config.locked) {
        ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(180, 60, 60, 230));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 80, 80, 240));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32(150, 40, 40, 240));
    }
    if (ImGui::SmallButton(lock_id)) p.config.locked = !p.config.locked;
    if (p.config.locked) ImGui::PopStyleColor(3);
    ImGui::SameLine();
    char close_id[32]; std::snprintf(close_id, sizeof(close_id), "X##ev_close_%d", idx);
    ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(120, 50, 50, 220));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(160, 70, 70, 240));
    if (ImGui::SmallButton(close_id)) close_req = true;
    ImGui::PopStyleColor(2);

    // Toggle Table <-> View per-panel
    char ts[32]; std::snprintf(ts, sizeof(ts), "ep%d", idx);
    draw_table_toggle(p.display, p.last_non_table, ts);

    // Render: si Table -> mini table; else chart.
    if (p.display == ViewMode::Table) {
        ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
                                 ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
                                 ImGuiTableFlags_ScrollX;
        char tid[64]; std::snprintf(tid, sizeof(tid), "##ep_table_%d", idx);
        if (so.cols > 0 && ImGui::BeginTable(tid, so.cols, flags, ImVec2(0, 0))) {
            for (int c = 0; c < so.cols; ++c)
                ImGui::TableSetupColumn(so.headers[c].c_str());
            ImGui::TableHeadersRow();
            for (int r = 0; r < so.rows; ++r) {
                ImGui::TableNextRow();
                for (int c = 0; c < so.cols; ++c) {
                    ImGui::TableSetColumnIndex(c);
                    const char* s = so.cells[(size_t)r * so.cols + c];
                    // Issue 0081-N: declarative renderer for extra panel mini-table.
                    // events_out not propagated to mini-table (secondary render).
                    bool custom_ep = false;
                    if (col_specs && c < (int)col_specs->size()) {
                        const ColumnSpec& cs = (*col_specs)[(size_t)c];
                        if (cs.renderer != CellRenderer::Text) {
                            draw_cell_custom(cs, s, r, c, nullptr);
                            custom_ep = true;
                        }
                    }
                    if (!custom_ep) ImGui::TextUnformatted(s ? s : "");
                }
            }
            ImGui::EndTable();
        }
    } else {
        viz::render(so, p.display, p.config, ImVec2(-1, -1));
    }

    ImGui::EndChild();
    (void)st;
    return close_req;
}

void draw_viz_config_popup(State& st) {
    if (!ImGui::BeginPopup("##viz_cfg_popup")) return;
    ImGui::Text("Configure: %s", view_mode_label(st.display));
    ImGui::Separator();

    auto cols = collect_active_col_info(st);
    std::vector<const char*> all_names;
    std::vector<const char*> num_names;
    std::vector<const char*> cat_names;
    for (auto& c : cols) {
        all_names.push_back(c.name.c_str());
        if (c.type == ColumnType::Int || c.type == ColumnType::Float)
            num_names.push_back(c.name.c_str());
        else
            cat_names.push_back(c.name.c_str());
    }

    auto& vc = st.viz_config;
    ViewMode m = st.display;

    auto combo_for_col = [&](const char* label, std::string& target,
                              const std::vector<const char*>& options) {
        const char* preview = target.empty() ? "(auto)" : target.c_str();
        ImGui::SetNextItemWidth(220);
        if (ImGui::BeginCombo(label, preview)) {
            if (ImGui::Selectable("(auto)", target.empty())) target.clear();
            for (auto& o : options) {
                bool sel = (target == o);
                if (ImGui::Selectable(o, sel)) target = o;
            }
            ImGui::EndCombo();
        }
    };

    // X col: scatter, line, area, stairs, hist2d, bubble
    bool needs_x = (m == ViewMode::Scatter || m == ViewMode::Line ||
                    m == ViewMode::Area    || m == ViewMode::Stairs ||
                    m == ViewMode::Histogram2D || m == ViewMode::Bubble);
    if (needs_x) combo_for_col("X column", vc.x_col, num_names);

    // Y cols: most modes
    bool needs_y = (m != ViewMode::Pie && m != ViewMode::Donut && m != ViewMode::Funnel &&
                    m != ViewMode::Candlestick);
    if (needs_y) {
        ImGui::Text("Y columns:");
        ImGui::SameLine();
        ImGui::TextDisabled("(%d selected; empty = auto)", (int)vc.y_cols.size());
        ImGui::Indent();
        // Show checkbox for each numeric col
        for (auto& nn : num_names) {
            std::string ns = nn;
            bool checked = std::find(vc.y_cols.begin(), vc.y_cols.end(), ns) != vc.y_cols.end();
            if (ImGui::Checkbox(nn, &checked)) {
                if (checked) vc.y_cols.push_back(ns);
                else {
                    auto it = std::find(vc.y_cols.begin(), vc.y_cols.end(), ns);
                    if (it != vc.y_cols.end()) vc.y_cols.erase(it);
                }
            }
        }
        ImGui::Unindent();
        if (ImGui::SmallButton("Clear Y##clr_y")) vc.y_cols.clear();
    }

    // Cat col: bar/pie/funnel/box/waterfall
    bool needs_cat = (m == ViewMode::Bar || m == ViewMode::Column ||
                      m == ViewMode::GroupedBar || m == ViewMode::StackedBar ||
                      m == ViewMode::Pie || m == ViewMode::Donut ||
                      m == ViewMode::Funnel || m == ViewMode::BoxPlot ||
                      m == ViewMode::Waterfall);
    if (needs_cat) {
        // Si el active stage YA esta agrupado (breakouts != empty), la categoria
        // del chart la dicta el breakout. Mostrar todas las cols del INPUT del
        // stage (= cols pre-agrupacion). Selecionar otra = reemplaza breakouts[0]
        // (re-agrupa).
        int as = st.active_stage;
        bool grouped = (as >= 0 && as < (int)st.stages.size() &&
                        !st.stages[as].breakouts.empty());
        const auto& U = ui();
        if (grouped) {
            std::vector<const char*> input_cat_names;
            for (size_t i = 0; i < U.input_headers_active.size() &&
                                i < U.input_types_active.size(); ++i) {
                ColumnType t = U.input_types_active[i];
                if (t == ColumnType::String || t == ColumnType::Date ||
                    t == ColumnType::Bool   || t == ColumnType::Json) {
                    input_cat_names.push_back(U.input_headers_active[i].c_str());
                }
            }
            std::string cur_break = st.stages[as].breakouts[0];
            const char* preview = cur_break.empty() ? "(none)" : cur_break.c_str();
            ImGui::SetNextItemWidth(220);
            if (ImGui::BeginCombo("Category (breakout)", preview)) {
                for (auto& o : input_cat_names) {
                    bool sel = (cur_break == o);
                    if (ImGui::Selectable(o, sel)) {
                        st.stages[as].breakouts[0] = o;
                    }
                }
                ImGui::EndCombo();
            }
        } else {
            combo_for_col("Category", vc.cat_col, cat_names);
        }
    }

    // Size col: bubble
    if (m == ViewMode::Bubble) combo_for_col("Size column", vc.size_col, num_names);

    // Color
    ImGui::Separator();
    float col_f[4] = {
        ((vc.primary_color)         & 0xFF) / 255.0f,
        ((vc.primary_color >>  8)   & 0xFF) / 255.0f,
        ((vc.primary_color >> 16)   & 0xFF) / 255.0f,
        ((vc.primary_color >> 24)   & 0xFF) / 255.0f,
    };
    if (vc.primary_color == 0) { col_f[0]=col_f[1]=col_f[2]=1.0f; col_f[3]=1.0f; }
    if (ImGui::ColorEdit4("Primary color", col_f, ImGuiColorEditFlags_AlphaBar)) {
        unsigned int r = (unsigned int)(col_f[0] * 255);
        unsigned int g = (unsigned int)(col_f[1] * 255);
        unsigned int b = (unsigned int)(col_f[2] * 255);
        unsigned int a = (unsigned int)(col_f[3] * 255);
        vc.primary_color = (a << 24) | (b << 16) | (g << 8) | r;
    }
    ImGui::SameLine();
    if (ImGui::SmallButton("Auto##color")) vc.primary_color = 0;

    // Hist bins
    if (m == ViewMode::Histogram || m == ViewMode::Histogram2D) {
        ImGui::SetNextItemWidth(120);
        int b = vc.hist_bins;
        if (ImGui::InputInt("Bins (0=auto)", &b)) {
            if (b < 0) b = 0;
            vc.hist_bins = b;
        }
    }

    // Pie radius
    if (m == ViewMode::Pie || m == ViewMode::Donut) {
        ImGui::SetNextItemWidth(120);
        float r = vc.pie_radius;
        if (ImGui::SliderFloat("Radius (0=auto)", &r, 0.0f, 0.5f, "%.2f")) {
            vc.pie_radius = r;
        }
    }

    // Toggles
    ImGui::Separator();
    ImGui::Checkbox("Show legend", &vc.show_legend);
    if (m == ViewMode::Line || m == ViewMode::Area || m == ViewMode::Stairs) {
        ImGui::SameLine();
        ImGui::Checkbox("Show markers", &vc.show_markers);
    }

    ImGui::Separator();
    if (ImGui::SmallButton("Reset config")) {
        vc = ViewConfig{};
    }
    ImGui::SameLine();
    if (ImGui::SmallButton("Close")) ImGui::CloseCurrentPopup();

    ImGui::EndPopup();
}

// Devuelve nombres + tipos del active stage output snapshot (poblado por render).
std::vector<ColInfo> collect_active_col_info(const State& st) {
    (void)st;
    auto& U = ui();
    std::vector<ColInfo> r;
    int n = (int)std::min(U.active_headers.size(), U.active_types.size());
    r.reserve(n);
    for (int i = 0; i < n; ++i) r.push_back({U.active_headers[i], U.active_types[i]});
    return r;
}

void draw_viz_selector(State& st) {
    int n_modes = 0;
    const ViewMode* modes = all_view_modes(&n_modes);

    // Right-align: reserve "View: [combo] [Fit] [Lock] [Config] [+ Viz]"
    const float combo_w  = 200.0f;
    const float total_w  = combo_w + 50.0f + 280.0f;
    float right_edge = ImGui::GetWindowContentRegionMax().x;
    float target_x   = right_edge - total_w;
    float min_x      = ImGui::GetCursorPosX() + 20.0f; // do not overlap breadcrumb
    if (target_x < min_x) target_x = min_x;
    ImGui::SameLine();
    ImGui::SetCursorPosX(target_x);

    ImGui::TextDisabled("View:");
    ImGui::SameLine();
    ImGui::SetNextItemWidth(combo_w);
    if (ImGui::BeginCombo("##viz_mode", view_mode_label(st.display))) {
        for (int i = 0; i < n_modes; ++i) {
            bool sel = (modes[i] == st.display);
            if (ImGui::Selectable(view_mode_label(modes[i]), sel)) {
                ViewMode nm = modes[i];
                if (nm != st.display) {
                    st.display = nm;
                    if (view_mode_needs_aggregation(nm)) {
                        auto_promote_aggregated(st);
                    }
                }
            }
            if (sel) ImGui::SetItemDefaultFocus();
        }
        ImGui::EndCombo();
    }
    ImGui::SameLine();
    if (ImGui::SmallButton("Fit##viz_fit")) {
        st.viz_config.fit_request = true;
    }
    ImGui::SameLine();
    bool locked = st.viz_config.locked;
    if (locked) {
        ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(180, 60, 60, 230));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 80, 80, 240));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32(150, 40, 40, 240));
    }
    if (ImGui::SmallButton(locked ? "Locked##viz_lock" : "Lock##viz_lock")) {
        st.viz_config.locked = !st.viz_config.locked;
    }
    if (locked) ImGui::PopStyleColor(3);
    ImGui::SameLine();
    if (ImGui::SmallButton("Config##viz_cfg")) {
        ImGui::OpenPopup("##viz_cfg_popup");
    }
    ImGui::SameLine();
    if (ImGui::SmallButton("Ask AI##ask_open")) {
        auto& U2 = ui();
        U2.ask_open = true;
        U2.ask_busy = false;
        U2.ask_error.clear();
        U2.ask_status.clear();
        U2.ask_response_code.clear();
        U2.ask_response_raw.clear();
        U2.ask_current_tql = tql::emit(st,
            std::vector<std::string>(),  // emit headers stage 0 (caller fill si necesario)
            std::vector<ColumnType>());
    }
    ImGui::SameLine();
    if (ImGui::SmallButton("+ Viz##viz_add")) {
        VizPanel p;
        p.display = ViewMode::Bar;
        if (view_mode_needs_aggregation(p.display)) {
            auto_promote_aggregated(st);
        }
        st.extra_panels.push_back(p);
    }
    draw_viz_config_popup(st);
    ImGui::NewLine();
}

// ---------------------------------------------------------------------------
// Join chips (fase 9 — solo visible si hay joinables).
// ---------------------------------------------------------------------------
void draw_joins_chips(State& st, const std::vector<TableInput>& joinables,
                      const std::vector<std::string>& main_headers) {
    ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32( 80, 130,  90, 220));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 110, 240));
    ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 60, 110,  70, 220));
    ImGui::TextDisabled("Joins:");
    ImGui::SameLine();

    int remove_idx = -1;
    for (size_t i = 0; i < st.joins.size(); ++i) {
        const auto& jn = st.joins[i];
        char lbl[256];
        std::string on_str;
        for (size_t k = 0; k < jn.on.size(); ++k) {
            if (k) on_str += ",";
            on_str += jn.on[k].first + "=" + jn.on[k].second;
        }
        std::snprintf(lbl, sizeof(lbl), "%s <- %s on %s (%s)##join_%zu",
                      jn.alias.empty() ? "_" : jn.alias.c_str(),
                      jn.source.c_str(),
                      on_str.c_str(),
                      join_strategy_label(jn.strategy),
                      i);
        ImGui::Button(lbl);
        ImGui::SameLine();
        char xlbl[32]; std::snprintf(xlbl, sizeof(xlbl), "x##rm_join_%zu", i);
        if (ImGui::SmallButton(xlbl)) remove_idx = (int)i;
        ImGui::SameLine();
    }
    if (remove_idx >= 0) st.joins.erase(st.joins.begin() + remove_idx);

    if (ImGui::SmallButton("+##add_join")) {
        ImGui::OpenPopup("##add_join_popup");
    }
    ImGui::PopStyleColor(3);

    // Popup add
    static int    pick_source_idx = 0;
    static char   pick_alias[64]  = "";
    static int    pick_strategy   = 0;
    static int    pick_left_col   = 0;
    static int    pick_right_col  = 0;
    if (ImGui::BeginPopup("##add_join_popup")) {
        ImGui::Text("Add join");
        ImGui::SetNextItemWidth(180);
        if (ImGui::BeginCombo("source", joinables[pick_source_idx].name.c_str())) {
            for (int k = 0; k < (int)joinables.size(); ++k) {
                bool sel = (k == pick_source_idx);
                if (ImGui::Selectable(joinables[k].name.c_str(), sel)) {
                    pick_source_idx = k;
                    pick_right_col  = 0;
                    if (pick_alias[0] == 0)
                        std::snprintf(pick_alias, sizeof(pick_alias), "%s", joinables[k].name.c_str());
                }
            }
            ImGui::EndCombo();
        }
        ImGui::SetNextItemWidth(180);
        ImGui::InputText("alias", pick_alias, sizeof(pick_alias));

        const char* strategies[] = {"left", "inner", "right", "full"};
        ImGui::SetNextItemWidth(120);
        ImGui::Combo("strategy", &pick_strategy, strategies, IM_ARRAYSIZE(strategies));

        // left col combo (de main_headers)
        ImGui::SetNextItemWidth(180);
        const char* lcur = (pick_left_col >= 0 && pick_left_col < (int)main_headers.size())
                            ? main_headers[pick_left_col].c_str() : "?";
        if (ImGui::BeginCombo("left col", lcur)) {
            for (int k = 0; k < (int)main_headers.size(); ++k) {
                bool sel = (k == pick_left_col);
                if (ImGui::Selectable(main_headers[k].c_str(), sel)) pick_left_col = k;
            }
            ImGui::EndCombo();
        }

        // right col combo (de joinables[pick_source_idx].headers)
        const TableInput& src = joinables[pick_source_idx];
        const char* rcur = (pick_right_col >= 0 && pick_right_col < (int)src.headers.size())
                            ? src.headers[pick_right_col].c_str() : "?";
        ImGui::SetNextItemWidth(180);
        if (ImGui::BeginCombo("right col", rcur)) {
            for (int k = 0; k < (int)src.headers.size(); ++k) {
                bool sel = (k == pick_right_col);
                if (ImGui::Selectable(src.headers[k].c_str(), sel)) pick_right_col = k;
            }
            ImGui::EndCombo();
        }

        ImGui::Separator();
        if (ImGui::SmallButton("Add")) {
            Join jn;
            jn.alias    = pick_alias;
            jn.source   = src.name;
            jn.on.push_back({main_headers[pick_left_col], src.headers[pick_right_col]});
            jn.strategy = (JoinStrategy)pick_strategy;
            st.joins.push_back(jn);
            pick_alias[0] = 0;
            ImGui::CloseCurrentPopup();
        }
        ImGui::SameLine();
        if (ImGui::SmallButton("Cancel")) ImGui::CloseCurrentPopup();

        ImGui::EndPopup();
    }
    ImGui::NewLine();
}

// ---------------------------------------------------------------------------
// Filter chips para el stage activo. eff_headers/eff_cols son del INPUT del
// stage activo (= orig+derived para stage 0; output del stage previo para 1+).
// ---------------------------------------------------------------------------
void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols,
                        const std::vector<ColumnType>& eff_types) {
    auto& U = ui();
    ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(120,  60, 170, 220));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(150,  85, 200, 240));
    ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 95,  45, 140, 240));
    if (ImGui::SmallButton("+##addfilter_btn")) ImGui::OpenPopup("##addfilter");
    ImGui::PopStyleColor(3);
    ImGui::SameLine();

    // Presets (fase 10): menu con Last7/30/90d (cols Date), ExcludeNulls (any),
    // NonZero (cols numericas). Apply append a stg.filters via build_preset_filters.
    if (ImGui::SmallButton("Presets##fpresets")) ImGui::OpenPopup("##presets_menu");
    if (ImGui::BeginPopup("##presets_menu")) {
        int first_date = -1, first_num = -1;
        for (int c = 0; c < eff_cols && c < (int)eff_types.size(); ++c) {
            if (first_date < 0 && eff_types[c] == ColumnType::Date) first_date = c;
            if (first_num  < 0 && (eff_types[c] == ColumnType::Int ||
                                    eff_types[c] == ColumnType::Float)) first_num = c;
        }
        auto apply_preset = [&](FilterPreset p, int col) {
            auto fs = build_preset_filters(p, col, today_iso());
            for (auto& f : fs) stg.filters.push_back(f);
        };
        if (first_date >= 0) {
            char l1[96], l2[96], l3[96];
            std::snprintf(l1, sizeof(l1), "Last 7 days on \"%s\"",  eff_headers[first_date]);
            std::snprintf(l2, sizeof(l2), "Last 30 days on \"%s\"", eff_headers[first_date]);
            std::snprintf(l3, sizeof(l3), "Last 90 days on \"%s\"", eff_headers[first_date]);
            if (ImGui::MenuItem(l1)) apply_preset(FilterPreset::Last7d,  first_date);
            if (ImGui::MenuItem(l2)) apply_preset(FilterPreset::Last30d, first_date);
            if (ImGui::MenuItem(l3)) apply_preset(FilterPreset::Last90d, first_date);
            ImGui::Separator();
        }
        if (ImGui::BeginMenu("Exclude nulls in...")) {
            for (int c = 0; c < eff_cols; ++c) {
                if (ImGui::MenuItem(eff_headers[c])) apply_preset(FilterPreset::ExcludeNulls, c);
            }
            ImGui::EndMenu();
        }
        if (first_num >= 0) {
            if (ImGui::BeginMenu("Non-zero in...")) {
                for (int c = 0; c < eff_cols && c < (int)eff_types.size(); ++c) {
                    if (eff_types[c] == ColumnType::Int || eff_types[c] == ColumnType::Float) {
                        if (ImGui::MenuItem(eff_headers[c])) apply_preset(FilterPreset::NonZero, c);
                    }
                }
                ImGui::EndMenu();
            }
        }
        ImGui::EndPopup();
    }
    ImGui::SameLine();

    if (stg.filters.empty()) {
        ImGui::TextDisabled("Sin filtros.");
        return;
    }
    for (size_t i = 0; i < stg.filters.size(); ) {
        const auto& f = stg.filters[i];
        const char* hdr = (f.col >= 0 && f.col < eff_cols) ? eff_headers[f.col] : "?";
        char buf[256];
        std::snprintf(buf, sizeof(buf), "%s %s %s  x##chip%zu",
                      hdr, op_label(f.op), f.value.c_str(), i);
        ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(120,  60, 170, 220));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(150,  85, 200, 240));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 95,  45, 140, 240));
        bool clicked = ImGui::SmallButton(buf);
        ImGui::PopStyleColor(3);
        // Click derecho: edit
        if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
            U.edit_chip_kind = 1;
            U.edit_chip_idx  = (int)i;
            U.edit_col_idx   = f.col;
            U.edit_op        = (int)f.op;
            U.edit_value     = f.value;
            ImGui::OpenPopup("##edit_filter");
        }
        if (clicked) { stg.filters.erase(stg.filters.begin() + i); continue; }
        ImGui::SameLine();
        ++i;
    }
    ImGui::NewLine();
}

// Chips de breakout (stage > 0).
void draw_breakout_chips(Stage& stg, const char* const* in_headers, int in_cols,
                          const std::vector<ColumnType>& in_types) {
    auto& U = ui();
    ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32( 60, 160, 170, 220));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 80, 190, 200, 240));
    ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 40, 130, 140, 240));
    if (ImGui::SmallButton("+##addbreakout_btn")) ImGui::OpenPopup("##addbreakout");
    ImGui::PopStyleColor(3);
    ImGui::SameLine();

    if (stg.breakouts.empty()) {
        ImGui::TextDisabled("Group by: ninguna col.");
        return;
    }
    for (size_t i = 0; i < stg.breakouts.size(); ) {
        std::string col_name;
        DateGranularity g = parse_breakout_granularity(stg.breakouts[i], col_name);

        // Resolve col index para lookup de tipo.
        int col_idx = -1;
        for (int c = 0; c < in_cols; ++c) {
            if (std::strcmp(in_headers[c], col_name.c_str()) == 0) { col_idx = c; break; }
        }
        bool is_date_col = (col_idx >= 0 && col_idx < (int)in_types.size()
                             && in_types[col_idx] == ColumnType::Date);

        char buf[256];
        std::snprintf(buf, sizeof(buf), "%s  x##bk%zu", stg.breakouts[i].c_str(), i);
        ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32( 60, 160, 170, 220));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 80, 190, 200, 240));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 40, 130, 140, 240));
        bool clicked = ImGui::SmallButton(buf);
        ImGui::PopStyleColor(3);
        if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
            U.edit_chip_kind = 2;
            U.edit_chip_idx  = (int)i;
            U.edit_col_idx = (col_idx >= 0) ? col_idx : 0;
            ImGui::OpenPopup("##edit_breakout");
        }
        if (clicked) { stg.breakouts.erase(stg.breakouts.begin() + i); continue; }

        // Granularity combo inline cuando col Date (fase 10).
        if (is_date_col) {
            ImGui::SameLine();
            const char* preview = (g == DateGranularity::None)
                                  ? "(raw)" : date_granularity_token(g);
            char combo_id[32];
            std::snprintf(combo_id, sizeof(combo_id), "##gran%zu", i);
            ImGui::SetNextItemWidth(72);
            if (ImGui::BeginCombo(combo_id, preview)) {
                DateGranularity opts[] = {
                    DateGranularity::None,
                    DateGranularity::Year,
                    DateGranularity::Month,
                    DateGranularity::Week,
                    DateGranularity::Day,
                    DateGranularity::Hour,
                };
                for (auto o : opts) {
                    const char* lbl = (o == DateGranularity::None)
                                      ? "(raw)" : date_granularity_token(o);
                    if (ImGui::Selectable(lbl, o == g)) {
                        stg.breakouts[i] = compose_breakout(col_name, o);
                    }
                }
                ImGui::EndCombo();
            }
        }

        ImGui::SameLine();
        ++i;
    }
    ImGui::NewLine();
}

const char* agg_fn_label(AggFn f) {
    switch (f) {
        case AggFn::Count:      return "count";
        case AggFn::Sum:        return "sum";
        case AggFn::Avg:        return "avg";
        case AggFn::Min:        return "min";
        case AggFn::Max:        return "max";
        case AggFn::Distinct:   return "distinct";
        case AggFn::Stddev:     return "stddev";
        case AggFn::Median:     return "median";
        case AggFn::P25:        return "p25";
        case AggFn::P75:        return "p75";
        case AggFn::P90:        return "p90";
        case AggFn::P99:        return "p99";
        case AggFn::Percentile: return "percentile";
    }
    return "?";
}

void draw_aggregation_chips(Stage& stg, const char* const* in_headers, int in_cols) {
    auto& U = ui();
    ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32( 40, 140,  60, 220));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 60, 170,  85, 240));
    ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 30, 110,  45, 240));
    if (ImGui::SmallButton("+##addagg_btn")) ImGui::OpenPopup("##addagg");
    ImGui::PopStyleColor(3);
    ImGui::SameLine();

    if (stg.aggregations.empty()) {
        ImGui::TextDisabled("Aggregations: ninguna.");
        return;
    }
    for (size_t i = 0; i < stg.aggregations.size(); ) {
        const auto& a = stg.aggregations[i];
        char buf[256];
        if (a.fn == AggFn::Count) {
            std::snprintf(buf, sizeof(buf), "count  x##ag%zu", i);
        } else if (a.fn == AggFn::Percentile) {
            std::snprintf(buf, sizeof(buf), "percentile(%s, %g)  x##ag%zu",
                          a.col.c_str(), a.arg, i);
        } else {
            std::snprintf(buf, sizeof(buf), "%s(%s)  x##ag%zu",
                          agg_fn_label(a.fn), a.col.c_str(), i);
        }
        ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32( 40, 140,  60, 220));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 60, 170,  85, 240));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 30, 110,  45, 240));
        bool clicked = ImGui::SmallButton(buf);
        ImGui::PopStyleColor(3);
        if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
            U.edit_chip_kind = 3;
            U.edit_chip_idx  = (int)i;
            U.edit_agg_fn    = (int)a.fn;
            U.edit_agg_arg   = a.arg;
            U.edit_col_idx   = 0;
            for (int c = 0; c < in_cols; ++c) {
                if (std::strcmp(in_headers[c], a.col.c_str()) == 0) {
                    U.edit_col_idx = c; break;
                }
            }
            ImGui::OpenPopup("##edit_agg");
        }
        if (clicked) { stg.aggregations.erase(stg.aggregations.begin() + i); continue; }
        ImGui::SameLine();
        ++i;
    }
    (void)in_headers; (void)in_cols;
    ImGui::NewLine();
}

void draw_sort_chips(Stage& stg) {
    auto& U = ui();
    ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(220, 130,  50, 230));
    ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(240, 155,  75, 245));
    ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32(180, 100,  30, 240));
    if (ImGui::SmallButton("+##addsort_btn")) ImGui::OpenPopup("##addsort");
    ImGui::PopStyleColor(3);
    ImGui::SameLine();

    if (stg.sorts.empty()) {
        ImGui::TextDisabled("Sort: ninguno.");
        return;
    }
    int erase_idx = -1;
    int drag_src  = -1;
    int drag_dst  = -1;
    for (size_t i = 0; i < stg.sorts.size(); ++i) {
        const auto& sc = stg.sorts[i];
        char buf[256];
        std::snprintf(buf, sizeof(buf), "%zu. %s %s  x##srt%zu",
                      i + 1, sc.col.c_str(), sc.desc ? "desc" : "asc", i);
        ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(220, 130,  50, 230));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(240, 155,  75, 245));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32(180, 100,  30, 240));
        bool clicked = ImGui::SmallButton(buf);
        ImGui::PopStyleColor(3);

        // Drag source: prioridad multi-sort reorderable.
        if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
            int idx = (int)i;
            ImGui::SetDragDropPayload("##sortreorder", &idx, sizeof(int));
            ImGui::Text("Move sort #%zu", i + 1);
            ImGui::EndDragDropSource();
        }
        if (ImGui::BeginDragDropTarget()) {
            if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("##sortreorder")) {
                drag_src = *(const int*)p->Data;
                drag_dst = (int)i;
            }
            ImGui::EndDragDropTarget();
        }
        if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
            U.edit_chip_kind  = 4;
            U.edit_chip_idx   = (int)i;
            U.edit_value      = sc.col;
            U.edit_sort_desc  = sc.desc;
            ImGui::OpenPopup("##edit_sort");
        }
        if (clicked) erase_idx = (int)i;
        ImGui::SameLine();
    }
    ImGui::NewLine();

    if (drag_src >= 0 && drag_dst >= 0 && drag_src != drag_dst &&
        drag_src < (int)stg.sorts.size() && drag_dst < (int)stg.sorts.size())
    {
        SortClause moved = std::move(stg.sorts[drag_src]);
        stg.sorts.erase(stg.sorts.begin() + drag_src);
        int insert_at = (drag_src < drag_dst) ? drag_dst : drag_dst;
        if (insert_at > (int)stg.sorts.size()) insert_at = (int)stg.sorts.size();
        stg.sorts.insert(stg.sorts.begin() + insert_at, std::move(moved));
    } else if (erase_idx >= 0 && erase_idx < (int)stg.sorts.size()) {
        stg.sorts.erase(stg.sorts.begin() + erase_idx);
    }
}

// ---- Edit chip popups: click derecho sobre chip abre popup. ----
// Header click handler:
//   click:        si col ya esta en sorts -> cicla su direccion asc/desc/off.
//                 sino -> append {col, asc} al final (multi-sort por defecto).
//   shift+click:  reset. Reemplaza sorts con {col, asc} (sort unico).
void apply_header_sort_click(Stage& stg, const std::string& col_name, bool shift) {
    if (shift) {
        stg.sorts.clear();
        stg.sorts.push_back({col_name, false});
        return;
    }
    int idx = -1;
    for (size_t i = 0; i < stg.sorts.size(); ++i) {
        if (stg.sorts[i].col == col_name) { idx = (int)i; break; }
    }
    if (idx < 0) {
        stg.sorts.push_back({col_name, false});
    } else {
        if (!stg.sorts[idx].desc) stg.sorts[idx].desc = true;
        else                        stg.sorts.erase(stg.sorts.begin() + idx);
    }
}

void draw_edit_filter_popup(Stage& stg, const char* const* headers, int n_cols,
                             const std::vector<ColumnType>& types) {
    auto& U = ui();
    if (!ImGui::BeginPopup("##edit_filter")) return;
    if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.filters.size()) {
        auto& f = stg.filters[U.edit_chip_idx];
        ImGui::SetNextItemWidth(200);
        const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
                          ? headers[U.edit_col_idx] : "?";
        if (ImGui::BeginCombo("col", cur)) {
            for (int c = 0; c < n_cols; ++c) {
                bool sel = (U.edit_col_idx == c);
                if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c;
            }
            ImGui::EndCombo();
        }
        ColumnType t = (U.edit_col_idx >= 0 && U.edit_col_idx < (int)types.size())
                       ? types[U.edit_col_idx] : ColumnType::String;
        auto ops = ops_for_type(t);
        ImGui::SetNextItemWidth(140);
        if (ImGui::BeginCombo("op", op_label((Op)U.edit_op))) {
            for (auto o : ops) {
                bool sel = ((int)o == U.edit_op);
                if (ImGui::Selectable(op_label(o), sel)) U.edit_op = (int)o;
            }
            ImGui::EndCombo();
        }
        char vbuf[256] = {0};
        std::snprintf(vbuf, sizeof(vbuf), "%s", U.edit_value.c_str());
        ImGui::SetNextItemWidth(220);
        if (ImGui::InputText("value", vbuf, sizeof(vbuf))) U.edit_value = vbuf;
        if (ImGui::Button("Save")) {
            f.col   = U.edit_col_idx;
            f.op    = (Op)U.edit_op;
            f.value = U.edit_value;
            ImGui::CloseCurrentPopup();
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

void draw_edit_breakout_popup(Stage& stg, const char* const* headers, int n_cols) {
    auto& U = ui();
    if (!ImGui::BeginPopup("##edit_breakout")) return;
    if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.breakouts.size()) {
        ImGui::SetNextItemWidth(240);
        const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
                          ? headers[U.edit_col_idx] : "?";
        if (ImGui::BeginCombo("col", cur)) {
            for (int c = 0; c < n_cols; ++c) {
                bool sel = (U.edit_col_idx == c);
                if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c;
            }
            ImGui::EndCombo();
        }
        if (ImGui::Button("Save")) {
            if (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
                stg.breakouts[U.edit_chip_idx] = headers[U.edit_col_idx];
            ImGui::CloseCurrentPopup();
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

void draw_edit_agg_popup(Stage& stg, const char* const* headers, int n_cols) {
    auto& U = ui();
    if (!ImGui::BeginPopup("##edit_agg")) return;
    if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.aggregations.size()) {
        const AggFn all_fns[] = {AggFn::Count, AggFn::Sum, AggFn::Avg, AggFn::Min, AggFn::Max,
                                  AggFn::Distinct, AggFn::Stddev, AggFn::Median,
                                  AggFn::P25, AggFn::P75, AggFn::P90, AggFn::P99,
                                  AggFn::Percentile};
        ImGui::SetNextItemWidth(160);
        if (ImGui::BeginCombo("fn", agg_fn_label((AggFn)U.edit_agg_fn))) {
            for (auto f : all_fns) {
                bool sel = ((int)f == U.edit_agg_fn);
                if (ImGui::Selectable(agg_fn_label(f), sel)) U.edit_agg_fn = (int)f;
            }
            ImGui::EndCombo();
        }
        if ((AggFn)U.edit_agg_fn != AggFn::Count) {
            ImGui::SetNextItemWidth(200);
            const char* cur = (U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
                              ? headers[U.edit_col_idx] : "?";
            if (ImGui::BeginCombo("col", cur)) {
                for (int c = 0; c < n_cols; ++c) {
                    bool sel = (U.edit_col_idx == c);
                    if (ImGui::Selectable(headers[c], sel)) U.edit_col_idx = c;
                }
                ImGui::EndCombo();
            }
        }
        if ((AggFn)U.edit_agg_fn == AggFn::Percentile) {
            float v = (float)U.edit_agg_arg;
            ImGui::SetNextItemWidth(140);
            if (ImGui::InputFloat("p (0..1)", &v, 0.05f, 0.1f, "%.2f"))
                U.edit_agg_arg = v;
        }
        if (ImGui::Button("Save")) {
            auto& a = stg.aggregations[U.edit_chip_idx];
            a.fn = (AggFn)U.edit_agg_fn;
            if (a.fn != AggFn::Count && U.edit_col_idx >= 0 && U.edit_col_idx < n_cols)
                a.col = headers[U.edit_col_idx];
            else if (a.fn == AggFn::Count) a.col.clear();
            a.arg = U.edit_agg_arg;
            a.alias.clear();
            ImGui::CloseCurrentPopup();
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

void draw_edit_sort_popup(Stage& stg, const char* const* headers, int n_cols) {
    auto& U = ui();
    if (!ImGui::BeginPopup("##edit_sort")) return;
    if (U.edit_chip_idx >= 0 && U.edit_chip_idx < (int)stg.sorts.size()) {
        ImGui::SetNextItemWidth(240);
        if (ImGui::BeginCombo("col", U.edit_value.c_str())) {
            for (int c = 0; c < n_cols; ++c) {
                bool sel = (U.edit_value == headers[c]);
                if (ImGui::Selectable(headers[c], sel)) U.edit_value = headers[c];
            }
            ImGui::EndCombo();
        }
        if (ImGui::RadioButton("asc",  !U.edit_sort_desc)) U.edit_sort_desc = false;
        ImGui::SameLine();
        if (ImGui::RadioButton("desc", U.edit_sort_desc)) U.edit_sort_desc = true;
        if (ImGui::Button("Save")) {
            auto& sc = stg.sorts[U.edit_chip_idx];
            sc.col  = U.edit_value;
            sc.desc = U.edit_sort_desc;
            ImGui::CloseCurrentPopup();
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

void maybe_recompute_stats(const char* const* cells, int rows, int orig_cols,
                           int eff_cols, const std::vector<Filter>& filters,
                           const std::vector<int>& visible,
                           const std::vector<int>& src_for_eff)
{
    auto& U = ui();
    if (!U.stats_mode) return;
    size_t fh = filters_hash(filters);
    bool ds_changed = (cells != U.last_cells || rows != U.last_rows ||
                       eff_cols != U.last_eff_cols ||
                       (int)U.stats_cache.size() != eff_cols);
    bool fl_changed = (fh != U.last_filter_h || (int)visible.size() != U.last_visible);
    if (!ds_changed && !fl_changed) return;
    U.stats_cache.resize(eff_cols);
    const int* idx = visible.empty() ? nullptr : visible.data();
    int        n   = (int)visible.size();
    for (int c = 0; c < eff_cols; ++c) {
        int src = src_for_eff[c];
        U.stats_cache[c] = compute_column_stats(cells, rows, orig_cols, src,
                                                 100000, idx, n);
    }
    U.last_cells    = cells;
    U.last_rows     = rows;
    U.last_eff_cols = eff_cols;
    U.last_filter_h = fh;
    U.last_visible  = (int)visible.size();
}

bool draw_typed_ops(ColumnType t, Op& out) {
    auto ops = ops_for_type(t);
    for (size_t i = 0; i < ops.size(); ++i) {
        if (i % 5 != 0) ImGui::SameLine();
        if (ImGui::SmallButton(op_label(ops[i]))) { out = ops[i]; return true; }
    }
    return false;
}

bool type_supports_range(ColumnType t) {
    return t == ColumnType::Int || t == ColumnType::Float || t == ColumnType::Date;
}

void draw_add_filter_popup(Stage& stg, const char* const* eff_headers_arr, int eff_cols,
                           const std::vector<ColumnType>& eff_types)
{
    auto& U = ui();
    if (!ImGui::BeginPopup("##addfilter")) return;
    if (U.addf_col < 0 || U.addf_col >= eff_cols) U.addf_col = 0;
    ImGui::SetNextItemWidth(220);
    if (ImGui::BeginCombo("col", eff_headers_arr[U.addf_col])) {
        for (int c = 0; c < eff_cols; ++c) {
            char it[160];
            std::snprintf(it, sizeof(it), "%s %s",
                          column_type_icon(eff_types[c]), eff_headers_arr[c]);
            bool sel = (U.addf_col == c);
            if (ImGui::Selectable(it, sel)) U.addf_col = c;
        }
        ImGui::EndCombo();
    }
    ColumnType t = eff_types[U.addf_col];
    ImGui::TextDisabled("type: %s %s", column_type_icon(t), column_type_name(t));

    bool can_range = type_supports_range(t);
    if (can_range) ImGui::Checkbox("Range (min/max)", &U.addf_range);
    else           U.addf_range = false;

    if (!U.addf_range) {
        char buf[256] = {0};
        std::snprintf(buf, sizeof(buf), "%s", U.addf_val.c_str());
        ImGui::SetNextItemWidth(220);
        if (ImGui::InputText("val", buf, sizeof(buf))) U.addf_val = buf;
        Op picked;
        if (draw_typed_ops(t, picked)) {
            stg.filters.push_back({U.addf_col, picked, U.addf_val});
            U.addf_val.clear();
            ImGui::CloseCurrentPopup();
        }
    } else {
        char lo[128] = {0}, hi[128] = {0};
        std::snprintf(lo, sizeof(lo), "%s", U.addf_lo.c_str());
        std::snprintf(hi, sizeof(hi), "%s", U.addf_hi.c_str());
        ImGui::SetNextItemWidth(100);
        if (ImGui::InputText("min", lo, sizeof(lo))) U.addf_lo = lo;
        ImGui::SameLine();
        ImGui::SetNextItemWidth(100);
        if (ImGui::InputText("max", hi, sizeof(hi))) U.addf_hi = hi;
        ImGui::SameLine();
        if (ImGui::SmallButton("Add range")) {
            if (!U.addf_lo.empty()) stg.filters.push_back({U.addf_col, Op::Gte, U.addf_lo});
            if (!U.addf_hi.empty()) stg.filters.push_back({U.addf_col, Op::Lte, U.addf_hi});
            U.addf_lo.clear(); U.addf_hi.clear();
            ImGui::CloseCurrentPopup();
        }
    }
    ImGui::EndPopup();
}

void draw_add_breakout_popup(Stage& stg, const char* const* in_headers, int in_cols,
                              const std::vector<ColumnType>& in_types,
                              const char* const* in_cells, int in_rows) {
    auto& U = ui();
    if (!ImGui::BeginPopup("##addbreakout")) return;
    if (U.brk_picker_col < 0 || U.brk_picker_col >= in_cols) U.brk_picker_col = 0;
    ImGui::SetNextItemWidth(220);
    if (ImGui::BeginCombo("col##bkcol", in_headers[U.brk_picker_col])) {
        for (int c = 0; c < in_cols; ++c) {
            char it[160];
            std::snprintf(it, sizeof(it), "%s %s",
                          column_type_icon(in_types[c]), in_headers[c]);
            bool sel = (U.brk_picker_col == c);
            if (ImGui::Selectable(it, sel)) U.brk_picker_col = c;
        }
        ImGui::EndCombo();
    }
    if (ImGui::Button("Add##bk")) {
        int c = U.brk_picker_col;
        std::string col = in_headers[c];
        // Fase 10: si col es Date, auto-detect granularidad via rango lexical
        // (ISO YYYY-MM-DD ordena bien). Default Day si rango invalido.
        if (c >= 0 && c < (int)in_types.size() && in_types[c] == ColumnType::Date) {
            std::string lo, hi;
            column_min_max(in_cells, in_rows, in_cols, c, lo, hi);
            DateGranularity g = auto_date_granularity(lo, hi);
            stg.breakouts.emplace_back(compose_breakout(col, g));
        } else {
            stg.breakouts.emplace_back(col);
        }
        ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

void draw_add_aggregation_popup(Stage& stg, const char* const* in_headers, int in_cols,
                                 const std::vector<ColumnType>& in_types) {
    auto& U = ui();
    if (!ImGui::BeginPopup("##addagg")) return;

    AggFn cur_fn = (AggFn)U.agg_picker_fn;
    ImGui::SetNextItemWidth(160);
    if (ImGui::BeginCombo("fn##aggfn", agg_fn_label(cur_fn))) {
        AggFn all[] = {AggFn::Count, AggFn::Sum, AggFn::Avg, AggFn::Min, AggFn::Max,
                       AggFn::Distinct, AggFn::Stddev, AggFn::Median,
                       AggFn::P25, AggFn::P75, AggFn::P90, AggFn::P99,
                       AggFn::Percentile};
        for (AggFn f : all) {
            bool sel = (f == cur_fn);
            if (ImGui::Selectable(agg_fn_label(f), sel)) U.agg_picker_fn = (int)f;
        }
        ImGui::EndCombo();
    }
    if (cur_fn != AggFn::Count) {
        if (U.agg_picker_col < 0 || U.agg_picker_col >= in_cols) U.agg_picker_col = 0;
        ImGui::SetNextItemWidth(220);
        if (ImGui::BeginCombo("col##aggcol", in_headers[U.agg_picker_col])) {
            for (int c = 0; c < in_cols; ++c) {
                char it[160];
                std::snprintf(it, sizeof(it), "%s %s",
                              column_type_icon(in_types[c]), in_headers[c]);
                bool sel = (U.agg_picker_col == c);
                if (ImGui::Selectable(it, sel)) U.agg_picker_col = c;
            }
            ImGui::EndCombo();
        }
    }
    if (cur_fn == AggFn::Percentile) {
        double v = U.agg_picker_arg;
        ImGui::SetNextItemWidth(120);
        if (ImGui::InputDouble("p (0..1)", &v, 0.05, 0.1, "%.2f")) {
            if (v < 0) v = 0; if (v > 1) v = 1;
            U.agg_picker_arg = v;
        }
    }
    if (ImGui::Button("Add##ag")) {
        Aggregation a;
        a.fn  = cur_fn;
        a.col = (cur_fn == AggFn::Count) ? "" : std::string(in_headers[U.agg_picker_col]);
        a.arg = (cur_fn == AggFn::Percentile) ? U.agg_picker_arg : 0.0;
        stg.aggregations.push_back(a);
        ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

void draw_add_sort_popup(Stage& stg, const char* const* in_headers, int in_cols,
                          const std::vector<ColumnType>& in_types) {
    auto& U = ui();
    if (!ImGui::BeginPopup("##addsort")) return;
    if (U.sort_picker_col < 0 || U.sort_picker_col >= in_cols) U.sort_picker_col = 0;
    ImGui::SetNextItemWidth(220);
    if (ImGui::BeginCombo("col##sortcol", in_headers[U.sort_picker_col])) {
        for (int c = 0; c < in_cols; ++c) {
            char it[160];
            std::snprintf(it, sizeof(it), "%s %s",
                          column_type_icon(in_types[c]), in_headers[c]);
            bool sel = (U.sort_picker_col == c);
            if (ImGui::Selectable(it, sel)) U.sort_picker_col = c;
        }
        ImGui::EndCombo();
    }
    ImGui::Checkbox("desc", &U.sort_picker_desc);
    if (ImGui::Button("Add##srt")) {
        SortClause sc;
        sc.col  = in_headers[U.sort_picker_col];
        sc.desc = U.sort_picker_desc;
        stg.sorts.push_back(sc);
        ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

void draw_header_menu(State& st, Stage& stg, int col,
                      const char* const* eff_headers_arr, int eff_cols,
                      const std::vector<ColumnType>& eff_types,
                      int orig_cols, bool is_raw_stage)
{
    auto& U = ui();
    ColumnType t = eff_types[col];

    if (ImGui::MenuItem("Sort ascending")) {
        stg.sorts.clear();
        stg.sorts.push_back({eff_headers_arr[col], false});
    }
    if (ImGui::MenuItem("Sort descending")) {
        stg.sorts.clear();
        stg.sorts.push_back({eff_headers_arr[col], true});
    }
    if (!stg.sorts.empty() && ImGui::MenuItem("Clear sort")) stg.sorts.clear();
    ImGui::Separator();

    auto& fbuf = U.filter_inputs[col];
    fbuf.resize(256, '\0');
    if (ImGui::BeginMenu("Filter...")) {
        ImGui::SetNextItemWidth(220);
        ImGui::InputText("##filterval", fbuf.data(), fbuf.size());
        std::string val(fbuf.c_str());
        auto ops = ops_for_type(t);
        for (size_t i = 0; i < ops.size(); ++i) {
            if (i % 5 != 0) ImGui::SameLine();
            if (ImGui::SmallButton(op_label(ops[i]))) {
                stg.filters.push_back({col, ops[i], val});
                ImGui::CloseCurrentPopup();
            }
        }
        ImGui::EndMenu();
    }

    // Change type / derived solo en stage 0.
    if (is_raw_stage) {
        if (ImGui::BeginMenu("Change type")) {
            const ColumnType types[] = {
                ColumnType::String, ColumnType::Int, ColumnType::Float,
                ColumnType::Bool,   ColumnType::Date, ColumnType::Json
            };
            for (auto nt : types) {
                char lab[64];
                std::snprintf(lab, sizeof(lab), "%s %s",
                              column_type_icon(nt), column_type_name(nt));
                if (ImGui::MenuItem(lab)) {
                    DerivedColumn d;
                    d.source_col = (col < orig_cols) ? col : stg.derived[col - orig_cols].source_col;
                    d.type = nt;
                    d.name = std::string(eff_headers_arr[col]) + "_" + column_type_name(nt);
                    stg.derived.push_back(d);
                }
            }
            ImGui::EndMenu();
        }
    }

    if (ImGui::BeginMenu("Conditional color")) {
        auto& vbuf = U.color_value_inputs[col];
        vbuf.resize(256, '\0');
        if (U.color_picker_vals.find(col) == U.color_picker_vals.end())
            U.color_picker_vals[col] = ImVec4(0.85f, 0.40f, 0.30f, 0.60f);
        ImVec4& cv = U.color_picker_vals[col];
        ImGui::SetNextItemWidth(180);
        ImGui::InputText("equals", vbuf.data(), vbuf.size());
        ImGui::ColorEdit4("color", &cv.x, ImGuiColorEditFlags_NoInputs);
        if (ImGui::Button("Apply")) {
            ImU32 c = ImGui::ColorConvertFloat4ToU32(cv);
            st.color_rules.push_back({col, std::string(vbuf.c_str()), (unsigned int)c});
            ImGui::CloseCurrentPopup();
        }
        ImGui::SameLine();
        if (ImGui::Button("Clear col")) {
            for (size_t i = 0; i < st.color_rules.size();) {
                if (st.color_rules[i].col == col) st.color_rules.erase(st.color_rules.begin() + i);
                else ++i;
            }
        }
        ImGui::EndMenu();
    }

    if (ImGui::MenuItem("Hide column")) st.col_visible[col] = false;

    if (is_raw_stage && col >= orig_cols && ImGui::MenuItem("Remove derived column")) {
        int k = col - orig_cols;
        stg.derived.erase(stg.derived.begin() + k);
    }

    ImGui::Separator();
    if (ImGui::BeginMenu("Columns")) {
        for (int k = 0; k < eff_cols; ++k) {
            bool v = st.col_visible[k];
            char lab[160];
            std::snprintf(lab, sizeof(lab), "%s %s",
                          column_type_icon(eff_types[k]), eff_headers_arr[k]);
            if (ImGui::Checkbox(lab, &v)) st.col_visible[k] = v;
        }
        if (ImGui::MenuItem("Show all")) {
            for (int k = 0; k < eff_cols; ++k) st.col_visible[k] = true;
        }
        ImGui::EndMenu();
    }
}

// ---------------------------------------------------------------------------
// Drill-down: anade un filter al stage previo y cambia active a stage previo.
// `col_name` y `value` se aplican como un Filter Op::Eq sobre el stage N-1.
// ---------------------------------------------------------------------------
void drill_into(State& st, int from_stage,
                const std::string& col_name, const std::string& value,
                const std::vector<std::string>& prev_input_headers)
{
    if (from_stage <= 0 || from_stage >= (int)st.stages.size()) return;
    int target = from_stage - 1;
    int ci = -1;
    for (size_t i = 0; i < prev_input_headers.size(); ++i) {
        if (prev_input_headers[i] == col_name) { ci = (int)i; break; }
    }
    if (ci < 0) return;

    // Fase 10: graba step en drill_back, limpia forward (rama nueva).
    DrillStep step;
    step.target_stage      = target;
    step.filter_pos        = (int)st.stages[target].filters.size();
    step.prev_active_stage = st.active_stage;
    step.added             = make_drill_filter(ci, value);
    apply_drill_step(st, step);
    auto& U = ui();
    U.drill_back.push_back(step);
    U.drill_forward.clear();
}

} // anon namespace

void render(const char* id,
            const std::vector<TableInput>& tables,
            State& st,
            std::vector<TableEvent>* events_out,
            bool show_chrome)
{
    if (tables.empty()) return;
    int main_idx = resolve_main_idx(tables, st.main_source);
    if (main_idx < 0) return;

    // Merge aux_column_specs from State into TableInput when the caller passed
    // empty column_specs.  Caller-provided specs always take precedence.
    // We keep a local copy to avoid mutating the caller's const tables.
    static thread_local TableInput main_t_merged;
    {
        const TableInput& src = tables[(size_t)main_idx];
        if (src.column_specs.empty() &&
            main_idx < (int)st.aux_column_specs.size() &&
            !st.aux_column_specs[(size_t)main_idx].empty())
        {
            main_t_merged = src;
            main_t_merged.column_specs = st.aux_column_specs[(size_t)main_idx];
        } else {
            main_t_merged = src;
        }
    }
    const TableInput& main_t = main_t_merged;
    static thread_local std::vector<const char*> main_hdr_ptrs;
    main_hdr_ptrs.clear();
    main_hdr_ptrs.reserve(main_t.cols);
    for (int c = 0; c < main_t.cols; ++c) main_hdr_ptrs.push_back(main_t.headers[c].c_str());
    const char* const* headers_in       = main_hdr_ptrs.data();
    int                col_count        = main_t.cols;
    const char* const* cells_in         = main_t.cells;
    int                row_count_in     = main_t.rows;
    const ColumnType*  declared_types_in = main_t.types.data();

    // Joinables = todas las demas tablas.
    static thread_local std::vector<TableInput> joinables_v;
    joinables_v.clear();
    for (int i = 0; i < (int)tables.size(); ++i) {
        if (i != main_idx) joinables_v.push_back(tables[(size_t)i]);
    }
    const std::vector<TableInput>* joinables = joinables_v.empty() ? nullptr : &joinables_v;

    // Per-table chrome visibility (issue: previously global in UiCache → flipping
    // one table's "Show UI" affected all tables on screen). Now lives in State.
    bool chrome_visible = st.chrome_user_set ? st.chrome_user_visible : show_chrome;

    // Toggle Hide/Show UI siempre visible (botoncito arriba a la derecha).
    {
        float right = ImGui::GetWindowContentRegionMax().x;
        ImGui::SetCursorPosX(right - 90.0f);
        if (ImGui::SmallButton(chrome_visible ? "Hide UI##chrome" : "Show UI##chrome")) {
            st.chrome_user_set     = true;
            st.chrome_user_visible = !chrome_visible;
        }
    }

    // Main source dropdown — solo si > 1 tabla disponibles.
    if (chrome_visible && tables.size() > 1) {
        ImGui::SameLine();
        float right = ImGui::GetWindowContentRegionMax().x;
        ImGui::SetCursorPosX(right - 90.0f - 280.0f);
        ImGui::TextDisabled("Main table:");
        ImGui::SameLine();
        ImGui::SetNextItemWidth(180);
        const char* cur_main = main_t.name.c_str();
        if (ImGui::BeginCombo("##main_table", cur_main)) {
            for (const auto& t : tables) {
                bool sel = (t.name == cur_main);
                if (ImGui::Selectable(t.name.c_str(), sel)) {
                    st.main_source = t.name;
                }
            }
            ImGui::EndCombo();
        }
    }

    st.ensure_stage0();

    // -------- Pre-pipeline: materialize joins --------
    // Si state.joins no vacio + joinables provistos, ejecutar chain de join_tables.
    // El resultado reemplaza headers/cells/declared_types para el resto del render.
    static thread_local std::vector<std::string> joined_headers_store;
    static thread_local std::vector<ColumnType>  joined_types_store;
    static thread_local std::vector<const char*> joined_headers_ptrs;
    static thread_local std::vector<const char*> joined_cells_ptrs;
    static thread_local std::vector<ColumnType>  joined_declared_types;
    static thread_local StageOutput              joined_so;

    const char* const* headers       = headers_in;
    const char* const* cells         = cells_in;
    int                row_count     = row_count_in;
    int                orig_cols     = col_count;
    const ColumnType*  declared_types = declared_types_in;

    bool joined = false;
    if (!st.joins.empty() && joinables && !joinables->empty()) {
        joined_so = StageOutput{};
        // Build initial left from main.
        std::vector<std::string> cur_h(orig_cols);
        std::vector<ColumnType>  cur_t(orig_cols);
        for (int c = 0; c < orig_cols; ++c) {
            cur_h[c] = headers_in[c];
            cur_t[c] = declared_types_in ? declared_types_in[c] : ColumnType::Auto;
        }
        const char* const* cur_cells = cells_in;
        int                cur_rows  = row_count_in;
        int                cur_cols  = orig_cols;

        // Chain join por cada joins[i].
        std::vector<StageOutput> chain;
        chain.reserve(st.joins.size());
        for (const auto& jn : st.joins) {
            const TableInput* match = nullptr;
            for (const auto& ti : *joinables) {
                if (ti.name == jn.source) { match = &ti; break; }
            }
            if (!match) continue;
            StageOutput so = join_tables(cur_cells, cur_rows, cur_cols,
                                          cur_h, cur_t, *match, jn);
            chain.push_back(std::move(so));
            const StageOutput& last = chain.back();
            cur_cells = last.cells.data();
            cur_rows  = last.rows;
            cur_cols  = last.cols;
            cur_h     = last.headers;
            cur_t     = last.types;
        }

        if (!chain.empty()) {
            joined = true;
            joined_so = std::move(chain.back());
            joined_headers_store = joined_so.headers;
            joined_types_store   = joined_so.types;
            joined_headers_ptrs.clear();
            joined_cells_ptrs.clear();
            for (const auto& s : joined_headers_store) joined_headers_ptrs.push_back(s.c_str());
            for (const auto& s : joined_so.cell_backing) joined_cells_ptrs.push_back(s.c_str());
            joined_declared_types = joined_types_store;

            headers        = joined_headers_ptrs.data();
            cells          = joined_cells_ptrs.data();
            row_count      = joined_so.rows;
            orig_cols      = joined_so.cols;
            declared_types = joined_declared_types.data();
        }
    }

    Stage& stage0 = st.stages[0];
    int eff_cols  = orig_cols + (int)stage0.derived.size();

    ensure_init(st, eff_cols);
    auto& U = ui();

    // Build eff_headers / src_for_eff / eff_types para STAGE 0.
    std::vector<const char*> eff_headers(eff_cols);
    std::vector<int>         src_for_eff(eff_cols);
    std::vector<ColumnType>  eff_types(eff_cols);
    for (int c = 0; c < eff_cols; ++c) {
        if (c < orig_cols) {
            eff_headers[c] = headers[c];
            src_for_eff[c] = c;
            ColumnType d   = declared_types ? declared_types[c] : ColumnType::Auto;
            eff_types[c]   = effective_type(d, cells, row_count, orig_cols, c);
        } else {
            const DerivedColumn& d = stage0.derived[c - orig_cols];
            eff_headers[c] = d.name.c_str();
            src_for_eff[c] = d.source_col;
            eff_types[c]   = d.type;
        }
    }

    static thread_local std::vector<std::string> hn_storage;
    static thread_local std::unordered_map<std::string, int> name_to_col;
    static thread_local std::unordered_map<std::string, int> derived_n2i;
    hn_storage.clear();
    name_to_col.clear();
    derived_n2i.clear();
    hn_storage.reserve(orig_cols);
    for (int c = 0; c < orig_cols; ++c) {
        hn_storage.emplace_back(headers[c]);
        name_to_col[hn_storage.back()] = c;
    }
    for (int i = 0; i < (int)stage0.derived.size(); ++i) {
        derived_n2i[stage0.derived[i].name] = i;
    }

    // Re-fit auto en cambio de display, stage o config.
    auto hash_cfg = [](const ViewConfig& c) -> size_t {
        std::string s = c.x_col + "|" + c.cat_col + "|" + c.size_col;
        for (auto& y : c.y_cols) { s += "|"; s += y; }
        s += "|"; s += std::to_string(c.primary_color);
        s += "|"; s += std::to_string(c.hist_bins);
        s += "|"; s += std::to_string(c.pie_radius);
        s += "|"; s += c.show_legend ? "1" : "0";
        s += "|"; s += c.show_markers ? "1" : "0";
        return std::hash<std::string>{}(s);
    };
    size_t cur_cfg_h = hash_cfg(st.viz_config);
    if (U.prev_viz_display != st.display || U.prev_viz_stage != st.active_stage ||
        U.prev_viz_cfg_h != cur_cfg_h) {
        st.viz_config.fit_request = true;
        U.prev_viz_display = st.display;
        U.prev_viz_stage   = st.active_stage;
        U.prev_viz_cfg_h   = cur_cfg_h;
    }

    // ----- Breadcrumb + viz selector (chrome) -----
    if (chrome_visible) {
        draw_stage_breadcrumb(st);
        draw_viz_selector(st);
    }
    int active = st.active_stage;
    bool is_raw = (active == 0);

    // ----- Chips del stage activo -----
    Stage& act = st.stages[active];

    if (is_raw && chrome_visible) {
        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
        // Joins chip row — solo si hay joinables disponibles.
        if (joinables && !joinables->empty()) {
            std::vector<std::string> mh(orig_cols);
            for (int c = 0; c < orig_cols; ++c) mh[c] = headers[c];
            draw_joins_chips(st, *joinables, mh);
        }

        draw_filter_chips(act, eff_headers.data(), eff_cols, eff_types);
        draw_add_filter_popup(act, eff_headers.data(), eff_cols, eff_types);
        draw_edit_filter_popup(act, eff_headers.data(), eff_cols, eff_types);

        // Custom columns chips (solo stage 0)
        {
            ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(110, 110, 110, 200));
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(140, 140, 140, 230));
            ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 85,  85,  85, 230));
            if (ImGui::SmallButton("+##addcustomcol")) {
                U.cf_open         = true;
                U.cf_editing      = false;
                U.cf_edit_idx     = -1;
                U.cf_target_stage = 0;
                U.cf_formula.clear();
                U.cf_name.clear();
                U.cf_type         = ColumnType::String;
                U.cf_error.clear();
            }
            ImGui::PopStyleColor(3);
            ImGui::SameLine();

            bool any = false;
            for (size_t i = 0; i < stage0.derived.size(); ++i) {
                if (stage0.derived[i].formula.empty()) continue;
                any = true;
                const auto& d = stage0.derived[i];
                char buf[256];
                std::snprintf(buf, sizeof(buf), "%s %s  x##custom%zu",
                              column_type_icon(d.type), d.name.c_str(), i);
                ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(140, 140, 140, 220));
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(170, 170, 170, 240));
                ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32(110, 110, 110, 240));
                bool clicked = ImGui::SmallButton(buf);
                ImGui::PopStyleColor(3);
                if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
                    U.cf_open         = true;
                    U.cf_editing      = true;
                    U.cf_edit_idx     = (int)i;
                    U.cf_target_stage = 0;
                    U.cf_formula      = d.formula;
                    U.cf_name         = d.name;
                    U.cf_type         = d.type;
                    U.cf_error.clear();
                }
                if (clicked) {
                    if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
                    stage0.derived.erase(stage0.derived.begin() + i);
                    break;
                }
                ImGui::SameLine();
            }
            if (!any) ImGui::TextDisabled("Custom columns: + para anadir.");
            else      ImGui::NewLine();
        }

        // Sort chips para stage 0 (input headers para popup).
        draw_sort_chips(act);
        draw_add_sort_popup(act, eff_headers.data(), eff_cols, eff_types);
        draw_edit_sort_popup(act, eff_headers.data(), eff_cols);
        ImGui::PopStyleVar(); // ItemSpacing
    }

    // Para stages 1+, compute input headers/types del stage previo.
    // Esto requiere compute_stage chain. Lo haremos abajo.

    // ---------- Compute view: chain compute_stage 0..active ----------
    // Stage 0 expressions: derived cols. Pero compute_stage no sabe de Lua.
    // Estrategia: stage 0 lo aplicamos a mano (orig cells + filter + sort)
    // y exponemos un eff_cells "virtual" donde derived cols se llenan via Lua
    // en el render. Esto preserva el path actual.
    //
    // Para stages 1+, compute_stage opera sobre cells materializadas. Hay que
    // materializar el stage 0 output como cells reales (con derived evaluadas).

    // Simpler: si active == 0, mantener el path actual (orig cells + Lua).
    // Si active > 0, materializar stage 0 + chain compute_stage(stage 1..active).

    if (is_raw) {
        // ----- Path stage 0: orig cells + filters/sort manuales + Lua per cell.

        // compute_visible_rows opera sobre orig cells. filter.col es eff col,
        // hay que traducir a src col (igual que codigo anterior).
        State st_tmp = st;
        st_tmp.ensure_stage0();
        for (auto& f : st_tmp.stages[0].filters) {
            if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col];
        }
        // Sort: la pasamos por @idx convention.
        st_tmp.stages[0].sorts.clear();
        if (!stage0.sorts.empty()) {
            // resolve col name -> col idx (de eff_cols) -> src
            const SortClause& sc0 = stage0.sorts.front();
            int sc_eff = -1;
            for (int c = 0; c < eff_cols; ++c) {
                if (std::strcmp(eff_headers[c], sc0.col.c_str()) == 0) { sc_eff = c; break; }
            }
            if (sc_eff >= 0) {
                int sc_src = src_for_eff[sc_eff];
                char tmp[16]; std::snprintf(tmp, sizeof(tmp), "@%d", sc_src);
                st_tmp.stages[0].sorts.push_back({tmp, sc0.desc});
            }
        }
        auto visible_rows = compute_visible_rows(cells, row_count, orig_cols, st_tmp);

        int visible_cols = 0;
        for (int k = 0; k < eff_cols; ++k) if (st.col_visible[k]) ++visible_cols;

        // Snapshot del active output (stage 0) para el config popup.
        U.active_headers.clear();
        U.active_types.clear();
        for (int k = 0; k < eff_cols; ++k) {
            if (!st.col_visible[k]) continue;
            U.active_headers.emplace_back(eff_headers[k]);
            U.active_types.push_back(eff_types[k]);
        }
        // Input == orig + derived (stage 0 no tiene upstream que agrupe).
        U.input_headers_active = U.active_headers;
        U.input_types_active   = U.active_types;

        if (chrome_visible)
        {
        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
        ImGui::Text("Filas: %d / %d   Columnas: %d / %d",
                    (int)visible_rows.size(), row_count, visible_cols, eff_cols);
        ImGui::SameLine();
        if (ImGui::SmallButton(U.stats_mode ? "Hide stats" : "Show stats")) {
            U.stats_mode = !U.stats_mode;
        }
        ImGui::SameLine();
        if (ImGui::SmallButton("Export CSV")) {
            std::string out;
            bool first = true;
            for (int oc = 0; oc < (int)st.col_order.size(); ++oc) {
                int c = st.col_order[oc];
                if (c < 0 || c >= eff_cols) continue;
                if (!st.col_visible[c]) continue;
                if (!first) out += ',';
                out += csv_escape(eff_headers[c]);
                first = false;
            }
            out += '\n';
            for (int r : visible_rows) {
                first = true;
                for (int oc = 0; oc < (int)st.col_order.size(); ++oc) {
                    int c = st.col_order[oc];
                    if (c < 0 || c >= eff_cols) continue;
                    if (!st.col_visible[c]) continue;
                    int src = src_for_eff[c];
                    if (!first) out += ',';
                    out += csv_escape(cells[r * orig_cols + src]);
                    first = false;
                }
                out += '\n';
            }
            const char* p = fn::local_path("export_table.csv");
            std::ofstream f(p, std::ios::binary | std::ios::trunc);
            if (f) { f << out; U.last_export_path = p; }
        }
        if (!U.last_export_path.empty()) {
            ImGui::SameLine();
            ImGui::TextDisabled("-> %s", U.last_export_path.c_str());
        }
        ImGui::SameLine();
        if (ImGui::SmallButton("Show TQL")) {
            std::vector<std::string> orig_headers(orig_cols);
            std::vector<ColumnType>  orig_types(orig_cols);
            for (int c = 0; c < orig_cols; ++c) {
                orig_headers[c] = headers[c];
                orig_types[c]   = eff_types[c];
            }
            U.tql_show_text = tql::emit(st, orig_headers, orig_types);
            U.tql_show_open = true;
        }
        ImGui::SameLine();
        if (ImGui::SmallButton("Apply TQL")) {
            U.tql_apply_open = true;
            U.tql_apply_error.clear();
        }
        ImGui::SameLine();
        ImGui::SetNextItemWidth(160);
        ImGui::InputText("##tql_file", U.tql_file_path, sizeof(U.tql_file_path));
        ImGui::SameLine();
        if (ImGui::SmallButton("Save .tql")) {
            std::vector<std::string> oh(orig_cols);
            std::vector<ColumnType>  ot(orig_cols);
            for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; }
            std::string text = tql::emit(st, oh, ot);
            const char* path = fn::local_path(U.tql_file_path);
            std::ofstream f(path);
            if (f) { f << text; U.tql_io_status = std::string("saved: ") + path; }
            else   { U.tql_io_status = std::string("save FAILED: ") + path; }
        }
        ImGui::SameLine();
        if (ImGui::SmallButton("Load .tql")) {
            const char* path = fn::local_path(U.tql_file_path);
            std::ifstream f(path);
            if (!f) { U.tql_io_status = std::string("load FAILED: ") + path; }
            else {
                std::string text((std::istreambuf_iterator<char>(f)),
                                  std::istreambuf_iterator<char>());
                std::vector<std::string> oh(orig_cols);
                std::vector<ColumnType>  ot(orig_cols);
                for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; }
                std::string err;
                bool ok = tql::apply(text, st, oh, ot, cells, row_count, orig_cols, &err);
                if (ok) U.tql_io_status = std::string("loaded: ") + path + (err.empty() ? "" : " (warn: " + err + ")");
                else    U.tql_io_status = std::string("load parse error: ") + err;
            }
        }
        if (!U.tql_io_status.empty()) {
            ImGui::SameLine();
            ImGui::TextDisabled("%s", U.tql_io_status.c_str());
        }
        ImGui::PopStyleVar();
        } // chrome_visible
        maybe_recompute_stats(cells, row_count, orig_cols, eff_cols,
                              st_tmp.stages[0].filters,
                              visible_rows, src_for_eff);

        // Toggle Table <-> View: solo visible cuando NO estamos en Table.
        // Desde la tabla no se ofrece volver a chart (la tabla es estado
        // canonico final). Cambia display via menu/chips si quieres ver chart.
        if (st.display != ViewMode::Table) {
            draw_table_toggle(st.display, U.last_non_table_main, "main", &st);
        }

        // SO compartido: main viz + extras. Construido on-demand.
        StageOutput so_main;
        bool so_built = false;
        auto build_so = [&]() -> StageOutput& {
            if (so_built) return so_main;
            so_built = true;
            std::vector<int> vcols;
            for (int c = 0; c < eff_cols; ++c) if (st.col_visible[c]) vcols.push_back(c);
            so_main.cols = (int)vcols.size();
            so_main.rows = (int)visible_rows.size();
            so_main.headers.reserve(so_main.cols);
            so_main.types.reserve(so_main.cols);
            for (int c : vcols) {
                so_main.headers.emplace_back(eff_headers[c]);
                so_main.types.push_back(eff_types[c]);
            }
            so_main.cell_backing.reserve((size_t)so_main.rows * so_main.cols);
            for (int r : visible_rows) {
                for (int c : vcols) {
                    if (c < orig_cols) {
                        const char* p = cells[r * orig_cols + c];
                        so_main.cell_backing.emplace_back(p ? p : "");
                    } else {
                        const DerivedColumn& d = stage0.derived[c - orig_cols];
                        if (!d.formula.empty() && d.lua_id >= 0) {
                            lua_engine::RowCtx ctx;
                            ctx.cells               = cells;
                            ctx.orig_cols           = orig_cols;
                            ctx.row                 = r;
                            ctx.header_names        = &hn_storage;
                            ctx.name_to_col         = &name_to_col;
                            ctx.types_orig          = eff_types.data();
                            ctx.n_types_orig        = orig_cols;
                            ctx.derived             = &stage0.derived;
                            ctx.derived_name_to_idx = &derived_n2i;
                            std::string err;
                            so_main.cell_backing.emplace_back(
                                lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err));
                        } else {
                            int src = d.source_col;
                            const char* sp = (src >= 0 && src < orig_cols) ? cells[r * orig_cols + src] : "";
                            so_main.cell_backing.emplace_back(sp ? sp : "");
                        }
                    }
                }
            }
            so_main.cells.reserve(so_main.cell_backing.size());
            for (auto& s : so_main.cell_backing) so_main.cells.push_back(s.c_str());
            return so_main;
        };

        if (visible_cols == 0) {
            ImGui::TextDisabled("(todas las columnas ocultas)");
            // Modales fuera del table block.
        } else if (st.display != ViewMode::Table) {
            viz::render(build_so(), st.display, st.viz_config, ImVec2(-1, -1));
        } else {
            ImGuiTableFlags flags =
                ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
                ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
            // Issue 0081-O.7/9: disable ALL Selectable bg painting. Hover + selection
            // are painted via TableSetBgColor(CellBg, ...) below, edge-to-edge.
            ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
            ImGui::PushStyleColor(ImGuiCol_HeaderActive,  ImVec4(0, 0, 0, 0));
            ImGui::PushStyleColor(ImGuiCol_Header,        ImVec4(0, 0, 0, 0));
            if (ImGui::BeginTable(id, visible_cols, flags, ImVec2(0, 0))) {

                for (int dc = 0; dc < (int)st.col_order.size(); ++dc) {
                    int c = st.col_order[dc];
                    if (c < 0 || c >= eff_cols) continue;
                    if (!st.col_visible[c]) continue;
                    ImGui::TableSetupColumn(eff_headers[c], ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c);
                }
                ImGui::TableSetupScrollFreeze(0, 1);

                ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
                for (int dc = 0; dc < (int)st.col_order.size(); ++dc) {
                    int c = st.col_order[dc];
                    if (c < 0 || c >= eff_cols) continue;
                    if (!st.col_visible[c]) continue;
                    ImGui::TableSetColumnIndex(dc);  // visual idx aprox; recomputado por engine
                    ImGui::PushID(c);

                    // Detecta si esta col esta en sorts (primario o secundario)
                    int  sort_pos  = -1;
                    bool sort_desc = false;
                    for (size_t si = 0; si < act.sorts.size(); ++si) {
                        if (act.sorts[si].col == eff_headers[c]) {
                            sort_pos = (int)si; sort_desc = act.sorts[si].desc; break;
                        }
                    }
                    char arrow[16] = "";
                    if (sort_pos == 0)      std::snprintf(arrow, sizeof(arrow), "  %s", sort_desc ? "v" : "^");
                    else if (sort_pos > 0)  std::snprintf(arrow, sizeof(arrow), "  %s%d", sort_desc ? "v" : "^", sort_pos + 1);
                    char label[200];
                    std::snprintf(label, sizeof(label), "%s %s%s",
                                  column_type_icon(eff_types[c]), eff_headers[c], arrow);

                    ImGui::PushStyleColor(ImGuiCol_Header,        IM_COL32(45, 50, 65, 200));
                    ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(65, 75, 95, 220));
                    ImGui::PushStyleColor(ImGuiCol_HeaderActive,  IM_COL32(80, 95, 130, 240));
                    bool clicked = ImGui::Selectable(label, false, ImGuiSelectableFlags_DontClosePopups);
                    ImGui::PopStyleColor(3);

                    if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
                        ImGui::SetDragDropPayload("##colreorder", &c, sizeof(int));
                        ImGui::Text("Move %s", eff_headers[c]);
                        ImGui::EndDragDropSource();
                    }
                    if (ImGui::BeginDragDropTarget()) {
                        if (const ImGuiPayload* p = ImGui::AcceptDragDropPayload("##colreorder")) {
                            int src = *(const int*)p->Data;
                            reorder_column(st, src, c);
                        }
                        ImGui::EndDragDropTarget();
                    }
                    if (clicked) {
                        bool shift = ImGui::GetIO().KeyShift;
                        apply_header_sort_click(act, eff_headers[c], shift);
                    }
                    if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
                        U.header_popup_col = c;
                        ImGui::OpenPopup("##hdr_menu");
                    }
                    if (ImGui::BeginPopup("##hdr_menu") && U.header_popup_col == c) {
                        draw_header_menu(st, act, c, eff_headers.data(), eff_cols, eff_types, orig_cols, true);
                        ImGui::EndPopup();
                    }

                    if (U.stats_mode && c < (int)U.stats_cache.size()) {
                        const ColStats& s = U.stats_cache[c];
                        ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 190, 220, 220));
                        ImGui::Text("missing: %d", s.empty_count);
                        ImGui::Text("uniq: %d%s",   s.unique_count, s.unique_capped ? "+" : "");
                        if (s.numeric) {
                            ImGui::Text("mean: %.2f", s.mean);
                            ImGui::Text("p25: %.2f",  s.p25);
                            ImGui::Text("p50: %.2f",  s.p50);
                            ImGui::Text("p75: %.2f",  s.p75);
                            if (!s.hist.empty()) {
                                char overlay[64];
                                std::snprintf(overlay, sizeof(overlay), "[%.2f..%.2f]", s.min, s.max);
                                ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 230));
                                ImGui::PlotHistogram("##hist", s.hist.data(), (int)s.hist.size(),
                                                     0, overlay, 0.0f, FLT_MAX, ImVec2(-1, 36));
                                ImGui::PopStyleColor();
                            }
                        } else if (!s.top_categories.empty()) {
                            int mx = 0;
                            for (const auto& kv : s.top_categories) if (kv.second > mx) mx = kv.second;
                            ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 220));
                            for (const auto& kv : s.top_categories) {
                                float frac = mx > 0 ? (float)kv.second / (float)mx : 0.f;
                                char ovl[96];
                                std::snprintf(ovl, sizeof(ovl), "%s (%d)", kv.first.c_str(), kv.second);
                                ImGui::ProgressBar(frac, ImVec2(-1, 12), ovl);
                            }
                            ImGui::PopStyleColor();
                        }
                        ImGui::PopStyleColor();
                    }
                    ImGui::PopID();
                }

                int sel_rmin = std::min(U.sel_anchor_row, U.sel_end_row);
                int sel_rmax = std::max(U.sel_anchor_row, U.sel_end_row);
                int sel_cmin = std::min(U.sel_anchor_col, U.sel_end_col);
                int sel_cmax = std::max(U.sel_anchor_col, U.sel_end_col);

                ImGuiListClipper clipper;
                clipper.Begin((int)visible_rows.size());
                while (clipper.Step()) {
                    for (int ri = clipper.DisplayStart; ri < clipper.DisplayEnd; ++ri) {
                        int r = visible_rows[ri];
                        ImGui::TableNextRow();
                        int draw_idx = 0;
                        for (int oc = 0; oc < (int)st.col_order.size(); ++oc) {
                            int c = st.col_order[oc];
                            if (c < 0 || c >= eff_cols) continue;
                            if (!st.col_visible[c]) continue;
                            ImGui::TableSetColumnIndex(draw_idx++);
                            // Issue 0081-O.7: capture cell rect before rendering for
                            // per-cell hover overlay (manual hit-test avoids relying on
                            // GetItemRect of the last sub-item inside draw_cell_custom).
                            ImVec2 cell_min = ImGui::GetCursorScreenPos();
                            float  cell_w   = ImGui::GetContentRegionAvail().x;
                            float  cell_h   = ImGui::GetTextLineHeight();
                            ImVec2 cell_max(cell_min.x + cell_w, cell_min.y + cell_h);

                            int src = src_for_eff[c];
                            std::string eval_buf;
                            const char* cell;
                            if (c >= orig_cols && !stage0.derived[c - orig_cols].formula.empty()) {
                                const auto& d = stage0.derived[c - orig_cols];
                                if (d.lua_id < 0) cell = "?";
                                else {
                                    lua_engine::RowCtx ctx;
                                    ctx.cells               = cells;
                                    ctx.orig_cols           = orig_cols;
                                    ctx.row                 = r;
                                    ctx.header_names        = &hn_storage;
                                    ctx.name_to_col         = &name_to_col;
                                    ctx.types_orig          = eff_types.data();
                                    ctx.n_types_orig        = orig_cols;
                                    ctx.derived             = &stage0.derived;
                                    ctx.derived_name_to_idx = &derived_n2i;
                                    std::string err;
                                    eval_buf = lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err);
                                    cell = eval_buf.c_str();
                                }
                            } else {
                                cell = cells[r * orig_cols + src];
                            }

                            for (const auto& cr : st.color_rules) {
                                if (cr.col == c && cell && cr.equals == cell) {
                                    ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, (ImU32)cr.color);
                                    break;
                                }
                            }
                            bool in_sel = (U.sel_active &&
                                           ri >= sel_rmin && ri <= sel_rmax &&
                                           oc >= sel_cmin && oc <= sel_cmax);
                            ImGui::PushID(r * eff_cols + c);
                            // Issue 0081-N/O: use declarative renderer when column_specs set.
                            {
                                bool custom_rendered = false;
                                const ColumnSpec* cell_cs = nullptr;
                                if (!main_t.column_specs.empty() &&
                                    c < (int)main_t.column_specs.size()) {
                                    cell_cs = &main_t.column_specs[(size_t)c];
                                    if (cell_cs->renderer != CellRenderer::Text) {
                                        draw_cell_custom(*cell_cs, cell, r, c, events_out);
                                        custom_rendered = true;
                                    }
                                }
                                if (!custom_rendered) {
                                    // Issue 0081-O.8: disable Selectable's own bg paint so
                                    // it doesn't double-up with the manual cell overlay below.
                                    // Pass explicit size so empty cells still have a hit area
                                    // (otherwise drag-select skips empty cells).
                                    ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
                                    ImGui::Selectable(cell ? cell : "", in_sel,
                                                      ImGuiSelectableFlags_AllowDoubleClick,
                                                      ImVec2(0, ImGui::GetTextLineHeight()));
                                    ImGui::PopStyleColor();
                                    // Tooltip for Text cells (Phase 2).
                                    if (cell_cs && cell_cs->tooltip_on_hover &&
                                        ImGui::IsItemHovered()) {
                                        const char* tip = (cell_cs->tooltip == "auto")
                                                          ? (cell ? cell : "")
                                                          : cell_cs->tooltip.c_str();
                                        if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
                                    }
                                }
                            }
                            // Issue 0081-O.7/9/10: per-cell bg via TableSetBgColor.
                            // Edge-to-edge (full cell incl. CellPadding). Selection (in_sel)
                            // overrides hover. Both colors uniform — no Selectable padding.
                            {
                                ImVec2 mp  = ImGui::GetMousePos();
                                ImVec2 pad = ImGui::GetStyle().CellPadding;
                                bool is_hovered =
                                    mp.x >= cell_min.x - pad.x && mp.x < cell_min.x + cell_w + pad.x &&
                                    mp.y >= cell_min.y - pad.y && mp.y < cell_min.y + cell_h + pad.y &&
                                    ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
                                if (in_sel) {
                                    // Selected (drag-range): blue, slightly stronger when hovered.
                                    ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
                                                            is_hovered
                                                            ? IM_COL32(102, 140, 217, 80)
                                                            : IM_COL32(102, 140, 217, 60));
                                } else if (is_hovered) {
                                    ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
                                                            IM_COL32(255, 255, 255, 22));
                                }
                            }
                            // AllowWhenBlockedByActiveItem: durante drag,
                            // otras celdas tambien reciben hover -> sel se
                            // pinta mientras arrastras.
                            bool hovered = ImGui::IsItemHovered(
                                ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
                            if (hovered) {
                                if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
                                    U.sel_anchor_row = ri; U.sel_anchor_col = oc;
                                    U.sel_end_row    = ri; U.sel_end_col    = oc;
                                    U.sel_active     = true;
                                    U.sel_dragging   = true;
                                } else if (U.sel_dragging) {
                                    U.sel_end_row = ri; U.sel_end_col = oc;
                                }
                                // RowDoubleClick event (Phase 2, v1.2.0).
                                if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
                                    && events_out) {
                                    TableEvent ev;
                                    ev.kind  = TableEventKind::RowDoubleClick;
                                    ev.row   = r;
                                    ev.col   = c;
                                    ev.value = cell ? cell : "";
                                    if (!main_t.column_specs.empty() &&
                                        c < (int)main_t.column_specs.size())
                                        ev.column_id = main_t.column_specs[(size_t)c].id;
                                    events_out->push_back(std::move(ev));
                                }
                                // RowRightClick event: emit event only, no popup drawn here.
                                // Caller inspects events_out and opens its own context menu.
                                if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
                                    U.pending_col     = c;
                                    U.pending_value   = cell ? cell : "";
                                    U.open_cell_popup = true;
                                    if (events_out) {
                                        TableEvent ev;
                                        ev.kind  = TableEventKind::RowRightClick;
                                        ev.row   = r;
                                        ev.col   = c;
                                        ev.value = cell ? cell : "";
                                        if (!main_t.column_specs.empty() &&
                                            c < (int)main_t.column_specs.size())
                                            ev.column_id = main_t.column_specs[(size_t)c].id;
                                        events_out->push_back(std::move(ev));
                                    }
                                }
                            }
                            ImGui::PopID();
                        }
                    }
                }
                if (U.sel_dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
                    U.sel_dragging = false;
                }
                ImGui::EndTable();
            }
            ImGui::PopStyleColor(3); // HeaderHovered/HeaderActive/Header (issue 0081-O.7)

            // Ctrl+C -> TSV.
            if (U.sel_active && ImGui::GetIO().KeyCtrl &&
                ImGui::IsKeyPressed(ImGuiKey_C, false))
            {
                int rmin = std::min(U.sel_anchor_row, U.sel_end_row);
                int rmax = std::max(U.sel_anchor_row, U.sel_end_row);
                int cmin = std::min(U.sel_anchor_col, U.sel_end_col);
                int cmax = std::max(U.sel_anchor_col, U.sel_end_col);
                std::string out;
                bool first = true;
                for (int oc = cmin; oc <= cmax; ++oc) {
                    if (oc < 0 || oc >= (int)st.col_order.size()) continue;
                    int c = st.col_order[oc];
                    if (c < 0 || c >= eff_cols) continue;
                    if (!st.col_visible[c]) continue;
                    if (!first) out += '\t';
                    out += eff_headers[c];
                    first = false;
                }
                out += '\n';
                for (int ri = rmin; ri <= rmax; ++ri) {
                    if (ri < 0 || ri >= (int)visible_rows.size()) continue;
                    int r = visible_rows[ri];
                    first = true;
                    for (int oc = cmin; oc <= cmax; ++oc) {
                        if (oc < 0 || oc >= (int)st.col_order.size()) continue;
                        int c = st.col_order[oc];
                        if (c < 0 || c >= eff_cols) continue;
                        if (!st.col_visible[c]) continue;
                        int src = src_for_eff[c];
                        const char* v = cells[r * orig_cols + src];
                        std::string sv = v ? v : "";
                        for (char& ch : sv) if (ch == '\t' || ch == '\n') ch = ' ';
                        if (!first) out += '\t';
                        out += sv;
                        first = false;
                    }
                    out += '\n';
                }
                ImGui::SetClipboardText(out.c_str());
            }
        }

        // Render extras panels (stage 0 path). Solo cuando display != Table —
        // desde la tabla no se muestran chart panels adicionales.
        if (st.display != ViewMode::Table && !st.extra_panels.empty() && visible_cols > 0) {
            int close_idx = -1;
            const std::vector<ColumnSpec>* ep_specs =
                main_t.column_specs.empty() ? nullptr : &main_t.column_specs;
            for (int i = 0; i < (int)st.extra_panels.size(); ++i) {
                if (draw_extra_panel(st, st.extra_panels[i], i, build_so(), ep_specs)) close_idx = i;
            }
            if (close_idx >= 0) st.extra_panels.erase(st.extra_panels.begin() + close_idx);
        }
    } else {
        // ----- Path stage > 0: materializar stage 0 con cells reales + chain.
        // Materializar stage 0: aplicar filters/sort sobre orig + evaluar derived.
        State st_tmp = st;
        for (auto& f : st_tmp.stages[0].filters) {
            if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col];
        }
        st_tmp.stages[0].sorts.clear();
        if (!stage0.sorts.empty()) {
            const SortClause& sc0 = stage0.sorts.front();
            int sc_eff = -1;
            for (int c = 0; c < eff_cols; ++c) {
                if (std::strcmp(eff_headers[c], sc0.col.c_str()) == 0) { sc_eff = c; break; }
            }
            if (sc_eff >= 0) {
                int sc_src = src_for_eff[sc_eff];
                char tmp[16]; std::snprintf(tmp, sizeof(tmp), "@%d", sc_src);
                st_tmp.stages[0].sorts.push_back({tmp, sc0.desc});
            }
        }
        auto vrows = compute_visible_rows(cells, row_count, orig_cols, st_tmp);

        // Materializar stage0 output: cells (eff_cols) con derived evaluadas.
        std::vector<std::string> mat_backing;
        std::vector<const char*> mat_cells;
        mat_backing.reserve((size_t)vrows.size() * eff_cols);
        mat_cells.reserve((size_t)vrows.size() * eff_cols);

        for (int r : vrows) {
            for (int c = 0; c < eff_cols; ++c) {
                const char* p;
                std::string buf;
                if (c < orig_cols) {
                    p = cells[r * orig_cols + c];
                    mat_backing.emplace_back(p ? p : "");
                } else {
                    const DerivedColumn& d = stage0.derived[c - orig_cols];
                    if (!d.formula.empty()) {
                        if (d.lua_id < 0) {
                            mat_backing.emplace_back("");
                        } else {
                            lua_engine::RowCtx ctx;
                            ctx.cells               = cells;
                            ctx.orig_cols           = orig_cols;
                            ctx.row                 = r;
                            ctx.header_names        = &hn_storage;
                            ctx.name_to_col         = &name_to_col;
                            ctx.types_orig          = eff_types.data();
                            ctx.n_types_orig        = orig_cols;
                            ctx.derived             = &stage0.derived;
                            ctx.derived_name_to_idx = &derived_n2i;
                            std::string err;
                            mat_backing.emplace_back(
                                lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &err));
                        }
                    } else {
                        // retipo puro
                        int src = d.source_col;
                        const char* sp = (src >= 0 && src < orig_cols) ? cells[r * orig_cols + src] : "";
                        mat_backing.emplace_back(sp ? sp : "");
                    }
                }
            }
        }
        // Punteros tras llenar backing (reserve garantiza no realloc).
        for (auto& s : mat_backing) mat_cells.push_back(s.c_str());

        std::vector<std::string> cur_headers(eff_cols);
        std::vector<ColumnType>  cur_types(eff_cols);
        for (int c = 0; c < eff_cols; ++c) {
            cur_headers[c] = eff_headers[c];
            cur_types[c]   = eff_types[c];
        }

        // Chain compute_stage 1..active.
        // Para encadenar, mantenemos vectores por iteracion. cur_cells apunta al
        // ultimo output.
        const char* const* cur_cells = mat_cells.data();
        int cur_rows = (int)vrows.size();
        int cur_cols_n = eff_cols;

        std::vector<StageOutput> outs;
        outs.reserve(st.stages.size());

        // Headers del INPUT del active (= output del active-1)
        std::vector<std::string> input_headers_active = cur_headers;
        std::vector<ColumnType>  input_types_active   = cur_types;

        for (int si = 1; si <= active; ++si) {
            const Stage& sN = st.stages[si];
            // Antes de computar: si es el active stage, los input_headers son cur_*.
            if (si == active) {
                input_headers_active = cur_headers;
                input_types_active   = cur_types;
            }
            StageOutput so = compute_stage(cur_cells, cur_rows, cur_cols_n,
                                            cur_headers, cur_types, sN);
            outs.push_back(std::move(so));
            const StageOutput& last = outs.back();
            cur_cells  = last.cells.data();
            cur_rows   = last.rows;
            cur_cols_n = last.cols;
            cur_headers = last.headers;
            cur_types   = last.types;
        }

        // ----- Chips del active stage (uses input_headers_active) -----
        std::vector<const char*> ih_ptrs(input_headers_active.size());
        for (size_t i = 0; i < input_headers_active.size(); ++i)
            ih_ptrs[i] = input_headers_active[i].c_str();
        int in_cols_n = (int)input_headers_active.size();

        if (chrome_visible) {
        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
        draw_filter_chips(act, ih_ptrs.data(), in_cols_n, input_types_active);
        draw_add_filter_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);
        draw_edit_filter_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);

        draw_breakout_chips(act, ih_ptrs.data(), in_cols_n, input_types_active);
        draw_add_breakout_popup(act, ih_ptrs.data(), in_cols_n, input_types_active,
                                 cur_cells, cur_rows);
        draw_edit_breakout_popup(act, ih_ptrs.data(), in_cols_n);

        draw_aggregation_chips(act, ih_ptrs.data(), in_cols_n);
        draw_add_aggregation_popup(act, ih_ptrs.data(), in_cols_n, input_types_active);
        draw_edit_agg_popup(act, ih_ptrs.data(), in_cols_n);

        // ----- Custom column chips (stages 1+, target = active stage) -----
        {
            ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(110, 110, 110, 200));
            ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(140, 140, 140, 230));
            ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32( 85,  85,  85, 230));
            if (ImGui::SmallButton("+##addcustomcol_stage")) {
                U.cf_open         = true;
                U.cf_editing      = false;
                U.cf_edit_idx     = -1;
                U.cf_target_stage = active;
                U.cf_formula.clear();
                U.cf_name.clear();
                U.cf_type         = ColumnType::String;
                U.cf_error.clear();
            }
            ImGui::PopStyleColor(3);
            ImGui::SameLine();
            bool any = false;
            for (size_t i = 0; i < act.derived.size(); ++i) {
                if (act.derived[i].formula.empty()) continue;
                any = true;
                const auto& d = act.derived[i];
                char buf[256];
                std::snprintf(buf, sizeof(buf), "%s %s  x##custom_st_%zu",
                              column_type_icon(d.type), d.name.c_str(), i);
                ImGui::PushStyleColor(ImGuiCol_Button,        IM_COL32(140, 140, 140, 220));
                ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(170, 170, 170, 240));
                ImGui::PushStyleColor(ImGuiCol_ButtonActive,  IM_COL32(110, 110, 110, 240));
                bool clicked = ImGui::SmallButton(buf);
                ImGui::PopStyleColor(3);
                if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
                    U.cf_open         = true;
                    U.cf_editing      = true;
                    U.cf_edit_idx     = (int)i;
                    U.cf_target_stage = active;
                    U.cf_formula      = d.formula;
                    U.cf_name         = d.name;
                    U.cf_type         = d.type;
                    U.cf_error.clear();
                }
                if (clicked) {
                    if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
                    act.derived.erase(act.derived.begin() + i);
                    break;
                }
                ImGui::SameLine();
            }
            if (!any) ImGui::TextDisabled("Custom columns (stage %d): + para anadir.", active);
            else      ImGui::NewLine();
        }

        draw_sort_chips(act);
        // Sort col options son los headers del OUTPUT del stage activo.
        std::vector<const char*> out_h_ptrs(cur_headers.size());
        for (size_t i = 0; i < cur_headers.size(); ++i) out_h_ptrs[i] = cur_headers[i].c_str();
        draw_add_sort_popup(act, out_h_ptrs.data(), (int)cur_headers.size(), cur_types);
        draw_edit_sort_popup(act, out_h_ptrs.data(), (int)cur_headers.size());
        ImGui::PopStyleVar();
        } // chrome_visible

        // ----- Materializar act.derived sobre cur_cells -----
        // Para cada derived col formula del active stage, eval per output row.
        std::vector<std::string> ext_backing;
        std::vector<const char*> ext_cells;
        std::vector<std::string> ext_headers;
        std::vector<ColumnType>  ext_types;
        if (!act.derived.empty()) {
            int orig_out_cols = cur_cols_n;
            std::vector<std::string> out_hn = cur_headers;
            std::unordered_map<std::string, int> out_n2c;
            for (size_t i = 0; i < out_hn.size(); ++i) out_n2c[out_hn[i]] = (int)i;
            int n_derived = (int)act.derived.size();
            int new_cols = orig_out_cols + n_derived;
            ext_backing.reserve((size_t)cur_rows * n_derived);
            ext_cells.reserve((size_t)cur_rows * new_cols);
            for (int r = 0; r < cur_rows; ++r) {
                // copia cols originales del output
                for (int c = 0; c < orig_out_cols; ++c) {
                    ext_cells.push_back(cur_cells[r * orig_out_cols + c]);
                }
                // anade derived eval
                for (int k = 0; k < n_derived; ++k) {
                    const DerivedColumn& d = act.derived[k];
                    if (d.formula.empty() || d.lua_id < 0) {
                        ext_backing.emplace_back("");
                    } else {
                        lua_engine::RowCtx ctx;
                        ctx.cells        = cur_cells;
                        ctx.orig_cols    = orig_out_cols;
                        ctx.row          = r;
                        ctx.header_names = &out_hn;
                        ctx.name_to_col  = &out_n2c;
                        ctx.types_orig   = cur_types.data();
                        ctx.n_types_orig = orig_out_cols;
                        std::string e;
                        ext_backing.emplace_back(
                            lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &e));
                    }
                    // marker placeholder; sera replaced abajo tras backing estable
                    ext_cells.push_back(nullptr);
                }
            }
            // Construir ext_cells reemplazando placeholders por punteros estables.
            size_t bi = 0;
            for (int r = 0; r < cur_rows; ++r) {
                for (int k = 0; k < n_derived; ++k) {
                    int idx = r * new_cols + orig_out_cols + k;
                    ext_cells[idx] = ext_backing[bi++].c_str();
                }
            }
            ext_headers = cur_headers;
            ext_types   = cur_types;
            for (int k = 0; k < n_derived; ++k) {
                ext_headers.push_back(act.derived[k].name);
                ext_types.push_back(act.derived[k].type);
            }
            cur_cells   = ext_cells.data();
            cur_cols_n  = new_cols;
            cur_headers = ext_headers;
            cur_types   = ext_types;
        }

        // Header row + cells render simple (sin clipper porque outputs son
        // pequenos tipicamente).
        // Snapshot del active output (stage>0) para config popup.
        U.active_headers = cur_headers;
        U.active_types   = cur_types;
        // Input del active stage = output del previo. Disponible en
        // input_headers_active/input_types_active.
        U.input_headers_active = input_headers_active;
        U.input_types_active   = input_types_active;

        if (chrome_visible) {
        ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
        ImGui::Text("Filas: %d   Columnas: %d", cur_rows, cur_cols_n);
        ImGui::SameLine();
        if (ImGui::SmallButton(U.stats_mode ? "Hide stats" : "Show stats")) {
            U.stats_mode = !U.stats_mode;
        }
        // Recompute stats sobre cur_cells del stage activo.
        if (U.stats_mode && cur_cols_n > 0) {
            U.stats_cache.resize(cur_cols_n);
            U.last_cells = cur_cells;
            for (int c = 0; c < cur_cols_n; ++c) {
                U.stats_cache[c] = compute_column_stats(cur_cells, cur_rows, cur_cols_n, c);
            }
        }
        ImGui::SameLine();
        if (ImGui::SmallButton("Show TQL")) {
            std::vector<std::string> oh(orig_cols);
            std::vector<ColumnType>  ot(orig_cols);
            for (int c = 0; c < orig_cols; ++c) {
                oh[c] = headers[c];
                ot[c] = eff_types[c];
            }
            U.tql_show_text = tql::emit(st, oh, ot);
            U.tql_show_open = true;
        }
        ImGui::SameLine();
        if (ImGui::SmallButton("Apply TQL")) {
            U.tql_apply_open = true;
            U.tql_apply_error.clear();
        }
        ImGui::SameLine();
        ImGui::SetNextItemWidth(160);
        ImGui::InputText("##tql_file2", U.tql_file_path, sizeof(U.tql_file_path));
        ImGui::SameLine();
        if (ImGui::SmallButton("Save .tql##s2")) {
            std::vector<std::string> oh(orig_cols);
            std::vector<ColumnType>  ot(orig_cols);
            for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; }
            std::string text = tql::emit(st, oh, ot);
            const char* path = fn::local_path(U.tql_file_path);
            std::ofstream f(path);
            if (f) { f << text; U.tql_io_status = std::string("saved: ") + path; }
            else   { U.tql_io_status = std::string("save FAILED: ") + path; }
        }
        ImGui::SameLine();
        if (ImGui::SmallButton("Load .tql##l2")) {
            const char* path = fn::local_path(U.tql_file_path);
            std::ifstream f(path);
            if (!f) { U.tql_io_status = std::string("load FAILED: ") + path; }
            else {
                std::string text((std::istreambuf_iterator<char>(f)),
                                  std::istreambuf_iterator<char>());
                std::vector<std::string> oh(orig_cols);
                std::vector<ColumnType>  ot(orig_cols);
                for (int c = 0; c < orig_cols; ++c) { oh[c] = headers[c]; ot[c] = eff_types[c]; }
                std::string err;
                bool ok = tql::apply(text, st, oh, ot, cells, row_count, orig_cols, &err);
                if (ok) U.tql_io_status = std::string("loaded: ") + path + (err.empty() ? "" : " (warn: " + err + ")");
                else    U.tql_io_status = std::string("load parse error: ") + err;
            }
        }
        if (!U.tql_io_status.empty()) {
            ImGui::SameLine();
            ImGui::TextDisabled("%s", U.tql_io_status.c_str());
        }
        ImGui::PopStyleVar();
        } // chrome_visible

        // Toggle Table <-> View: solo visible cuando NO estamos en Table.
        if (st.display != ViewMode::Table) {
            draw_table_toggle(st.display, U.last_non_table_main, "main2", &st);
        }

        if (st.display != ViewMode::Table && cur_cols_n > 0) {
            // outs.back() es el StageOutput del active. Si active no tiene outs
            // (cur_rows poblado pero outs vacio cuando active>0 y chain corta),
            // construir uno on-the-fly desde cur_cells.
            StageOutput so_local;
            const StageOutput* so_ptr = nullptr;
            if (!outs.empty()) {
                so_ptr = &outs.back();
            } else {
                so_local.cols = cur_cols_n;
                so_local.rows = cur_rows;
                so_local.headers = cur_headers;
                so_local.types   = cur_types;
                so_local.cells.reserve((size_t)cur_rows * cur_cols_n);
                for (int i = 0; i < cur_rows * cur_cols_n; ++i)
                    so_local.cells.push_back(cur_cells[i]);
                so_ptr = &so_local;
            }
            int clicked_row = -1;
            viz::render(*so_ptr, st.display, st.viz_config, ImVec2(-1, -1), &clicked_row);
            // Fase 10: click sobre chart -> drill al stage previo usando
            // breakout col[0] como filtro Op::Eq sobre cells[clicked_row].
            if (clicked_row >= 0 && active > 0 &&
                so_ptr->cols > 0 && clicked_row < so_ptr->rows) {
                int n_brk = (int)st.stages[active].breakouts.size();
                if (n_brk > 0) {
                    const char* v = so_ptr->cells[clicked_row * so_ptr->cols + 0];
                    std::string col_clean;
                    parse_breakout_granularity(so_ptr->headers[0], col_clean);
                    drill_into(st, active, col_clean,
                                v ? std::string(v) : "",
                                input_headers_active);
                }
            }
            goto stage_n_table_end;
        }

        {
        ImGuiTableFlags flags =
            ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
            ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY;
        // Issue 0081-O.7: tone down row hover (default 0.31 alpha was too bright).
        ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.05f));
        ImGui::PushStyleColor(ImGuiCol_HeaderActive,  ImVec4(1.0f, 1.0f, 1.0f, 0.08f));
        ImGui::PushStyleColor(ImGuiCol_Header,        ImVec4(0.40f, 0.55f, 0.85f, 0.20f));
        if (cur_cols_n > 0 && ImGui::BeginTable(id, cur_cols_n, flags, ImVec2(0, 0))) {
            for (int c = 0; c < cur_cols_n; ++c) {
                ImGui::TableSetupColumn(cur_headers[c].c_str(),
                                        ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c);
            }
            ImGui::TableSetupScrollFreeze(0, 1);

            // Custom header row: nombre + icon + stats inline si stats_mode.
            ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
            for (int c = 0; c < cur_cols_n; ++c) {
                ImGui::TableSetColumnIndex(c);
                // Sort indicator
                int  sort_pos  = -1;
                bool sort_desc = false;
                for (size_t si = 0; si < act.sorts.size(); ++si) {
                    if (act.sorts[si].col == cur_headers[c]) {
                        sort_pos = (int)si; sort_desc = act.sorts[si].desc; break;
                    }
                }
                char arrow[16] = "";
                if (sort_pos == 0)      std::snprintf(arrow, sizeof(arrow), "  %s", sort_desc ? "v" : "^");
                else if (sort_pos > 0)  std::snprintf(arrow, sizeof(arrow), "  %s%d", sort_desc ? "v" : "^", sort_pos + 1);

                ImGui::PushStyleColor(ImGuiCol_Header,        IM_COL32(45, 50, 65, 200));
                ImGui::PushStyleColor(ImGuiCol_HeaderHovered, IM_COL32(65, 75, 95, 220));
                ImGui::PushStyleColor(ImGuiCol_HeaderActive,  IM_COL32(80, 95, 130, 240));
                char lbl[200];
                std::snprintf(lbl, sizeof(lbl), "%s %s%s",
                              column_type_icon(cur_types[c]),
                              cur_headers[c].c_str(), arrow);
                bool h_clicked = ImGui::Selectable(lbl, false, ImGuiSelectableFlags_DontClosePopups);
                ImGui::PopStyleColor(3);
                if (h_clicked) {
                    bool shift = ImGui::GetIO().KeyShift;
                    apply_header_sort_click(act, cur_headers[c], shift);
                }

                if (U.stats_mode && c < (int)U.stats_cache.size()) {
                    const ColStats& s = U.stats_cache[c];
                    ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(170, 190, 220, 220));
                    ImGui::Text("missing: %d", s.empty_count);
                    ImGui::Text("uniq: %d%s", s.unique_count, s.unique_capped ? "+" : "");
                    if (s.numeric) {
                        ImGui::Text("mean: %.2f", s.mean);
                        ImGui::Text("p25: %.2f",  s.p25);
                        ImGui::Text("p50: %.2f",  s.p50);
                        ImGui::Text("p75: %.2f",  s.p75);
                        if (!s.hist.empty()) {
                            char overlay[64];
                            std::snprintf(overlay, sizeof(overlay), "[%.2f..%.2f]", s.min, s.max);
                            ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 230));
                            ImGui::PlotHistogram("##hist", s.hist.data(), (int)s.hist.size(),
                                                 0, overlay, 0.0f, FLT_MAX, ImVec2(-1, 36));
                            ImGui::PopStyleColor();
                        }
                    } else if (!s.top_categories.empty()) {
                        int mx = 0;
                        for (const auto& kv : s.top_categories) if (kv.second > mx) mx = kv.second;
                        ImGui::PushStyleColor(ImGuiCol_PlotHistogram, IM_COL32(70, 140, 220, 220));
                        for (const auto& kv : s.top_categories) {
                            float frac = mx > 0 ? (float)kv.second / (float)mx : 0.f;
                            char ovl[96];
                            std::snprintf(ovl, sizeof(ovl), "%s (%d)", kv.first.c_str(), kv.second);
                            ImGui::ProgressBar(frac, ImVec2(-1, 12), ovl);
                        }
                        ImGui::PopStyleColor();
                    }
                    ImGui::PopStyleColor();
                }
            }

            int n_brk = (int)st.stages[active].breakouts.size();

            for (int r = 0; r < cur_rows; ++r) {
                ImGui::TableNextRow();
                for (int c = 0; c < cur_cols_n; ++c) {
                    ImGui::TableSetColumnIndex(c);
                    const char* cell = cur_cells[r * cur_cols_n + c];
                    ImGui::PushID(r * cur_cols_n + c);
                    // Issue 0081-N/O: declarative renderer for aggregated stage tables.
                    {
                        bool custom_rendered = false;
                        const ColumnSpec* cell_cs2 = nullptr;
                        if (!main_t.column_specs.empty() &&
                            c < (int)main_t.column_specs.size()) {
                            cell_cs2 = &main_t.column_specs[(size_t)c];
                            if (cell_cs2->renderer != CellRenderer::Text) {
                                draw_cell_custom(*cell_cs2, cell, r, c, events_out);
                                custom_rendered = true;
                            }
                        }
                        if (!custom_rendered) {
                            // Issue 0081-O.8: disable Selectable's bg paint to avoid
                            // double hover with the manual cell overlay; explicit size
                            // ensures empty cells have a hit area.
                            ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0));
                            ImGui::Selectable(cell ? cell : "", false, 0,
                                              ImVec2(0, ImGui::GetTextLineHeight()));
                            ImGui::PopStyleColor();
                            // Tooltip for Text cells (Phase 2).
                            if (cell_cs2 && cell_cs2->tooltip_on_hover &&
                                ImGui::IsItemHovered()) {
                                const char* tip = (cell_cs2->tooltip == "auto")
                                                  ? (cell ? cell : "")
                                                  : cell_cs2->tooltip.c_str();
                                if (tip && tip[0]) ImGui::SetTooltip("%s", tip);
                            }
                        }
                    }
                    if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
                        U.pending_col   = c;
                        U.pending_value = cell ? cell : "";
                        U.inspect_row   = r;
                        ImGui::OpenPopup("##drill_popup");
                        // RowRightClick event (Phase 2, v1.2.0).
                        if (events_out) {
                            TableEvent ev;
                            ev.kind  = TableEventKind::RowRightClick;
                            ev.row   = r;
                            ev.col   = c;
                            ev.value = cell ? cell : "";
                            if (!main_t.column_specs.empty() &&
                                c < (int)main_t.column_specs.size())
                                ev.column_id = main_t.column_specs[(size_t)c].id;
                            events_out->push_back(std::move(ev));
                        }
                    }
                    if (ImGui::BeginPopup("##drill_popup")) {
                        if (c < n_brk) {
                            char lbl[256];
                            std::snprintf(lbl, sizeof(lbl), "Drill into: %s = %s",
                                          cur_headers[c].c_str(), cell ? cell : "");
                            if (ImGui::MenuItem(lbl)) {
                                drill_into(st, active, cur_headers[c],
                                           cell ? std::string(cell) : "",
                                           input_headers_active);
                                ImGui::CloseCurrentPopup();
                            }
                            ImGui::Separator();
                        }
                        if (ImGui::MenuItem("Inspect row...")) {
                            U.inspect_row  = r;
                            U.inspect_open = true;
                            ImGui::CloseCurrentPopup();
                        }
                        ImGui::EndPopup();
                    }
                    ImGui::PopID();
                }
            }
            ImGui::EndTable();
        }
        ImGui::PopStyleColor(3); // HeaderHovered/HeaderActive/Header (issue 0081-O.7)
        }
        stage_n_table_end:;

        // Row inspector modal (fase 10). Activado via right-click "Inspect row..."
        // sobre celdas del table del stage activo. `cur_cells` ya es row-major.
        draw_row_inspector_modal(st, active, cur_cells, cur_rows, cur_cols_n,
                                  cur_headers, cur_types, input_headers_active);

        // Render extras (stage>0 path). Solo cuando display != Table.
        if (st.display != ViewMode::Table && !st.extra_panels.empty() && cur_cols_n > 0) {
            StageOutput so_local;
            const StageOutput* so_ptr = nullptr;
            if (!outs.empty()) {
                so_ptr = &outs.back();
            } else {
                so_local.cols = cur_cols_n;
                so_local.rows = cur_rows;
                so_local.headers = cur_headers;
                so_local.types   = cur_types;
                so_local.cells.reserve((size_t)cur_rows * cur_cols_n);
                for (int i = 0; i < cur_rows * cur_cols_n; ++i)
                    so_local.cells.push_back(cur_cells[i]);
                so_ptr = &so_local;
            }
            int close_idx = -1;
            const std::vector<ColumnSpec>* ep_specs2 =
                main_t.column_specs.empty() ? nullptr : &main_t.column_specs;
            for (int i = 0; i < (int)st.extra_panels.size(); ++i) {
                if (draw_extra_panel(st, st.extra_panels[i], i, *so_ptr, ep_specs2)) close_idx = i;
            }
            if (close_idx >= 0) st.extra_panels.erase(st.extra_panels.begin() + close_idx);
        }
    }

    // ---------- Modales (comunes a ambos paths) ----------
    if (U.cf_open) ImGui::OpenPopup("Custom column");
    if (ImGui::BeginPopupModal("Custom column", &U.cf_open,
                                ImGuiWindowFlags_AlwaysAutoResize))
    {
        ImGui::Text("Nombre:");
        char name_buf[128] = {0};
        std::snprintf(name_buf, sizeof(name_buf), "%s", U.cf_name.c_str());
        ImGui::SetNextItemWidth(520);
        if (ImGui::InputText("##cfname", name_buf, sizeof(name_buf))) U.cf_name = name_buf;

        ImGui::Spacing();
        ImGui::Text("Formula (Lua). Acceso celdas via row.<col_name> o row[idx].");
        ImGui::TextDisabled("Ejemplo: return row.size_kb * 1024");

        static char formula_buf[4096] = {0};
        if (U.cf_force_cursor || std::strcmp(formula_buf, U.cf_formula.c_str()) != 0) {
            std::snprintf(formula_buf, sizeof(formula_buf), "%s", U.cf_formula.c_str());
        }
        ImGuiInputTextFlags flags =
            ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways;
        if (ImGui::InputTextMultiline("##cfformula", formula_buf, sizeof(formula_buf),
                                       ImVec2(520, 200), flags, autocomplete_cb, &U)) {
            U.cf_formula = formula_buf;
        }
        if (U.cf_ac_open) {
            ImVec2 box_min = ImGui::GetItemRectMin();
            ImVec2 box_max = ImGui::GetItemRectMax();
            ImGui::SetNextWindowPos(ImVec2(box_min.x + 20, box_max.y + 4));
            ImGui::SetNextWindowSize(ImVec2(280, 0));
            ImGuiWindowFlags wf =
                ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
                ImGuiWindowFlags_NoMove     | ImGuiWindowFlags_NoSavedSettings |
                ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_AlwaysAutoResize;
            if (ImGui::Begin("##colpicker", nullptr, wf)) {
                ImGui::TextDisabled("Pick column:");
                ImGui::Separator();
                auto ci_contains = [](const std::string& hay, const std::string& nd) {
                    if (nd.empty()) return true;
                    std::string a = hay, b = nd;
                    for (char& c : a) if (c >= 'A' && c <= 'Z') c += 32;
                    for (char& c : b) if (c >= 'A' && c <= 'Z') c += 32;
                    return a.find(b) != std::string::npos;
                };
                int shown = 0;
                for (int c = 0; c < eff_cols && shown < 12; ++c) {
                    std::string nm = eff_headers[c];
                    if (!ci_contains(nm, U.cf_ac_filter)) continue;
                    char lbl[200];
                    std::snprintf(lbl, sizeof(lbl), "%s %s",
                                  column_type_icon(eff_types[c]), nm.c_str());
                    if (ImGui::Selectable(lbl)) {
                        int new_cursor = 0;
                        std::string updated = insert_column_ref(
                            U.cf_formula, U.cf_ac_start, U.cf_ac_cursor, nm, new_cursor);
                        U.cf_formula      = updated;
                        U.cf_target_cursor= new_cursor;
                        U.cf_force_cursor = true;
                        U.cf_ac_open      = false;
                    }
                    ++shown;
                }
                if (shown == 0) ImGui::TextDisabled("(sin matches)");
            }
            ImGui::End();
        }

        if (!U.cf_error.empty()) {
            ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(230, 100, 100, 255));
            ImGui::TextWrapped("Error: %s", U.cf_error.c_str());
            ImGui::PopStyleColor();
        }

        if (ImGui::Button("Compile & save")) {
            std::string err;
            int lid = lua_engine::compile(lua_engine::get(), U.cf_formula, &err);
            if (lid < 0) {
                U.cf_error = err;
            } else {
                // Build sample context segun cf_target_stage.
                //   target == 0: usa orig cells + stage 0 derived.
                //   target >  0: recomputa chain hasta el target (excluyendo
                //                derived del target) y sample sobre ese output.
                int ts = U.cf_target_stage;
                if (ts < 0 || ts >= (int)st.stages.size()) ts = 0;
                int sample = 0;
                std::vector<std::string> samples_str;

                if (ts == 0) {
                    sample = std::min(64, row_count);
                    for (int r = 0; r < sample; ++r) {
                        lua_engine::RowCtx ctx;
                        ctx.cells               = cells;
                        ctx.orig_cols           = orig_cols;
                        ctx.row                 = r;
                        ctx.header_names        = &hn_storage;
                        ctx.name_to_col         = &name_to_col;
                        ctx.types_orig          = eff_types.data();
                        ctx.n_types_orig        = orig_cols;
                        ctx.derived             = &stage0.derived;
                        ctx.derived_name_to_idx = &derived_n2i;
                        std::string e;
                        samples_str.emplace_back(
                            lua_engine::eval(lua_engine::get(), lid, ctx, &e));
                    }
                } else {
                    // Recompute chain hasta stage ts output (sin aplicar derived
                    // del propio ts).
                    State st_sample = st;
                    // Limpia derived del target stage para que el sample no
                    // se referencie a si mismo.
                    if (ts < (int)st_sample.stages.size())
                        st_sample.stages[ts].derived.clear();
                    // Reusa la logica de materializacion: simple recompute manual.
                    // Aplica stage 0 (orig + derived) materializado.
                    State stmp = st;
                    Stage& s0 = stmp.stages[0];
                    for (auto& f : s0.filters) {
                        if (f.col >= 0 && f.col < eff_cols) f.col = src_for_eff[f.col];
                    }
                    s0.sorts.clear();
                    auto v0 = compute_visible_rows(cells, row_count, orig_cols, stmp);

                    std::vector<std::string> mb;
                    std::vector<const char*> mc;
                    mb.reserve((size_t)v0.size() * eff_cols);
                    mc.reserve((size_t)v0.size() * eff_cols);
                    for (int r : v0) {
                        for (int c = 0; c < eff_cols; ++c) {
                            if (c < orig_cols) {
                                const char* p = cells[r * orig_cols + c];
                                mb.emplace_back(p ? p : "");
                            } else {
                                const DerivedColumn& d = stage0.derived[c - orig_cols];
                                if (!d.formula.empty() && d.lua_id >= 0) {
                                    lua_engine::RowCtx ctx;
                                    ctx.cells = cells; ctx.orig_cols = orig_cols;
                                    ctx.row = r;
                                    ctx.header_names = &hn_storage;
                                    ctx.name_to_col  = &name_to_col;
                                    ctx.types_orig = eff_types.data();
                                    ctx.n_types_orig = orig_cols;
                                    ctx.derived = &stage0.derived;
                                    ctx.derived_name_to_idx = &derived_n2i;
                                    std::string e;
                                    mb.emplace_back(lua_engine::eval(lua_engine::get(), d.lua_id, ctx, &e));
                                } else if (d.source_col >= 0) {
                                    const char* p = cells[r * orig_cols + d.source_col];
                                    mb.emplace_back(p ? p : "");
                                } else mb.emplace_back("");
                            }
                        }
                    }
                    for (auto& s : mb) mc.push_back(s.c_str());

                    std::vector<std::string> ch(eff_cols);
                    std::vector<ColumnType>  ct(eff_cols);
                    for (int c = 0; c < eff_cols; ++c) { ch[c] = eff_headers[c]; ct[c] = eff_types[c]; }

                    const char* const* cc = mc.data();
                    int cr = (int)v0.size();
                    int cn = eff_cols;
                    std::vector<StageOutput> tmps;
                    for (int si = 1; si <= ts; ++si) {
                        Stage stage_sn = st.stages[si];
                        // En el target stage NO apliques sus propias derived.
                        if (si == ts) stage_sn.derived.clear();
                        tmps.push_back(compute_stage(cc, cr, cn, ch, ct, stage_sn));
                        const StageOutput& l = tmps.back();
                        cc = l.cells.data(); cr = l.rows; cn = l.cols;
                        ch = l.headers; ct = l.types;
                    }
                    // Build name_to_col map for the target stage output.
                    std::vector<std::string> hn_t = ch;
                    std::unordered_map<std::string, int> n2c_t;
                    for (size_t i = 0; i < hn_t.size(); ++i) n2c_t[hn_t[i]] = (int)i;
                    sample = std::min(64, cr);
                    for (int r = 0; r < sample; ++r) {
                        lua_engine::RowCtx ctx;
                        ctx.cells        = cc;
                        ctx.orig_cols    = cn;
                        ctx.row          = r;
                        ctx.header_names = &hn_t;
                        ctx.name_to_col  = &n2c_t;
                        ctx.types_orig   = ct.data();
                        ctx.n_types_orig = cn;
                        std::string e;
                        samples_str.emplace_back(
                            lua_engine::eval(lua_engine::get(), lid, ctx, &e));
                    }
                }

                std::vector<const char*> samples_ptr;
                samples_ptr.reserve(samples_str.size());
                for (auto& s : samples_str) samples_ptr.push_back(s.c_str());
                ColumnType auto_t = auto_detect_type(samples_ptr.data(),
                                                     (int)samples_ptr.size(), 1, 0);

                // Save to target stage.
                if (ts < 0 || ts >= (int)st.stages.size()) ts = 0;
                auto& target_derived = st.stages[ts].derived;
                if (U.cf_editing && U.cf_edit_idx >= 0 &&
                    U.cf_edit_idx < (int)target_derived.size())
                {
                    auto& d = target_derived[U.cf_edit_idx];
                    if (d.lua_id >= 0) lua_engine::release(lua_engine::get(), d.lua_id);
                    d.formula = U.cf_formula;
                    d.name    = U.cf_name.empty() ? "custom" : U.cf_name;
                    d.type    = auto_t;
                    d.lua_id  = lid;
                    d.compile_error.clear();
                } else {
                    DerivedColumn d;
                    d.source_col = -1;
                    d.type       = auto_t;
                    d.name       = U.cf_name.empty() ? "custom" : U.cf_name;
                    d.formula    = U.cf_formula;
                    d.lua_id     = lid;
                    target_derived.push_back(d);
                }
                U.cf_open  = false;
                U.cf_error.clear();
            }
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) {
            U.cf_open = false;
            U.cf_error.clear();
        }
        ImGui::EndPopup();
    }

    if (U.tql_show_open) ImGui::OpenPopup("Show TQL");
    if (ImGui::BeginPopupModal("Show TQL", &U.tql_show_open,
                                ImGuiWindowFlags_AlwaysAutoResize))
    {
        ImGui::Text("TQL serializado del estado actual (read-only):");
        ImGui::InputTextMultiline("##tqlshow", U.tql_show_text.data(),
                                   U.tql_show_text.size() + 1,
                                   ImVec2(560, 280),
                                   ImGuiInputTextFlags_ReadOnly);
        if (ImGui::Button("Copy to clipboard")) {
            ImGui::SetClipboardText(U.tql_show_text.c_str());
        }
        ImGui::SameLine();
        if (ImGui::Button("Close")) U.tql_show_open = false;
        ImGui::EndPopup();
    }

    if (U.tql_apply_open) ImGui::OpenPopup("Apply TQL");
    if (ImGui::BeginPopupModal("Apply TQL", &U.tql_apply_open,
                                ImGuiWindowFlags_AlwaysAutoResize))
    {
        ImGui::Text("Pega un chunk TQL (Lua). Ver docs/TQL.md para sintaxis.");
        static char tql_buf[8192] = {0};
        if (std::strcmp(tql_buf, U.tql_apply_text.c_str()) != 0) {
            std::snprintf(tql_buf, sizeof(tql_buf), "%s", U.tql_apply_text.c_str());
        }
        if (ImGui::InputTextMultiline("##tqlapply", tql_buf, sizeof(tql_buf),
                                       ImVec2(560, 280))) {
            U.tql_apply_text = tql_buf;
        }
        if (!U.tql_apply_error.empty()) {
            ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(230, 100, 100, 255));
            ImGui::TextWrapped("Error: %s", U.tql_apply_error.c_str());
            ImGui::PopStyleColor();
        }
        if (ImGui::Button("Apply")) {
            std::vector<std::string> orig_headers(orig_cols);
            std::vector<ColumnType>  orig_types(orig_cols);
            for (int c = 0; c < orig_cols; ++c) {
                orig_headers[c] = headers[c];
                orig_types[c]   = eff_types[c];
            }
            std::string err;
            bool ok = tql::apply(U.tql_apply_text, st, orig_headers, orig_types,
                                  cells, row_count, orig_cols, &err);
            if (ok) {
                U.tql_apply_open  = false;
                U.tql_apply_error.clear();
            } else {
                U.tql_apply_error = err;
            }
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) {
            U.tql_apply_open = false;
            U.tql_apply_error.clear();
        }
        ImGui::EndPopup();
    }

    // Ask AI modal (fase 11 — issue 0080).
    if (U.ask_open) ImGui::OpenPopup("Ask AI");
    ImGui::SetNextWindowSize(ImVec2(820, 560), ImGuiCond_Appearing);
    if (ImGui::BeginPopupModal("Ask AI", &U.ask_open,
                                ImGuiWindowFlags_NoSavedSettings)) {
        ImGui::TextDisabled("Ask en lenguaje natural. Default TQL. SQL solo si DuckDB linkado.");
        const char* modes[] = {"TQL", "SQL (DuckDB)"};
#ifndef FN_TQL_DUCKDB
        // SQL mode disabled visually pero el toggle existe (informativo)
        if (U.ask_mode == 1) U.ask_mode = 0;
#endif
        ImGui::Combo("Output##askmode", &U.ask_mode, modes, IM_ARRAYSIZE(modes));
#ifndef FN_TQL_DUCKDB
        if (U.ask_mode == 1) {
            ImGui::TextColored(ImVec4(1, 0.5f, 0.3f, 1),
                "SQL mode requires FN_TQL_DUCKDB=1 build flag.");
        }
#endif
        ImGui::InputTextMultiline("##ask_q", U.ask_question, sizeof(U.ask_question),
                                   ImVec2(-1, 80));
        ImGui::BeginDisabled(U.ask_busy);
        if (ImGui::Button("Send")) {
            U.ask_busy = true;
            U.ask_status = "Sending...";
            U.ask_error.clear();
            U.ask_response_code.clear();
            U.ask_response_raw.clear();

            // Build AskInput desde el state actual.
            llm_anthropic::AskInput in;
            in.question = U.ask_question;
            in.tql_current = U.ask_current_tql;
            in.col_names = U.active_headers;
            in.col_types = U.active_types;
            in.mode = (U.ask_mode == 1)
                ? llm_anthropic::OutputMode::SQL
                : llm_anthropic::OutputMode::TQL;

            // Llamada blocking (UI freeze breve durante red).
            auto r = llm_anthropic::ask(in);
            U.ask_busy = false;
            if (!r.error.empty()) {
                U.ask_error = r.error;
                U.ask_status = "Error";
            } else {
                U.ask_response_raw = r.raw;
                U.ask_response_code = r.code;
                U.ask_status = "Got response.";
                // Llenar edit buffer
                std::snprintf(U.ask_edit_buf, sizeof(U.ask_edit_buf),
                              "%s", r.code.c_str());
            }
        }
        ImGui::EndDisabled();
        ImGui::SameLine();
        if (!U.ask_status.empty()) {
            ImGui::TextDisabled("%s", U.ask_status.c_str());
        }
        if (!U.ask_error.empty()) {
            ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", U.ask_error.c_str());
        }
        ImGui::Separator();
        ImGui::Columns(2, "ask_cols", true);
        ImGui::TextUnformatted("Current");
        ImGui::InputTextMultiline("##ask_cur",
            const_cast<char*>(U.ask_current_tql.c_str()),
            U.ask_current_tql.size() + 1,
            ImVec2(-1, 240),
            ImGuiInputTextFlags_ReadOnly);
        ImGui::NextColumn();
        ImGui::TextUnformatted("Proposed (editable before apply)");
        ImGui::InputTextMultiline("##ask_new", U.ask_edit_buf, sizeof(U.ask_edit_buf),
                                   ImVec2(-1, 240));
        ImGui::Columns(1);

        bool can_apply = !U.ask_busy && U.ask_edit_buf[0] != '\0';
        ImGui::BeginDisabled(!can_apply);
        if (ImGui::Button("Apply")) {
            std::string err;
            if (U.ask_mode == 0) {
                // TQL apply
                bool ok = tql::apply(U.ask_edit_buf, st,
                                      U.active_headers,
                                      U.active_types,
                                      nullptr, 0,
                                      (int)U.active_headers.size(),
                                      &err);
                if (ok) {
                    U.ask_status = "Applied OK.";
                    U.ask_open = false;
                } else {
                    U.ask_error = "tql::apply error: " + err;
                    U.ask_status = "Apply failed.";
                }
            } else {
#ifdef FN_TQL_DUCKDB
                // SQL apply: ejecutar via tql_duckdb sobre TableInputs activas.
                // Para tablas en memoria construimos un TableInput basico desde
                // active_headers/types. v1 no recupera cells originales aqui;
                // reportamos solo error si fallo. Caller real deberia pasar
                // tables() del render scope. Sin esto, marcamos status info.
                U.ask_status = "SQL execute disponible (FN_TQL_DUCKDB ON). "
                               "Integracion full pendiente: usar tql_duckdb::execute desde caller.";
#else
                U.ask_status = "SQL execute requires FN_TQL_DUCKDB build flag.";
#endif
            }
        }
        ImGui::EndDisabled();
        ImGui::SameLine();
        if (ImGui::Button("Reject")) {
            U.ask_response_code.clear();
            U.ask_edit_buf[0] = '\0';
        }
        ImGui::SameLine();
        if (ImGui::Button("Close")) {
            U.ask_open = false;
        }
        ImGui::EndPopup();
    }

    if (U.open_cell_popup) { ImGui::OpenPopup("##cell_op"); U.open_cell_popup = false; }
    if (ImGui::BeginPopup("##cell_op")) {
        ColumnType t = (U.pending_col >= 0 && U.pending_col < eff_cols)
                       ? eff_types[U.pending_col] : ColumnType::String;
        const char* hdr = (U.pending_col >= 0 && U.pending_col < eff_cols)
                          ? eff_headers[U.pending_col] : "?";
        ImGui::TextDisabled("%s %s ?? \"%s\"",
                            column_type_icon(t), hdr, U.pending_value.c_str());
        ImGui::Separator();
        auto ops = ops_for_type(t);
        for (Op o : ops) {
            if (ImGui::MenuItem(op_label(o))) {
                st.stages[0].filters.push_back({U.pending_col, o, U.pending_value});
                ImGui::CloseCurrentPopup();
            }
        }
        ImGui::EndPopup();
    }
}

} // namespace data_table
