#include "viz/viz_render.h"
#include "core/auto_detect_type.h"   // parse_number
#include "implot.h"

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>

namespace viz {

using data_table::StageOutput;
using data_table::ColumnType;
using data_table::ViewMode;
using data_table::ViewConfig;
using data_table::parse_number;

// Hit-test helpers (pure math; extracted from data_table_logic).
namespace {

// Returns index of nearest point in xs[]/ys[] to (tx,ty). -1 if n==0.
static int nearest_index_2d(double tx, double ty,
                             const double* xs, const double* ys, int n) {
    int best = -1;
    double best_d = 1e300;
    for (int i = 0; i < n; ++i) {
        double dx = xs[i] - tx, dy = ys[i] - ty;
        double d = dx*dx + dy*dy;
        if (d < best_d) { best_d = d; best = i; }
    }
    return best;
}

// Returns angle in [0, 360) CCW from top (12 o'clock) for point (mx,my)
// relative to center (cx,cy).
static double pie_angle(double cx, double cy, double mx, double my) {
    constexpr double kPI = 3.14159265358979323846;
    double dx = mx - cx, dy = my - cy;
    double angle_deg = std::atan2(dy, dx) * 180.0 / kPI;
    double offset = angle_deg - 90.0;
    while (offset < 0.0)    offset += 360.0;
    while (offset >= 360.0) offset -= 360.0;
    return offset;
}

// Returns slice index for angle (deg, [0,360)) in a pie of n slices with
// given `values`. Returns n-1 for last slice. -1 if n==0.
static int pie_slice_at_angle(double angle, const double* values, int n) {
    if (n <= 0) return -1;
    double total = 0.0;
    for (int i = 0; i < n; ++i) total += values[i];
    if (total == 0.0) return 0;
    double acc = 0.0;
    for (int i = 0; i < n; ++i) {
        double sweep = (values[i] / total) * 360.0;
        if (angle >= acc && angle < acc + sweep) return i;
        acc += sweep;
    }
    return n - 1;
}

// Maps plot coordinates (px,py) in a heatmap (rows x cols, y=0..rows x=0..cols)
// to (row_out, col_out). Sets -1 if out of range.
static void heatmap_cell_at(double px, double py, int rows, int cols,
                             int& row_out, int& col_out) {
    col_out = (int)std::floor(px);
    row_out = (int)std::floor(py);
    if (row_out < 0 || row_out >= rows || col_out < 0 || col_out >= cols) {
        row_out = col_out = -1;
    }
}

} // anon (hit-test helpers)

static int find_header(const StageOutput& out, const std::string& name) {
    if (name.empty()) return -1;
    for (size_t c = 0; c < out.headers.size(); ++c)
        if (out.headers[c] == name) return (int)c;
    return -1;
}

static int resolve_x(const StageOutput& out, const ViewConfig& cfg, int fallback) {
    int c = find_header(out, cfg.x_col);
    return (c >= 0) ? c : fallback;
}
static int resolve_cat(const StageOutput& out, const ViewConfig& cfg, int fallback) {
    int c = find_header(out, cfg.cat_col);
    return (c >= 0) ? c : fallback;
}
static int resolve_size(const StageOutput& out, const ViewConfig& cfg, int fallback) {
    int c = find_header(out, cfg.size_col);
    return (c >= 0) ? c : fallback;
}

int first_numeric_col(const StageOutput& out) {
    for (size_t c = 0; c < out.types.size(); ++c) {
        if (out.types[c] == ColumnType::Int || out.types[c] == ColumnType::Float) return (int)c;
    }
    return -1;
}

int first_category_col(const StageOutput& out) {
    for (size_t c = 0; c < out.types.size(); ++c) {
        ColumnType t = out.types[c];
        if (t == ColumnType::String || t == ColumnType::Date || t == ColumnType::Bool ||
            t == ColumnType::Json) return (int)c;
    }
    return -1;
}

std::vector<double> extract_numeric(const StageOutput& out, int col) {
    std::vector<double> v;
    if (col < 0 || col >= out.cols) return v;
    v.reserve(out.rows);
    for (int r = 0; r < out.rows; ++r) {
        const char* s = out.cells[(size_t)r * out.cols + col];
        double d = 0;
        if (s && *s && parse_number(s, d)) v.push_back(d);
        else                                v.push_back(std::nan(""));
    }
    return v;
}

std::vector<std::string> extract_category(const StageOutput& out, int col) {
    std::vector<std::string> v;
    if (col < 0 || col >= out.cols) return v;
    v.reserve(out.rows);
    for (int r = 0; r < out.rows; ++r) {
        const char* s = out.cells[(size_t)r * out.cols + col];
        v.emplace_back(s ? s : "");
    }
    return v;
}

namespace {

struct NumCol { int idx; std::string name; std::vector<double> vals; };

std::vector<NumCol> collect_numeric(const StageOutput& out, int max_n = 16) {
    std::vector<NumCol> r;
    for (size_t c = 0; c < out.types.size() && (int)r.size() < max_n; ++c) {
        if (out.types[c] == ColumnType::Int || out.types[c] == ColumnType::Float) {
            NumCol nc;
            nc.idx  = (int)c;
            nc.name = out.headers[c];
            nc.vals = extract_numeric(out, (int)c);
            r.push_back(std::move(nc));
        }
    }
    return r;
}

std::vector<NumCol> collect_numeric_filtered(const StageOutput& out,
                                              const ViewConfig& cfg,
                                              int max_n = 16) {
    if (cfg.y_cols.empty()) return collect_numeric(out, max_n);
    std::vector<NumCol> r;
    for (const auto& name : cfg.y_cols) {
        if ((int)r.size() >= max_n) break;
        int c = find_header(out, name);
        if (c < 0) continue;
        if (out.types[c] != ColumnType::Int && out.types[c] != ColumnType::Float) continue;
        NumCol nc;
        nc.idx  = c;
        nc.name = out.headers[c];
        nc.vals = extract_numeric(out, c);
        r.push_back(std::move(nc));
    }
    if (r.empty()) return collect_numeric(out, max_n);
    return r;
}

ImPlotSpec spec_with_color(unsigned int rgba_color) {
    if (rgba_color == 0) return ImPlotSpec();
    ImU32 c = (ImU32)rgba_color;
    return ImPlotSpec(ImPlotProp_LineColor, c, ImPlotProp_FillColor, c);
}

// Axis flags: locked = no pan/zoom; unlocked = 0 (sin AutoFit, para preservar
// pan/zoom del user). Re-fit explicito via SetNextAxesToFit cuando fit_request.
ImPlotAxisFlags axflag(const ViewConfig& cfg, ImPlotAxisFlags base = 0) {
    if (cfg.locked) return base | ImPlotAxisFlags_Lock;
    return base;
}

// Llamar antes de BeginPlot. Si cfg.fit_request -> fuerza re-fit y limpia el flag.
void maybe_fit(const ViewConfig& cfg) {
    if (cfg.fit_request) {
        ImPlot::SetNextAxesToFit();
        cfg.fit_request = false;
    }
}

void info_text(const char* msg) {
    ImVec2 avail = ImGui::GetContentRegionAvail();
    ImVec2 sz    = ImGui::CalcTextSize(msg);
    ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
                                ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f));
    ImGui::TextDisabled("%s", msg);
}

