// data_table_chips — barra de chips superior de la tabla TQL.
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
//
// Contiene: draw_joins_chips, draw_filter_chips, draw_breakout_chips,
//           draw_aggregation_chips, draw_sort_chips, apply_header_sort_click,
//           draw_edit_*_popup, draw_add_*_popup, draw_header_menu, draw_tql_bar.

#include "viz/data_table_chips.h"
#include "data_table/data_table_internal.h"   // UiState, ui(), helpers inline

#include "core/tql_emit.h"
#include "core/tql_apply.h"
#include "core/tql_helpers.h"                  // parse_breakout_granularity, date_granularity_token
#include "viz/data_table_color_rules.h"         // draw_color_rule_menu
#include "imgui.h"
#include "app_base.h"                           // fn::local_path

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <fstream>
#include <string>
#include <vector>

namespace data_table {

// ---------------------------------------------------------------------------
// Static helpers (solo usados en este TU)
// ---------------------------------------------------------------------------

// column_type_icon: returns a Tabler icon UTF-8 sequence for each ColumnType.
// (duplicado de data_table.cpp — lo dejamos static aqui para no depender del
// entrypoint; si se necesita compartir mover a data_table_internal.h inline.)
static const char* column_type_icon_chips(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 "?";
}

// agg_fn_label_chips: nombre del fn de agregacion.
static const char* agg_fn_label_chips(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 "?";
}

// date helpers (solo para draw_add_breakout_popup)
namespace {

static bool parse_ymd_chips(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_chips(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 DateGranularity auto_date_gran_chips(const std::string& min_ymd,
                                             const std::string& max_ymd) {
    int y1,m1,d1, y2,m2,d2;
    if (!parse_ymd_chips(min_ymd, y1,m1,d1)) return DateGranularity::Day;
    if (!parse_ymd_chips(max_ymd, y2,m2,d2)) return DateGranularity::Day;
    long span = ymd_to_days_chips(y2,m2,d2) - ymd_to_days_chips(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 void column_min_max_chips(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; }
    }
}

} // anon

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

// ---------------------------------------------------------------------------
// draw_typed_ops
// ---------------------------------------------------------------------------
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;
}

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

// ---------------------------------------------------------------------------
// apply_header_sort_click
// ---------------------------------------------------------------------------
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);
    }
}

// ---------------------------------------------------------------------------
// draw_sort_chips
// ---------------------------------------------------------------------------
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);

        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_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);
    }
}

// ---------------------------------------------------------------------------
// draw_add_sort_popup
// ---------------------------------------------------------------------------
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_chips(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();
}

// ---------------------------------------------------------------------------
// draw_edit_sort_popup
// ---------------------------------------------------------------------------
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();
}

// ---------------------------------------------------------------------------
// draw_filter_chips
// ---------------------------------------------------------------------------
void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols,
                       const std::vector<ColumnType>& eff_types) {
    auto& U = ui();
    // Preset filter helpers needed locally
    auto today_iso_chips = []() -> std::string {
        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;
    };

    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 menu
    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;
        }
        // build_preset_filters lambda
        auto build_pf = [&](FilterPreset preset, int col) -> std::vector<Filter> {
            std::vector<Filter> out;
            std::string today = today_iso_chips();
            auto last_n = [&](int n) {
                int y, m, d;
                if (!parse_ymd_chips(today, y, m, d)) return;
                long days = ymd_to_days_chips(y, m, d) - n;
                // reverse
                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 doy2 = doe - (365*yoe + yoe/4 - yoe/100);
                unsigned mp  = (5*doy2 + 2)/153;
                unsigned day = doy2 - (153*mp + 2)/5 + 1;
                unsigned mon = mp < 10 ? mp + 3 : mp - 9;
                if (mon <= 2) yr += 1;
                char buf[16];
                std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", yr, (int)mon, (int)day);
                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;
        };
        auto apply_preset = [&](FilterPreset p, int col) {
            auto fs = build_pf(p, col);
            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);
        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();
}

// ---------------------------------------------------------------------------
// draw_add_filter_popup
// ---------------------------------------------------------------------------
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_chips(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_chips(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();
}

// ---------------------------------------------------------------------------
// draw_edit_filter_popup
// ---------------------------------------------------------------------------
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();
}

