// compute_stage — Pure TQL stage execution.
// Promoted from cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp
// (issue 0081-B). No ImGui, no I/O. Helper statics are file-private.
#include "core/compute_stage.h"
#include "core/tql_helpers.h"  // aggregation_alias, agg_fn_token (avoids ODR dup)

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

namespace data_table {

ColumnType aggregation_type(const Aggregation& a,
                            const std::vector<std::string>& in_headers,
                            const std::vector<ColumnType>&  in_types)
{
    if (a.fn == AggFn::Count || a.fn == AggFn::Distinct) return ColumnType::Int;
    if (a.fn == AggFn::Min || a.fn == AggFn::Max) {
        for (size_t i = 0; i < in_headers.size(); ++i) {
            if (in_headers[i] == a.col && i < in_types.size()) return in_types[i];
        }
        return ColumnType::String;
    }
    return ColumnType::Float;
}

std::vector<int> apply_filters(const char* const* cells, int rows, int cols,
                               const std::vector<Filter>& filters)
{
    std::vector<int> out;
    out.reserve(rows);
    for (int r = 0; r < rows; ++r) {
        bool keep = true;
        for (const auto& f : filters) {
            if (f.col < 0 || f.col >= cols) continue;
            const char* cell = cells[r * cols + f.col];
            // compare inline: numeric if both parseable, lexical otherwise
            auto do_compare = [](const char* a, const char* b, Op op) -> bool {
                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;
                char* ea = nullptr; char* eb = nullptr;
                na = std::strtod(a, &ea); nb = std::strtod(b, &eb);
                bool numeric = (ea != a && *ea == '\0') && (eb != b && *eb == '\0');
                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;
            };
            if (!do_compare(cell, f.value.c_str(), f.op)) { keep = false; break; }
        }
        if (keep) out.push_back(r);
    }
    return out;
}

// ----------------------------------------------------------------------------
// File-private helpers (static)
// ----------------------------------------------------------------------------

static bool parse_num(const char* s, double& out) {
    if (!s || !*s) return false;
    char* end = nullptr;
    double v = std::strtod(s, &end);
    if (end == s) return false;
    while (*end == ' ' || *end == '\t') end++;
    if (*end != '\0') return false;
    out = v;
    return true;
}

namespace {

int find_col(const std::vector<std::string>& headers, const std::string& name) {
    for (size_t i = 0; i < headers.size(); ++i) if (headers[i] == name) return (int)i;
    return -1;
}

int cmp_cells(const char* a, const char* b) {
    if (!a) a = ""; if (!b) b = "";
    double na, nb;
    bool num = parse_num(a, na) && parse_num(b, nb);
    if (num) return (na < nb) ? -1 : (na > nb ? 1 : 0);
    return std::strcmp(a, b);
}

void apply_sorts(std::vector<int>& row_idx,
                 const char* const* cells, int cols,
                 const std::vector<std::string>& headers,
                 const std::vector<SortClause>& sorts)
{
    if (sorts.empty()) return;
    std::vector<int> sort_cols(sorts.size());
    for (size_t i = 0; i < sorts.size(); ++i) sort_cols[i] = find_col(headers, sorts[i].col);
    std::sort(row_idx.begin(), row_idx.end(), [&](int a, int b){
        for (size_t i = 0; i < sorts.size(); ++i) {
            int sc = sort_cols[i];
            if (sc < 0) continue;
            int c = cmp_cells(cells[a * cols + sc], cells[b * cols + sc]);
            if (c != 0) return sorts[i].desc ? (c > 0) : (c < 0);
        }
        return false;
    });
}

double percentile_value(std::vector<double>& v, double p) {
    if (v.empty()) return 0.0;
    std::sort(v.begin(), v.end());
    double idx = p * (v.size() - 1);
    size_t lo = (size_t)idx;
    size_t hi = std::min(lo + 1, v.size() - 1);
    double t  = idx - lo;
    return v[lo] * (1.0 - t) + v[hi] * t;
}

double compute_agg_numeric(AggFn fn, std::vector<double>& vals, double arg) {
    if (vals.empty()) return 0.0;
    switch (fn) {
        case AggFn::Sum: {
            double s = 0; for (double v : vals) s += v; return s;
        }
        case AggFn::Avg: {
            double s = 0; for (double v : vals) s += v; return s / vals.size();
        }
        case AggFn::Min: {
            double m = vals[0]; for (double v : vals) if (v < m) m = v; return m;
        }
        case AggFn::Max: {
            double m = vals[0]; for (double v : vals) if (v > m) m = v; return m;
        }
        case AggFn::Stddev: {
            double s = 0; for (double v : vals) s += v;
            double mean = s / vals.size();
            double var = 0; for (double v : vals) { double d = v - mean; var += d * d; }
            return std::sqrt(var / vals.size());
        }
        case AggFn::Median:     return percentile_value(vals, 0.50);
        case AggFn::P25:        return percentile_value(vals, 0.25);
        case AggFn::P75:        return percentile_value(vals, 0.75);
        case AggFn::P90:        return percentile_value(vals, 0.90);
        case AggFn::P99:        return percentile_value(vals, 0.99);
        case AggFn::Percentile: return percentile_value(vals, arg);
        default: return 0.0;
    }
}

std::string format_double(double v) {
    char buf[64];
    long long iv = (long long)v;
    if ((double)iv == v) std::snprintf(buf, sizeof(buf), "%lld", iv);
    else                  std::snprintf(buf, sizeof(buf), "%.4g", v);
    return buf;
}

// parse_breakout_granularity: strip optional ":granularity" suffix.
// DateGranularity is defined in data_table_types.h (issue 0081).
DateGranularity parse_breakout_granularity_local(const std::string& breakout,
                                                  std::string& col_out)
{
    auto pos = breakout.rfind(':');
    if (pos == std::string::npos) {
        col_out = breakout;
        return DateGranularity::None;
    }
    std::string suffix = breakout.substr(pos + 1);
    DateGranularity g = DateGranularity::None;
    if      (suffix == "year")  g = DateGranularity::Year;
    else if (suffix == "month") g = DateGranularity::Month;
    else if (suffix == "week")  g = DateGranularity::Week;
    else if (suffix == "day")   g = DateGranularity::Day;
    else if (suffix == "hour")  g = DateGranularity::Hour;
    if (g == DateGranularity::None) {
        col_out = breakout;
        return DateGranularity::None;
    }
    col_out = breakout.substr(0, pos);
    return g;
}

// truncate_date: truncate ISO date string to given granularity.
std::string truncate_date_local(const std::string& date, DateGranularity g) {
    if (g == DateGranularity::None) return date;
    if (date.size() < 10) return date;
    // Parse YYYY-MM-DD
    auto d = [](char c){ return c >= '0' && c <= '9'; };
    if (!d(date[0])||!d(date[1])||!d(date[2])||!d(date[3])) return date;
    if (date[4] != '-') return date;
    if (!d(date[5])||!d(date[6])) return date;
    if (date[7] != '-') return date;
    if (!d(date[8])||!d(date[9])) return date;
    int y = (date[0]-'0')*1000+(date[1]-'0')*100+(date[2]-'0')*10+(date[3]-'0');
    int m = (date[5]-'0')*10+(date[6]-'0');
    int dd_= (date[8]-'0')*10+(date[9]-'0');
    if (m < 1 || m > 12 || dd_ < 1 || dd_ > 31) return date;
    char buf[32];
    switch (g) {
        case DateGranularity::Year:
            std::snprintf(buf, sizeof(buf), "%04d", y);
            return buf;
        case DateGranularity::Month:
            std::snprintf(buf, sizeof(buf), "%04d-%02d", y, m);
            return buf;
        case DateGranularity::Day:
            std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", y, m, dd_);
            return buf;
        case DateGranularity::Hour: {
            int hh = 0;
            if (date.size() >= 13 && date[10] == 'T'
                && d(date[11]) && d(date[12])) {
                hh = (date[11]-'0')*10 + (date[12]-'0');
                if (hh < 0 || hh > 23) hh = 0;
            }
            std::snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d", y, m, dd_, hh);
            return buf;
        }
        case DateGranularity::Week: {
            // Hinnant civil calendar: ymd -> days since 0000-03-01.
            // days%7: 0=Wed. Monday offset: ((days%7 - 5 + 7) % 7).
            auto ymd_to_days = [](int yy, int mm, int dd2) -> long {
                if (mm <= 2) { yy -= 1; mm += 12; }
                long era = (yy >= 0 ? yy : yy - 399) / 400;
                unsigned yoe = (unsigned)(yy - era * 400);
                unsigned doy = (unsigned)((153*(mm-3)+2)/5 + dd2 - 1);
                unsigned doe = yoe*365 + yoe/4 - yoe/100 + doy;
                return era * 146097 + (long)doe;
            };
            auto days_to_ymd = [](long days, int& yy, int& mm, int& dd2) {
                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 day2 = doy - (153*mp+2)/5 + 1;
                unsigned mon = mp < 10 ? mp+3 : mp-9;
                if (mon <= 2) yr += 1;
                yy = yr; mm = (int)mon; dd2 = (int)day2;
            };
            long days = ymd_to_days(y, m, dd_);
            int mod = (int)(((days % 7) + 7) % 7);
            int rem = ((mod - 5) % 7 + 7) % 7;
            long monday = days - rem;
            int yy2, mm2, dd2;
            days_to_ymd(monday, yy2, mm2, dd2);
            std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d", yy2, mm2, dd2);
            return buf;
        }
        default: return date;
    }
}

} // anon namespace

// ----------------------------------------------------------------------------
// compute_stage (public)
// ----------------------------------------------------------------------------

StageOutput compute_stage(const char* const* in_cells, int in_rows, int in_cols,
                          const std::vector<std::string>& in_headers,
                          const std::vector<ColumnType>&  in_types,
                          const Stage& stage)
{
    StageOutput out;
    auto visible = apply_filters(in_cells, in_rows, in_cols, stage.filters);

    bool grouped = !stage.breakouts.empty() || !stage.aggregations.empty();

    if (!grouped) {
        // Passthrough: same shape, filtered + sorted.
        out.cols    = in_cols;
        out.headers = in_headers;
        out.types   = in_types;
        apply_sorts(visible, in_cells, in_cols, in_headers, stage.sorts);
        out.rows = (int)visible.size();
        out.cells.reserve((size_t)out.rows * in_cols);
        for (int r : visible) {
            for (int c = 0; c < in_cols; ++c) out.cells.push_back(in_cells[r * in_cols + c]);
        }
        return out;
    }

    // Grouped: group visible rows by breakout values, compute aggregations.
    int nbreaks = (int)stage.breakouts.size();
    std::vector<int>             break_cols(nbreaks);
    std::vector<DateGranularity> break_grans(nbreaks);
    bool any_trunc = false;
    for (int i = 0; i < nbreaks; ++i) {
        std::string col_name;
        break_grans[i] = parse_breakout_granularity_local(stage.breakouts[i], col_name);
        if (break_grans[i] != DateGranularity::None) any_trunc = true;
        break_cols[i] = find_col(in_headers, col_name);
    }

    // Pre-allocate cell_backing to avoid pointer invalidation on push_back.
    out.cell_backing.reserve(
        (size_t)in_rows * (size_t)nbreaks +
        (size_t)in_rows * stage.aggregations.size() + 16);

    std::vector<const char*> trunc_ptrs;
    if (any_trunc) {
        trunc_ptrs.assign((size_t)in_rows * (size_t)nbreaks, nullptr);
        for (int r = 0; r < in_rows; ++r) {
            for (int i = 0; i < nbreaks; ++i) {
                if (break_grans[i] == DateGranularity::None) continue;
                int bc = break_cols[i];
                if (bc < 0) continue;
                const char* v = in_cells[r * in_cols + bc];
                out.cell_backing.emplace_back(
                    truncate_date_local(v ? v : "", break_grans[i]));
                trunc_ptrs[(size_t)r * nbreaks + i] = out.cell_backing.back().c_str();
            }
        }
    }

    auto cell_for = [&](int r, int i) -> const char* {
        int bc = break_cols[i];
        if (bc < 0) return "";
        if (break_grans[i] != DateGranularity::None) {
            return trunc_ptrs[(size_t)r * nbreaks + i];
        }
        const char* v = in_cells[r * in_cols + bc];
        return v ? v : "";
    };

    auto make_key = [&](int r) -> std::string {
        std::string k;
        for (int i = 0; i < nbreaks; ++i) {
            if (i > 0) k += '\x1f';
            k += cell_for(r, i);
        }
        return k;
    };

    std::unordered_map<std::string, int> key_to_group;
    std::vector<std::string>              group_keys;
    std::vector<std::vector<int>>         group_rows;
    std::vector<std::vector<const char*>> group_breakvals;
    for (int r : visible) {
        std::string k = make_key(r);
        auto it = key_to_group.find(k);
        int gi;
        if (it == key_to_group.end()) {
            gi = (int)group_rows.size();
            key_to_group.emplace(k, gi);
            group_keys.push_back(k);
            group_rows.emplace_back();
            std::vector<const char*> bv((size_t)nbreaks, "");
            for (int i = 0; i < nbreaks; ++i) bv[i] = cell_for(r, i);
            group_breakvals.push_back(std::move(bv));
        } else gi = it->second;
        group_rows[gi].push_back(r);
    }

    // Build output headers + types: breakouts + aggregation aliases.
    int out_cols = (int)stage.breakouts.size() + (int)stage.aggregations.size();
    out.cols = out_cols;
    out.headers.reserve(out_cols);
    out.types.reserve(out_cols);
    for (int i = 0; i < nbreaks; ++i) {
        out.headers.push_back(stage.breakouts[i]);
        int bc = break_cols[i];
        ColumnType ot = ColumnType::String;
        if (break_grans[i] == DateGranularity::None
            && bc >= 0 && bc < (int)in_types.size()) {
            ot = in_types[bc];
        }
        out.types.push_back(ot);
    }
    for (const auto& a : stage.aggregations) {
        out.headers.push_back(aggregation_alias(a));
        out.types.push_back(aggregation_type(a, in_headers, in_types));
    }

    // Compute aggregation per group.
    int n_groups = (int)group_rows.size();
    // Reserve exact size to prevent pointer invalidation.
    out.cell_backing.reserve(out.cell_backing.size() + (size_t)n_groups * stage.aggregations.size() + 16);

    auto store_backing = [&](const std::string& s) -> const char* {
        out.cell_backing.push_back(s);
        return out.cell_backing.back().c_str();
    };

    std::vector<const char*> flat;
    flat.reserve((size_t)n_groups * out_cols);
    for (int gi = 0; gi < n_groups; ++gi) {
        for (size_t i = 0; i < stage.breakouts.size(); ++i) {
            flat.push_back(group_breakvals[gi][i]);
        }
        for (const auto& a : stage.aggregations) {
            if (a.fn == AggFn::Count) {
                flat.push_back(store_backing(format_double((double)group_rows[gi].size())));
                continue;
            }
            if (a.fn == AggFn::Distinct) {
                int ac = find_col(in_headers, a.col);
                if (ac < 0) { flat.push_back(store_backing("0")); continue; }
                std::unordered_set<std::string> uniq;
                for (int r : group_rows[gi]) {
                    const char* v = in_cells[r * in_cols + ac];
                    if (v && *v) uniq.insert(v);
                }
                flat.push_back(store_backing(format_double((double)uniq.size())));
                continue;
            }
            int ac = find_col(in_headers, a.col);
            if (ac < 0) { flat.push_back(store_backing("")); continue; }
            // min/max over strings: preserve type
            if ((a.fn == AggFn::Min || a.fn == AggFn::Max) &&
                ac < (int)in_types.size() &&
                (in_types[ac] == ColumnType::String || in_types[ac] == ColumnType::Date))
            {
                const char* best = nullptr;
                for (int r : group_rows[gi]) {
                    const char* v = in_cells[r * in_cols + ac];
                    if (!v || !*v) continue;
                    if (!best) { best = v; continue; }
                    int c = std::strcmp(v, best);
                    if ((a.fn == AggFn::Min && c < 0) || (a.fn == AggFn::Max && c > 0)) best = v;
                }
                flat.push_back(best ? best : store_backing(""));
                continue;
            }
            std::vector<double> vals;
            vals.reserve(group_rows[gi].size());
            for (int r : group_rows[gi]) {
                const char* v = in_cells[r * in_cols + ac];
                if (!v || !*v) continue;
                double d;
                if (parse_num(v, d)) vals.push_back(d);
            }
            double agg_val = compute_agg_numeric(a.fn, vals, a.arg);
            flat.push_back(store_backing(format_double(agg_val)));
        }
    }

    // Sort groups by stage.sorts (col-name lookup in out.headers).
    std::vector<int> grp_idx(n_groups);
    for (int i = 0; i < n_groups; ++i) grp_idx[i] = i;
    apply_sorts(grp_idx, flat.data(), out_cols, out.headers, stage.sorts);

    out.rows = n_groups;
    out.cells.reserve((size_t)n_groups * out_cols);
    for (int gi : grp_idx) {
        for (int c = 0; c < out_cols; ++c) {
            out.cells.push_back(flat[gi * out_cols + c]);
        }
    }
    return out;
}

// ---------------------------------------------------------------------------
// State method implementations (declared in data_table_types.h).
// Placed here as the header documents "defined in compute_stage.cpp".
// Promoted from playground data_table_logic.cpp — issue 0081-I Wave 3.5.
// ---------------------------------------------------------------------------

void State::ensure_stage0() {
    if (stages.empty()) stages.push_back(Stage{});
    if (active_stage < 0) active_stage = 0;
    if (active_stage >= (int)stages.size()) active_stage = (int)stages.size() - 1;
}

Stage& State::raw() {
    ensure_stage0();
    return stages[0];
}

const Stage& State::raw() const {
    static thread_local Stage empty;
    if (stages.empty()) return empty;
    return stages[0];
}

Stage& State::active() {
    ensure_stage0();
    return stages[active_stage];
}

const Stage& State::active_const() const {
    static thread_local Stage empty;
    if (stages.empty()) return empty;
    int a = active_stage;
    if (a < 0 || a >= (int)stages.size()) a = 0;
    return stages[a];
}

} // namespace data_table