// Drop NaN and pair with optional labels.
std::vector<double> finite(const std::vector<double>& v) {
    std::vector<double> r; r.reserve(v.size());
    for (double d : v) if (!std::isnan(d)) r.push_back(d);
    return r;
}

bool render_bar_like(const StageOutput& out, ViewMode mode,
                     const ViewConfig& cfg, ImVec2 size,
                     int* clicked_row_out = nullptr) {
    int cat_col = resolve_cat(out, cfg, first_category_col(out));
    auto nums = collect_numeric_filtered(out, cfg, 8);
    if (cat_col < 0 || nums.empty()) {
        info_text("Need 1 category + 1+ numeric columns");
        return false;
    }
    auto cats = extract_category(out, cat_col);
    int n = (int)cats.size();
    if (n == 0) { info_text("Empty data"); return false; }

    // Ticks
    std::vector<double> ticks(n);
    std::vector<const char*> labels(n);
    for (int i = 0; i < n; ++i) { ticks[i] = i; labels[i] = cats[i].c_str(); }

    bool horiz = (mode == ViewMode::Bar);
    ImPlotFlags pflags = cfg.show_legend ? 0 : ImPlotFlags_NoLegend;
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##bar", size, pflags)) return false;

    ImPlotAxisFlags ax_cat = axflag(cfg);
    ImPlotAxisFlags ax_num = axflag(cfg);

    if (horiz) {
        ImPlot::SetupAxes(out.headers[nums[0].idx].c_str(), out.headers[cat_col].c_str(),
                          ax_num, ax_cat);
        ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), n, labels.data(), false);
    } else {
        ImPlot::SetupAxes(out.headers[cat_col].c_str(), out.headers[nums[0].idx].c_str(),
                          ax_cat, ax_num);
        ImPlot::SetupAxisTicks(ImAxis_X1, ticks.data(), n, labels.data(), false);
    }

    if (mode == ViewMode::StackedBar || mode == ViewMode::GroupedBar) {
        // Build flat matrix items x groups
        int items  = (int)nums.size();
        std::vector<double> mat((size_t)items * n, 0.0);
        std::vector<const char*> series_labels(items);
        for (int it = 0; it < items; ++it) {
            series_labels[it] = nums[it].name.c_str();
            for (int g = 0; g < n; ++g) {
                double d = nums[it].vals[g];
                mat[(size_t)it * n + g] = std::isnan(d) ? 0.0 : d;
            }
        }
        int flags = (mode == ViewMode::StackedBar) ? ImPlotBarGroupsFlags_Stacked : 0;
        if (horiz) flags |= ImPlotBarGroupsFlags_Horizontal;
        ImPlot::PlotBarGroups(series_labels.data(), mat.data(), items, n, 0.67, 0,
                              ImPlotSpec(ImPlotProp_Flags, flags));
    } else {
        // Single series (first numeric col).
        std::vector<double> ys(n);
        for (int i = 0; i < n; ++i) {
            double d = nums[0].vals[i];
            ys[i] = std::isnan(d) ? 0.0 : d;
        }
        ImPlotSpec spc = spec_with_color(cfg.primary_color);
        if (horiz) {
            if (cfg.primary_color != 0) {
                ImU32 col = (ImU32)cfg.primary_color;
                ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.67,
                                 ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal,
                                            ImPlotProp_FillColor, col,
                                            ImPlotProp_LineColor, col));
            } else {
                ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.67,
                                 ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal));
            }
        } else {
            ImPlot::PlotBars(nums[0].name.c_str(), ticks.data(), ys.data(), n, 0.67, spc);
        }
    }
    // Hit-test fase 10: idx = round(plot.{x|y}) en single-series mode.
    if (clicked_row_out &&
        mode != ViewMode::GroupedBar && mode != ViewMode::StackedBar &&
        ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
        ImPlotPoint p = ImPlot::GetPlotMousePos();
        double target = horiz ? p.y : p.x;
        int idx = (int)(target + 0.5);
        if (idx >= 0 && idx < n) *clicked_row_out = idx;
    }
    ImPlot::EndPlot();
    return true;
}