// ---------------------------------------------------------------------------
// draw_breakout_chips
// ---------------------------------------------------------------------------
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);

        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; }

        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_chips(col_name, o);
                    }
                }
                ImGui::EndCombo();
            }
        }
        ImGui::SameLine();
        ++i;
    }
    ImGui::NewLine();
}

// ---------------------------------------------------------------------------
// draw_add_breakout_popup
// ---------------------------------------------------------------------------
void draw_add_breakout_popup(Stage& stg, const char* const* in_headers, int in_cols,
                              const std::vector<ColumnType>& in_types,
                              const char* const* cur_cells, int cur_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_chips(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];
        if (c >= 0 && c < (int)in_types.size() && in_types[c] == ColumnType::Date) {
            std::string lo, hi;
            column_min_max_chips(cur_cells, cur_rows, in_cols, c, lo, hi);
            DateGranularity g = auto_date_gran_chips(lo, hi);
            stg.breakouts.emplace_back(compose_breakout_chips(col, g));
        } else {
            stg.breakouts.emplace_back(col);
        }
        ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();
}

// ---------------------------------------------------------------------------
// draw_edit_breakout_popup
// ---------------------------------------------------------------------------
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();
}

// ---------------------------------------------------------------------------
// draw_aggregation_chips
// ---------------------------------------------------------------------------
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_chips(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();
}