bool render_line_like(const StageOutput& out, ViewMode mode,
                      const ViewConfig& cfg, ImVec2 size) {
    auto nums = collect_numeric_filtered(out, cfg, 8);
    if (nums.empty()) { info_text("Need at least 1 numeric column"); return false; }

    ImPlotFlags pflags = cfg.show_legend ? 0 : ImPlotFlags_NoLegend;
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##line", size, pflags)) return false;
    ImPlot::SetupAxes(nullptr, nullptr, axflag(cfg), axflag(cfg));

    int n = nums.empty() ? 0 : (int)nums[0].vals.size();
    if (n == 0) { ImPlot::EndPlot(); return false; }

    // X column: cfg.x_col override; sino primer numeric si hay >=2; sino indices.
    int x_idx = -1;
    if (!cfg.x_col.empty()) {
        int xc = find_header(out, cfg.x_col);
        if (xc >= 0 && (out.types[xc] == ColumnType::Int || out.types[xc] == ColumnType::Float)) {
            x_idx = xc;
        }
    }
    std::vector<double> idx_xs;
    const double* xs = nullptr;
    int start_y = 0;
    std::vector<double> x_data_external;
    if (x_idx >= 0) {
        x_data_external = extract_numeric(out, x_idx);
        xs = x_data_external.data();
    } else if (nums.size() >= 2 && cfg.y_cols.empty()) {
        xs = nums[0].vals.data();
        start_y = 1;
    } else {
        idx_xs.resize(n);
        for (int i = 0; i < n; ++i) idx_xs[i] = i;
        xs = idx_xs.data();
    }

    bool only_one = (cfg.primary_color != 0) && (nums.size() - start_y == 1);
    for (size_t i = (size_t)start_y; i < nums.size(); ++i) {
        const auto& nc = nums[i];
        ImU32 col = only_one ? (ImU32)cfg.primary_color : 0;
        int marker = cfg.show_markers ? ImPlotMarker_Circle : ImPlotMarker_None;
        if (mode == ViewMode::Area) {
            if (col) {
                ImPlot::PlotShaded(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(), 0.0,
                                    ImPlotSpec(ImPlotProp_FillColor, col, ImPlotProp_LineColor, col));
            } else {
                ImPlot::PlotShaded(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(), 0.0);
            }
        } else if (mode == ViewMode::Stairs) {
            if (col) {
                ImPlot::PlotStairs(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
                                    ImPlotSpec(ImPlotProp_LineColor, col));
            } else {
                ImPlot::PlotStairs(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size());
            }
        } else {
            if (col) {
                ImPlot::PlotLine(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
                                  ImPlotSpec(ImPlotProp_Flags, ImPlotLineFlags_SkipNaN,
                                             ImPlotProp_LineColor, col,
                                             ImPlotProp_Marker, marker));
            } else {
                ImPlot::PlotLine(nc.name.c_str(), xs, nc.vals.data(), (int)nc.vals.size(),
                                  ImPlotSpec(ImPlotProp_Flags, ImPlotLineFlags_SkipNaN,
                                             ImPlotProp_Marker, marker));
            }
        }
    }
    ImPlot::EndPlot();
    return true;
}

bool render_scatter(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
                     int* clicked_row_out = nullptr) {
    // Soporte cfg.x_col + cfg.y_cols[0]
    int xc = find_header(out, cfg.x_col);
    int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
    std::vector<NumCol> nums;
    if (xc >= 0 && yc >= 0) {
        NumCol a{xc, out.headers[xc], extract_numeric(out, xc)};
        NumCol b{yc, out.headers[yc], extract_numeric(out, yc)};
        nums = {a, b};
    } else {
        nums = collect_numeric(out, 4);
    }
    if (nums.size() < 2) { info_text("Need 2 numeric columns"); return false; }
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##scatter", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
    ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str(),
                      axflag(cfg), axflag(cfg));
    if (cfg.primary_color) {
        ImU32 col = (ImU32)cfg.primary_color;
        ImPlot::PlotScatter("##s", nums[0].vals.data(), nums[1].vals.data(),
                             (int)nums[0].vals.size(),
                             ImPlotSpec(ImPlotProp_MarkerFillColor, col,
                                        ImPlotProp_MarkerLineColor, col));
    } else {
        ImPlot::PlotScatter("##s", nums[0].vals.data(), nums[1].vals.data(),
                             (int)nums[0].vals.size());
    }
    if (clicked_row_out &&
        ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
        ImPlotPoint p = ImPlot::GetPlotMousePos();
        int idx = nearest_index_2d(p.x, p.y,
                                    nums[0].vals.data(), nums[1].vals.data(),
                                    (int)nums[0].vals.size());
        if (idx >= 0) *clicked_row_out = idx;
    }
    ImPlot::EndPlot();
    return true;
}

bool render_bubble(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
                    int* clicked_row_out = nullptr) {
    int xc = find_header(out, cfg.x_col);
    int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
    int sc = resolve_size(out, cfg, -1);
    std::vector<NumCol> nums;
    if (xc >= 0 && yc >= 0 && sc >= 0) {
        nums = {
            {xc, out.headers[xc], extract_numeric(out, xc)},
            {yc, out.headers[yc], extract_numeric(out, yc)},
            {sc, out.headers[sc], extract_numeric(out, sc)},
        };
    } else {
        nums = collect_numeric(out, 4);
    }
    if (nums.size() < 3) { info_text("Need 3 numeric columns (x, y, size)"); return false; }
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##bubble", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
    ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str(),
                      axflag(cfg), axflag(cfg));
    ImPlot::PlotBubbles("##b", nums[0].vals.data(), nums[1].vals.data(),
                         nums[2].vals.data(), (int)nums[0].vals.size());
    if (clicked_row_out &&
        ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
        ImPlotPoint p = ImPlot::GetPlotMousePos();
        int idx = nearest_index_2d(p.x, p.y,
                                    nums[0].vals.data(), nums[1].vals.data(),
                                    (int)nums[0].vals.size());
        if (idx >= 0) *clicked_row_out = idx;
    }
    ImPlot::EndPlot();
    return true;
}

bool render_histogram(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
    auto nums = collect_numeric_filtered(out, cfg, 4);
    if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
    auto vals = finite(nums[0].vals);
    if (vals.empty()) { info_text("No finite values"); return false; }
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##hist", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
    ImPlot::SetupAxes(nums[0].name.c_str(), "count",
                      axflag(cfg), axflag(cfg));
    int bins = (cfg.hist_bins > 0) ? cfg.hist_bins : ImPlotBin_Sturges;
    if (cfg.primary_color) {
        ImU32 col = (ImU32)cfg.primary_color;
        ImPlot::PlotHistogram("##h", vals.data(), (int)vals.size(), bins, 1.0,
                               ImPlotRange(),
                               ImPlotSpec(ImPlotProp_FillColor, col,
                                          ImPlotProp_LineColor, col));
    } else {
        ImPlot::PlotHistogram("##h", vals.data(), (int)vals.size(), bins);
    }
    ImPlot::EndPlot();
    return true;
}

bool render_hist2d(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
    int xc = find_header(out, cfg.x_col);
    int yc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
    std::vector<NumCol> nums;
    if (xc >= 0 && yc >= 0) {
        nums = {
            {xc, out.headers[xc], extract_numeric(out, xc)},
            {yc, out.headers[yc], extract_numeric(out, yc)},
        };
    } else {
        nums = collect_numeric(out, 2);
    }
    if (nums.size() < 2) { info_text("Need 2 numeric columns"); return false; }
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##hist2d", size, cfg.show_legend ? 0 : ImPlotFlags_NoLegend)) return false;
    ImPlot::SetupAxes(nums[0].name.c_str(), nums[1].name.c_str());
    int bins = (cfg.hist_bins > 0) ? cfg.hist_bins : ImPlotBin_Sturges;
    ImPlot::PlotHistogram2D("##h2", nums[0].vals.data(), nums[1].vals.data(),
                             (int)nums[0].vals.size(), bins, bins);
    ImPlot::EndPlot();
    return true;
}