// ---------------------------------------------------------------------------
// draw_add_aggregation_popup
// ---------------------------------------------------------------------------
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_chips(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_chips(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_chips(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();
}

// ---------------------------------------------------------------------------
// draw_edit_agg_popup
// ---------------------------------------------------------------------------
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_chips((AggFn)U.edit_agg_fn))) {
            for (auto f : all_fns) {
                bool sel = ((int)f == U.edit_agg_fn);
                if (ImGui::Selectable(agg_fn_label_chips(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();
}

// ---------------------------------------------------------------------------
// draw_joins_chips
// Firma del header: (State&, const std::vector<TableInput>&, const char* const*, int, const std::vector<ColumnType>&)
// ---------------------------------------------------------------------------
void draw_joins_chips(State& st, const std::vector<TableInput>& joinables,
                      const char* const* eff_headers, int eff_cols,
                      const std::vector<ColumnType>& /*eff_types*/) {
    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-join. State variables are static (one join popup at a time).
    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");
        if (pick_source_idx >= (int)joinables.size()) pick_source_idx = 0;
        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 eff_headers)
        ImGui::SetNextItemWidth(180);
        const char* lcur = (pick_left_col >= 0 && pick_left_col < eff_cols)
                            ? eff_headers[pick_left_col] : "?";
        if (ImGui::BeginCombo("left col", lcur)) {
            for (int k = 0; k < eff_cols; ++k) {
                bool sel = (k == pick_left_col);
                if (ImGui::Selectable(eff_headers[k], 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") && pick_left_col < eff_cols &&
            pick_right_col < (int)src.headers.size()) {
            Join jn;
            jn.alias    = pick_alias;
            jn.source   = src.name;
            jn.on.push_back({std::string(eff_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();
}

// ---------------------------------------------------------------------------
// draw_header_menu
// ---------------------------------------------------------------------------
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();
    }

    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_chips(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")) {
        draw_color_rule_menu(st, col, t, U.color_rules);
        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_chips(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();
    }
}

// ---------------------------------------------------------------------------
// draw_tql_bar
// Extrae el bloque TQL de render() (Show/Apply/Save/Load + modales).
// En el entrypoint data_table.cpp, reemplazar el bloque inline por llamadas
// a draw_tql_bar(U.tql_bar, st, orig_headers, orig_types, cells, row_count, orig_cols).
// ---------------------------------------------------------------------------
void draw_tql_bar(TqlBarState& tql_bar,
                  State& st,
                  const std::vector<std::string>& orig_headers,
                  const std::vector<ColumnType>& orig_types,
                  const char* const* cells,
                  int row_count, int orig_cols) {
    if (ImGui::SmallButton("Show TQL")) {
        tql_bar.show_text = tql::emit(st, orig_headers, orig_types);
        tql_bar.show_open = true;
    }
    ImGui::SameLine();
    if (ImGui::SmallButton("Apply TQL")) {
        tql_bar.apply_open = true;
        tql_bar.apply_error.clear();
    }
    ImGui::SameLine();
    ImGui::SetNextItemWidth(160);
    ImGui::InputText("##tql_file", tql_bar.file_path, sizeof(tql_bar.file_path));
    ImGui::SameLine();
    if (ImGui::SmallButton("Save .tql")) {
        std::string text = tql::emit(st, orig_headers, orig_types);
        const char* path = fn::local_path(tql_bar.file_path);
        std::ofstream f(path);
        if (f) { f << text; tql_bar.io_status = std::string("saved: ") + path; }
        else   { tql_bar.io_status = std::string("save FAILED: ") + path; }
    }
    ImGui::SameLine();
    if (ImGui::SmallButton("Load .tql")) {
        const char* path = fn::local_path(tql_bar.file_path);
        std::ifstream f(path);
        if (!f) { tql_bar.io_status = std::string("load FAILED: ") + path; }
        else {
            std::string text((std::istreambuf_iterator<char>(f)),
                              std::istreambuf_iterator<char>());
            std::string err;
            bool ok = tql::apply(text, st, orig_headers, orig_types,
                                  cells, row_count, orig_cols, &err);
            if (ok) tql_bar.io_status = std::string("loaded: ") + path +
                                         (err.empty() ? "" : " (warn: " + err + ")");
            else    tql_bar.io_status = std::string("load parse error: ") + err;
        }
    }
    if (!tql_bar.io_status.empty()) {
        ImGui::SameLine();
        ImGui::TextDisabled("%s", tql_bar.io_status.c_str());
    }

    // Modal "Show TQL"
    if (tql_bar.show_open) {
        ImGui::OpenPopup("##tql_show_modal");
        tql_bar.show_open = false;
    }
    ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_Appearing);
    if (ImGui::BeginPopupModal("##tql_show_modal", nullptr,
                                ImGuiWindowFlags_NoSavedSettings)) {
        ImGui::TextUnformatted("TQL (read-only):");
        ImGui::SameLine();
        if (ImGui::SmallButton("Copy")) {
            ImGui::SetClipboardText(tql_bar.show_text.c_str());
        }
        ImGui::InputTextMultiline("##tql_show_text",
                                   const_cast<char*>(tql_bar.show_text.c_str()),
                                   tql_bar.show_text.size() + 1,
                                   ImVec2(-1, -40),
                                   ImGuiInputTextFlags_ReadOnly);
        if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
        ImGui::EndPopup();
    }

    // Modal "Apply TQL"
    if (tql_bar.apply_open) {
        ImGui::OpenPopup("##tql_apply_modal");
        tql_bar.apply_open = false;
        // Pre-fill apply_text with current TQL
        tql_bar.apply_text = tql::emit(st, orig_headers, orig_types);
    }
    ImGui::SetNextWindowSize(ImVec2(600, 420), ImGuiCond_Appearing);
    if (ImGui::BeginPopupModal("##tql_apply_modal", nullptr,
                                ImGuiWindowFlags_NoSavedSettings)) {
        ImGui::TextUnformatted("Paste / edit TQL and click Apply:");
        tql_bar.apply_text.resize(4096, '\0');
        ImGui::InputTextMultiline("##tql_apply_text",
                                   tql_bar.apply_text.data(),
                                   tql_bar.apply_text.size(),
                                   ImVec2(-1, -80),
                                   ImGuiInputTextFlags_None);
        if (!tql_bar.apply_error.empty()) {
            ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 100, 100, 255));
            ImGui::TextWrapped("Error: %s", tql_bar.apply_error.c_str());
            ImGui::PopStyleColor();
        }
        if (ImGui::Button("Apply")) {
            std::string err;
            bool ok = tql::apply(std::string(tql_bar.apply_text.c_str()),
                                  st, orig_headers, orig_types,
                                  cells, row_count, orig_cols, &err);
            if (ok) {
                tql_bar.apply_error.clear();
                ImGui::CloseCurrentPopup();
            } else {
                tql_bar.apply_error = err;
            }
        }
        ImGui::SameLine();
        if (ImGui::Button("Cancel")) {
            tql_bar.apply_error.clear();
            ImGui::CloseCurrentPopup();
        }
        ImGui::EndPopup();
    }
}

} // namespace data_table