bool render_heatmap(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
                     int* clicked_row_out = nullptr) {
    auto nums = collect_numeric_filtered(out, cfg, 64);
    if (nums.empty()) { info_text("Need numeric columns"); return false; }
    int cols = (int)nums.size();
    int rows = (int)nums[0].vals.size();
    if (rows == 0) { info_text("No rows"); return false; }
    std::vector<double> mat((size_t)rows * cols, 0.0);
    double mn = +1e300, mx = -1e300;
    for (int c = 0; c < cols; ++c) {
        for (int r = 0; r < rows; ++r) {
            double d = nums[c].vals[r];
            if (std::isnan(d)) d = 0;
            mat[(size_t)r * cols + c] = d;
            if (d < mn) mn = d; if (d > mx) mx = d;
        }
    }
    if (mn == mx) { mx = mn + 1; }
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##heatmap", size, 0)) return false;
    ImPlot::PlotHeatmap("##hm", mat.data(), rows, cols, mn, mx, nullptr);
    if (clicked_row_out &&
        ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
        ImPlotPoint p = ImPlot::GetPlotMousePos();
        // ImPlot heatmap Y se pinta de top a bottom; plot mouse_y va igual
        // (default scale 0..rows). Mapeo directo.
        int rr, cc;
        heatmap_cell_at(p.x, p.y, rows, cols, rr, cc);
        if (rr >= 0) *clicked_row_out = rr;
        (void)cc;
    }
    ImPlot::EndPlot();
    return true;
}

bool render_pie(const StageOutput& out, const ViewConfig& cfg, bool donut, ImVec2 size,
                 int* clicked_row_out = nullptr) {
    int cat = resolve_cat(out, cfg, first_category_col(out));
    auto nums = collect_numeric_filtered(out, cfg, 1);
    if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
    auto cats = extract_category(out, cat);
    int n = std::min((int)cats.size(), (int)nums[0].vals.size());
    if (n == 0) return false;
    std::vector<double> values(n);
    std::vector<const char*> labels(n);
    for (int i = 0; i < n; ++i) {
        double d = nums[0].vals[i];
        values[i] = std::isnan(d) ? 0.0 : std::abs(d);
        labels[i] = cats[i].c_str();
    }
    ImPlotFlags pf = ImPlotFlags_Equal;
    if (!cfg.show_legend) pf |= ImPlotFlags_NoLegend;
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##pie", size, pf)) return false;
    ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
                      ImPlotAxisFlags_NoDecorations);
    ImPlot::SetupAxesLimits(0, 1, 0, 1, ImPlotCond_Always);
    double radius = (cfg.pie_radius > 0) ? cfg.pie_radius : (donut ? 0.4 : 0.45);
    ImPlot::PlotPieChart(labels.data(), values.data(), n, 0.5, 0.5, radius, "%.1f");
    if (donut) {
        // Draw inner hole as solid circle by overlaying a smaller pie of one slice transparent.
        // Simpler: just visually it's a circle with text. Use no extra primitive for now.
    }
    if (clicked_row_out &&
        ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
        ImPlotPoint p = ImPlot::GetPlotMousePos();
        double dx = p.x - 0.5, dy = p.y - 0.5;
        double dist2 = dx*dx + dy*dy;
        double inner = donut ? (radius * 0.5) : 0.0;
        if (dist2 <= radius * radius && dist2 >= inner * inner) {
            double ang = pie_angle(0.5, 0.5, p.x, p.y);
            int idx = pie_slice_at_angle(ang, values.data(), n);
            if (idx >= 0) *clicked_row_out = idx;
        }
    }
    ImPlot::EndPlot();
    return true;
}

bool render_funnel(const StageOutput& out, const ViewConfig& cfg, ImVec2 size,
                    int* clicked_row_out = nullptr) {
    int cat = resolve_cat(out, cfg, first_category_col(out));
    auto nums = collect_numeric_filtered(out, cfg, 1);
    if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
    auto cats = extract_category(out, cat);
    int n = std::min((int)cats.size(), (int)nums[0].vals.size());
    if (n == 0) return false;
    // Sort desc by value
    std::vector<int> idx(n);
    for (int i = 0; i < n; ++i) idx[i] = i;
    std::sort(idx.begin(), idx.end(), [&](int a, int b) {
        double da = std::isnan(nums[0].vals[a]) ? -1e300 : nums[0].vals[a];
        double db = std::isnan(nums[0].vals[b]) ? -1e300 : nums[0].vals[b];
        return da > db;
    });
    std::vector<double> ys(n);
    std::vector<double> ticks(n);
    std::vector<const char*> labels(n);
    std::vector<std::string> labels_store(n);
    for (int i = 0; i < n; ++i) {
        double d = nums[0].vals[idx[i]];
        ys[i]      = std::isnan(d) ? 0 : d;
        ticks[i]   = n - 1 - i;       // descending order
        labels_store[i] = cats[idx[i]];
        labels[i]  = labels_store[i].c_str();
    }
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##funnel", size, 0)) return false;
    ImPlot::SetupAxes(nums[0].name.c_str(), out.headers[cat].c_str(),
                      axflag(cfg), axflag(cfg));
    ImPlot::SetupAxisTicks(ImAxis_Y1, ticks.data(), n, labels.data(), false);
    ImPlot::PlotBars(nums[0].name.c_str(), ys.data(), ticks.data(), n, 0.85,
                     ImPlotSpec(ImPlotProp_Flags, ImPlotBarsFlags_Horizontal));
    if (clicked_row_out &&
        ImPlot::IsPlotHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
        ImPlotPoint p = ImPlot::GetPlotMousePos();
        int tick_idx = (int)(p.y + 0.5);
        // ticks[i] = n-1-i. Invertir para idx en orden sorted descendiente.
        int sorted_pos = (n - 1) - tick_idx;
        if (sorted_pos >= 0 && sorted_pos < n) {
            // idx[sorted_pos] da indice de row original en out.
            *clicked_row_out = idx[sorted_pos];
        }
    }
    ImPlot::EndPlot();
    return true;
}

bool render_waterfall(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
    auto nums = collect_numeric_filtered(out, cfg, 1);
    if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
    int n = (int)nums[0].vals.size();
    if (n == 0) return false;
    int cat = resolve_cat(out, cfg, first_category_col(out));
    auto cats = (cat >= 0) ? extract_category(out, cat) : std::vector<std::string>();

    std::vector<double> running(n + 1, 0);
    for (int i = 0; i < n; ++i) {
        double d = std::isnan(nums[0].vals[i]) ? 0 : nums[0].vals[i];
        running[i + 1] = running[i] + d;
    }
    std::vector<double> ticks(n);
    for (int i = 0; i < n; ++i) ticks[i] = i;
    std::vector<const char*> labels(n);
    for (int i = 0; i < n; ++i) labels[i] = (i < (int)cats.size()) ? cats[i].c_str() : "";

    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##waterfall", size, 0)) return false;
    ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
                      axflag(cfg), axflag(cfg));
    if (cat >= 0) ImPlot::SetupAxisTicks(ImAxis_X1, ticks.data(), n, labels.data(), false);
    // Draw stems with rectangles via error-bars trick: low=cum_prev, high=cum_curr.
    std::vector<double> mid(n), err(n);
    for (int i = 0; i < n; ++i) {
        mid[i] = (running[i] + running[i + 1]) * 0.5;
        err[i] = std::abs((running[i + 1] - running[i]) * 0.5);
    }
    ImPlot::PlotErrorBars("##wf", ticks.data(), mid.data(), err.data(), n);
    ImPlot::PlotLine("cum", running.data() + 1, n);
    ImPlot::EndPlot();
    return true;
}

bool render_kpi_single(const StageOutput& out, const ViewConfig& cfg) {
    int nc = !cfg.y_cols.empty() ? find_header(out, cfg.y_cols[0]) : -1;
    if (nc < 0) nc = first_numeric_col(out);
    if (nc < 0) { info_text("Need 1 numeric column"); return false; }
    auto vals = extract_numeric(out, nc);
    if (vals.empty()) { info_text("Empty"); return false; }
    double last = std::nan("");
    for (auto v : vals) if (!std::isnan(v)) last = v;
    if (std::isnan(last)) { info_text("No finite values"); return false; }

    char buf[64];
    if (std::abs(last) >= 1e6)       std::snprintf(buf, sizeof(buf), "%.2fM", last / 1e6);
    else if (std::abs(last) >= 1e3)  std::snprintf(buf, sizeof(buf), "%.2fK", last / 1e3);
    else                              std::snprintf(buf, sizeof(buf), "%.3g", last);

    ImVec2 avail = ImGui::GetContentRegionAvail();
    ImGui::SetWindowFontScale(4.0f);
    ImVec2 sz = ImGui::CalcTextSize(buf);
    ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
                                ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f - 20));
    ImGui::TextUnformatted(buf);
    ImGui::SetWindowFontScale(1.0f);

    sz = ImGui::CalcTextSize(out.headers[nc].c_str());
    ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (avail.x - sz.x) * 0.5f,
                                ImGui::GetCursorPosY() + (avail.y - sz.y) * 0.5f - 10));
    ImGui::TextDisabled("%s", out.headers[nc].c_str());
    return true;
}

bool render_kpi_grid(const StageOutput& out, const ViewConfig& cfg) {
    auto nums = collect_numeric_filtered(out, cfg, 12);
    if (nums.empty()) { info_text("Need numeric columns"); return false; }
    ImVec2 avail = ImGui::GetContentRegionAvail();
    int per_row = std::max(1, (int)(avail.x / 220));
    int idx = 0;
    for (auto& nc : nums) {
        double last = std::nan("");
        for (auto v : nc.vals) if (!std::isnan(v)) last = v;
        if (std::isnan(last)) last = 0;
        char buf[64];
        if (std::abs(last) >= 1e6)       std::snprintf(buf, sizeof(buf), "%.2fM", last / 1e6);
        else if (std::abs(last) >= 1e3)  std::snprintf(buf, sizeof(buf), "%.2fK", last / 1e3);
        else                              std::snprintf(buf, sizeof(buf), "%.4g", last);

        ImGui::BeginChild((ImGuiID)(0x1000 + idx), ImVec2(210, 100), true);
        ImGui::TextDisabled("%s", nc.name.c_str());
        ImGui::SetWindowFontScale(2.4f);
        ImGui::TextUnformatted(buf);
        ImGui::SetWindowFontScale(1.0f);
        ImGui::EndChild();

        if ((idx % per_row) != (per_row - 1)) ImGui::SameLine();
        idx++;
    }
    return true;
}

bool render_stem(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
    auto nums = collect_numeric_filtered(out, cfg, 1);
    if (nums.empty()) { info_text("Need 1 numeric column"); return false; }
    int n = (int)nums[0].vals.size();
    std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##stem", size, 0)) return false;
    ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
                      axflag(cfg), axflag(cfg));
    ImPlot::PlotStems(nums[0].name.c_str(), xs.data(), nums[0].vals.data(), n);
    ImPlot::EndPlot();
    return true;
}

bool render_errorbars(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
    auto nums = collect_numeric_filtered(out, cfg, 4);
    if (nums.size() < 2) { info_text("Need 2 numeric columns (value, err)"); return false; }
    int n = (int)nums[0].vals.size();
    std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##eb", size, 0)) return false;
    ImPlot::SetupAxes(nullptr, nums[0].name.c_str(),
                      axflag(cfg), axflag(cfg));
    ImPlot::PlotErrorBars(nums[0].name.c_str(), xs.data(),
                          nums[0].vals.data(), nums[1].vals.data(), n);
    ImPlot::PlotScatter("##s", xs.data(), nums[0].vals.data(), n);
    ImPlot::EndPlot();
    return true;
}

// BoxPlot: agrupar por categoria, calcular min/p25/p50/p75/max y dibujar
// rectangulos manuales via PlotShaded + lineas.
bool render_boxplot(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
    int cat = resolve_cat(out, cfg, first_category_col(out));
    auto nums = collect_numeric_filtered(out, cfg, 1);
    if (cat < 0 || nums.empty()) { info_text("Need 1 category + 1 numeric"); return false; }
    auto cats = extract_category(out, cat);
    int n = std::min((int)cats.size(), (int)nums[0].vals.size());
    if (n == 0) return false;

    // Group values by category
    std::unordered_map<std::string, std::vector<double>> groups;
    std::vector<std::string> order;
    for (int i = 0; i < n; ++i) {
        if (groups.find(cats[i]) == groups.end()) order.push_back(cats[i]);
        double d = nums[0].vals[i];
        if (!std::isnan(d)) groups[cats[i]].push_back(d);
    }
    int G = (int)order.size();
    if (G == 0) return false;

    std::vector<double> mn(G), p25(G), p50(G), p75(G), mx(G), xs(G);
    std::vector<const char*> labels(G);
    for (int g = 0; g < G; ++g) {
        auto& v = groups[order[g]];
        std::sort(v.begin(), v.end());
        int N = (int)v.size();
        xs[g]    = g;
        labels[g]= order[g].c_str();
        if (N == 0) { mn[g]=p25[g]=p50[g]=p75[g]=mx[g]=0; continue; }
        mn[g]  = v.front();
        mx[g]  = v.back();
        p25[g] = v[std::min(N - 1, (int)(N * 0.25))];
        p50[g] = v[std::min(N - 1, (int)(N * 0.50))];
        p75[g] = v[std::min(N - 1, (int)(N * 0.75))];
    }

    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##box", size, 0)) return false;
    ImPlot::SetupAxes(out.headers[cat].c_str(), nums[0].name.c_str(),
                      axflag(cfg), axflag(cfg));
    ImPlot::SetupAxisTicks(ImAxis_X1, xs.data(), G, labels.data(), false);

    // Whiskers: stems from min to max
    for (int g = 0; g < G; ++g) {
        double lo[2] = { mn[g], mx[g] };
        double xx[2] = { xs[g], xs[g] };
        ImPlot::PlotLine("##wh", xx, lo, 2);
    }
    // Box: p25..p75 as bars centered on p50
    std::vector<double> mid(G), half(G);
    for (int g = 0; g < G; ++g) {
        mid[g]  = (p25[g] + p75[g]) * 0.5;
        half[g] = (p75[g] - p25[g]) * 0.5;
    }
    ImPlot::PlotErrorBars("box", xs.data(), mid.data(), half.data(), G);
    ImPlot::PlotScatter("median", xs.data(), p50.data(), G);
    ImPlot::EndPlot();
    return true;
}

// Candlestick: tiempo + O/H/L/C. Asume 4 primeras cols numericas en ese orden.
bool render_candlestick(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
    auto nums = collect_numeric_filtered(out, cfg, 8);
    if (nums.size() < 4) { info_text("Need 4 numeric columns: O/H/L/C"); return false; }
    int n = (int)nums[0].vals.size();
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##candle", size, 0)) return false;
    ImPlot::SetupAxes("t", "price", axflag(cfg), axflag(cfg));
    std::vector<double> xs(n); for (int i = 0; i < n; ++i) xs[i] = i;
    const auto& O = nums[0].vals;
    const auto& H = nums[1].vals;
    const auto& L = nums[2].vals;
    const auto& C = nums[3].vals;
    // Wicks
    for (int i = 0; i < n; ++i) {
        double xx[2] = { xs[i], xs[i] };
        double yy[2] = { L[i], H[i] };
        ImPlot::PlotLine("##wick", xx, yy, 2);
    }
    // Body via PlotBars(mid, |C-O|)? Simpler: separate lines.
    std::vector<double> body_low(n), body_high(n), body_mid(n), body_err(n);
    for (int i = 0; i < n; ++i) {
        body_low[i]  = std::min(O[i], C[i]);
        body_high[i] = std::max(O[i], C[i]);
        body_mid[i]  = (body_low[i] + body_high[i]) * 0.5;
        body_err[i]  = (body_high[i] - body_low[i]) * 0.5;
    }
    ImPlot::PlotErrorBars("OHLC", xs.data(), body_mid.data(), body_err.data(), n);
    ImPlot::EndPlot();
    return true;
}

bool render_radar(const StageOutput& out, const ViewConfig& cfg, ImVec2 size) {
    auto nums = collect_numeric_filtered(out, cfg, 12);
    if (nums.size() < 3) { info_text("Need 3+ numeric columns"); return false; }
    int K = (int)nums.size();
    int n = (int)nums[0].vals.size();
    if (n == 0) return false;
    // Take first row as the polygon.
    std::vector<double> xs(K + 1), ys(K + 1);
    double radius_norm = 0;
    for (int k = 0; k < K; ++k) {
        double d = nums[k].vals[0];
        if (std::isnan(d)) d = 0;
        radius_norm = std::max(radius_norm, std::abs(d));
    }
    if (radius_norm == 0) radius_norm = 1;
    for (int k = 0; k < K; ++k) {
        double v = nums[k].vals[0]; if (std::isnan(v)) v = 0;
        double angle = 2 * 3.14159265358979 * k / K - 3.14159265358979 / 2;
        double r = v / radius_norm;
        xs[k] = std::cos(angle) * r;
        ys[k] = std::sin(angle) * r;
    }
    xs[K] = xs[0]; ys[K] = ys[0];
    maybe_fit(cfg);
    if (!ImPlot::BeginPlot("##radar", size,
                           ImPlotFlags_Equal | ImPlotFlags_NoLegend)) return false;
    ImPlot::SetupAxesLimits(-1.2, 1.2, -1.2, 1.2, ImPlotCond_Always);
    ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations,
                      ImPlotAxisFlags_NoDecorations);
    // Grid rings
    for (double rr : {0.25, 0.5, 0.75, 1.0}) {
        double gx[64], gy[64];
        for (int i = 0; i < 64; ++i) {
            double a = 2 * 3.14159265358979 * i / 63;
            gx[i] = std::cos(a) * rr; gy[i] = std::sin(a) * rr;
        }
        ImPlot::PlotLine("##grid", gx, gy, 64);
    }
    ImPlot::PlotLine("radar", xs.data(), ys.data(), K + 1);
    // Axis labels
    for (int k = 0; k < K; ++k) {
        double a = 2 * 3.14159265358979 * k / K - 3.14159265358979 / 2;
        ImPlot::PlotText(nums[k].name.c_str(), std::cos(a) * 1.1, std::sin(a) * 1.1);
    }
    ImPlot::EndPlot();
    return true;
}

} // anon

bool render(const StageOutput& out, ViewMode mode,
            const ViewConfig& cfg, ImVec2 size,
            int* clicked_row_out) {
    if (clicked_row_out) *clicked_row_out = -1;
    if (out.rows == 0 || out.cols == 0) {
        info_text("No data");
        return false;
    }
    switch (mode) {
        case ViewMode::Table:        return false;
        case ViewMode::Bar:
        case ViewMode::Column:
        case ViewMode::GroupedBar:
        case ViewMode::StackedBar:   return render_bar_like(out, mode, cfg, size, clicked_row_out);
        case ViewMode::Line:
        case ViewMode::Area:
        case ViewMode::Stairs:       return render_line_like(out, mode, cfg, size);
        case ViewMode::Scatter:      return render_scatter(out, cfg, size, clicked_row_out);
        case ViewMode::Bubble:       return render_bubble(out, cfg, size, clicked_row_out);
        case ViewMode::Histogram:    return render_histogram(out, cfg, size);
        case ViewMode::Histogram2D:  return render_hist2d(out, cfg, size);
        case ViewMode::Heatmap:      return render_heatmap(out, cfg, size, clicked_row_out);
        case ViewMode::BoxPlot:      return render_boxplot(out, cfg, size);
        case ViewMode::Stem:         return render_stem(out, cfg, size);
        case ViewMode::ErrorBars:    return render_errorbars(out, cfg, size);
        case ViewMode::Pie:          return render_pie(out, cfg, false, size, clicked_row_out);
        case ViewMode::Donut:        return render_pie(out, cfg, true,  size, clicked_row_out);
        case ViewMode::Funnel:       return render_funnel(out, cfg, size, clicked_row_out);
        case ViewMode::Waterfall:    return render_waterfall(out, cfg, size);
        case ViewMode::KPI:          return render_kpi_single(out, cfg);
        case ViewMode::KPIGrid:      return render_kpi_grid(out, cfg);
        case ViewMode::Candlestick:  return render_candlestick(out, cfg, size);
        case ViewMode::Radar:        return render_radar(out, cfg, size);
    }
    return false;
}

} // namespace viz
