// lua_engine.cpp — implementacion del sandbox Lua 5.4 para formulas.
// Extraido del playground tables (issue 0081-D).
#include "core/lua_engine.h"
#include "core/auto_detect_type.h"   // parse_number, is_bool_text, etc.

extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

#include <cctype>
#include <cstring>
#include <cstdio>
#include <string>

namespace lua_engine {

struct Engine {
    lua_State*               L = nullptr;
    std::vector<RowCtx*>     ctx_stack;
    std::vector<int>         visiting_derived;
};

namespace {

Engine* g_engine = nullptr;

Engine* engine_from_state(lua_State* L) {
    return *static_cast<Engine**>(lua_getextraspace(L));
}

RowCtx* current_ctx(lua_State* L) {
    Engine* e = engine_from_state(L);
    if (!e || e->ctx_stack.empty()) return nullptr;
    return e->ctx_stack.back();
}

// ---------------------------------------------------------------------------
// Push de cell respetando tipo declarado:
//  Int/Float -> number (integer si exacto)
//  Bool      -> boolean (true/false/1/0); en otro caso push string
//  Date/String/Json/Auto -> string
//  Si types_orig == nullptr -> heuristica: parse_number; si parsea -> number.
// ---------------------------------------------------------------------------
void push_typed(lua_State* L, const char* v, data_table::ColumnType t) {
    if (!v || !*v) { lua_pushnil(L); return; }
    using data_table::ColumnType;
    using data_table::parse_number;
    if (t == ColumnType::Int) {
        double d;
        if (parse_number(v, d)) {
            long long iv = (long long)d;
            if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
            else                 lua_pushnumber (L, (lua_Number)d);
        } else lua_pushstring(L, v);
        return;
    }
    if (t == ColumnType::Float) {
        double d;
        if (parse_number(v, d)) {
            long long iv = (long long)d;
            if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
            else                 lua_pushnumber (L, (lua_Number)d);
        } else lua_pushstring(L, v);
        return;
    }
    if (t == ColumnType::Bool) {
        if (std::strcmp(v, "true")  == 0 || std::strcmp(v, "1") == 0) lua_pushboolean(L, 1);
        else if (std::strcmp(v, "false") == 0 || std::strcmp(v, "0") == 0) lua_pushboolean(L, 0);
        else lua_pushstring(L, v);
        return;
    }
    if (t == ColumnType::Auto) {
        // Sin tipo declarado: heuristica. parse_number -> number, else string.
        double d;
        if (parse_number(v, d)) {
            long long iv = (long long)d;
            if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
            else                 lua_pushnumber (L, (lua_Number)d);
        } else lua_pushstring(L, v);
        return;
    }
    // String / Date / Json
    lua_pushstring(L, v);
}

// Fwd: para recursion en row_index.
std::string eval_internal(Engine* e, int id, const RowCtx& ctx, std::string* err_out);

int row_index(lua_State* L) {
    Engine* eng = engine_from_state(L);
    RowCtx* ctx = current_ctx(L);
    if (!ctx) { lua_pushnil(L); return 1; }

    using data_table::ColumnType;
    auto get_orig_type = [&](int c) -> ColumnType {
        if (ctx->types_orig && c < ctx->n_types_orig) return ctx->types_orig[c];
        return ColumnType::Auto;
    };

    if (lua_type(L, 2) == LUA_TSTRING) {
        const char* key = lua_tostring(L, 2);
        if (ctx->name_to_col) {
            auto it = ctx->name_to_col->find(key);
            if (it != ctx->name_to_col->end()) {
                int col = it->second;
                push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + col], get_orig_type(col));
                return 1;
            }
        }
        if (ctx->derived_name_to_idx && ctx->derived) {
            auto it = ctx->derived_name_to_idx->find(key);
            if (it != ctx->derived_name_to_idx->end()) {
                int didx = it->second;
                if (didx < 0 || didx >= (int)ctx->derived->size()) {
                    lua_pushnil(L); return 1;
                }
                // cycle check
                for (int v : eng->visiting_derived) {
                    if (v == didx) { lua_pushnil(L); return 1; }
                }
                const auto& d = (*ctx->derived)[didx];
                if (d.formula.empty()) {
                    // retipo puro
                    if (d.source_col < 0 || d.source_col >= ctx->orig_cols) {
                        lua_pushnil(L); return 1;
                    }
                    push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + d.source_col], d.type);
                } else if (d.lua_id < 0) {
                    lua_pushnil(L);
                } else {
                    eng->visiting_derived.push_back(didx);
                    std::string err;
                    std::string r = eval_internal(eng, d.lua_id, *ctx, &err);
                    eng->visiting_derived.pop_back();
                    push_typed(L, r.c_str(), d.type);
                }
                return 1;
            }
        }
        lua_pushnil(L);
        return 1;
    }
    if (lua_type(L, 2) == LUA_TNUMBER) {
        int idx = (int)lua_tointeger(L, 2);
        if (idx >= 1 && idx <= ctx->orig_cols) {
            int col = idx - 1;
            push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + col], get_orig_type(col));
            return 1;
        }
    }
    lua_pushnil(L);
    return 1;
}

// --- fn.* builtins ---
int b_upper(lua_State* L) {
    const char* s = luaL_checkstring(L, 1);
    std::string out(s);
    for (char& c : out) if (c >= 'a' && c <= 'z') c -= 32;
    lua_pushlstring(L, out.data(), out.size());
    return 1;
}
int b_lower(lua_State* L) {
    const char* s = luaL_checkstring(L, 1);
    std::string out(s);
    for (char& c : out) if (c >= 'A' && c <= 'Z') c += 32;
    lua_pushlstring(L, out.data(), out.size());
    return 1;
}
int b_length(lua_State* L) {
    if (lua_isnil(L, 1)) { lua_pushinteger(L, 0); return 1; }
    const char* s = luaL_checkstring(L, 1);
    lua_pushinteger(L, (lua_Integer)std::strlen(s));
    return 1;
}
int b_substring(lua_State* L) {
    const char* s = luaL_checkstring(L, 1);
    int start = (int)luaL_checkinteger(L, 2);
    int len   = (int)luaL_optinteger(L, 3, -1);
    int slen  = (int)std::strlen(s);
    if (start < 1) start = 1;
    if (start > slen) { lua_pushlstring(L, "", 0); return 1; }
    int from = start - 1;
    int take = (len < 0) ? slen - from : len;
    if (from + take > slen) take = slen - from;
    lua_pushlstring(L, s + from, take);
    return 1;
}
int b_contains(lua_State* L) {
    const char* h = luaL_checkstring(L, 1);
    const char* n = luaL_checkstring(L, 2);
    lua_pushboolean(L, std::strstr(h, n) != nullptr);
    return 1;
}
int b_starts_with(lua_State* L) {
    const char* h = luaL_checkstring(L, 1);
    const char* n = luaL_checkstring(L, 2);
    size_t ln = std::strlen(n);
    lua_pushboolean(L, std::strncmp(h, n, ln) == 0);
    return 1;
}
int b_ends_with(lua_State* L) {
    const char* h = luaL_checkstring(L, 1);
    const char* n = luaL_checkstring(L, 2);
    size_t lh = std::strlen(h), ln = std::strlen(n);
    lua_pushboolean(L, ln <= lh && std::strcmp(h + lh - ln, n) == 0);
    return 1;
}
int b_replace(lua_State* L) {
    const char* s    = luaL_checkstring(L, 1);
    const char* find = luaL_checkstring(L, 2);
    const char* repl = luaL_checkstring(L, 3);
    std::string out;
    size_t flen = std::strlen(find);
    if (flen == 0) { lua_pushstring(L, s); return 1; }
    for (const char* p = s; *p; ) {
        if (std::strncmp(p, find, flen) == 0) { out += repl; p += flen; }
        else                                  { out += *p++; }
    }
    lua_pushlstring(L, out.data(), out.size());
    return 1;
}
int b_trim(lua_State* L) {
    const char* s = luaL_checkstring(L, 1);
    while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') ++s;
    const char* e = s + std::strlen(s);
    while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\n' || e[-1] == '\r')) --e;
    lua_pushlstring(L, s, e - s);
    return 1;
}
int b_concat(lua_State* L) {
    int n = lua_gettop(L);
    std::string out;
    for (int i = 1; i <= n; ++i) {
        size_t sl = 0;
        const char* s = luaL_tolstring(L, i, &sl);
        out.append(s, sl);
        lua_pop(L, 1);
    }
    lua_pushlstring(L, out.data(), out.size());
    return 1;
}
int b_to_number(lua_State* L) {
    if (lua_isnumber(L, 1)) { lua_pushvalue(L, 1); return 1; }
    const char* s = luaL_checkstring(L, 1);
    char* end = nullptr;
    double v = std::strtod(s, &end);
    if (end == s) { lua_pushnil(L); return 1; }
    lua_pushnumber(L, v);
    return 1;
}
int b_to_string(lua_State* L) { luaL_tolstring(L, 1, nullptr); return 1; }
int b_to_bool(lua_State* L) {
    if (lua_isboolean(L, 1)) { lua_pushvalue(L, 1); return 1; }
    const char* s = luaL_optstring(L, 1, "");
    lua_pushboolean(L, std::strcmp(s, "true") == 0 || std::strcmp(s, "1") == 0);
    return 1;
}
int b_is_null(lua_State* L) { lua_pushboolean(L, lua_isnil(L, 1)); return 1; }
int b_is_empty(lua_State* L) {
    if (lua_isnil(L, 1)) { lua_pushboolean(L, 1); return 1; }
    const char* s = luaL_optstring(L, 1, "");
    lua_pushboolean(L, *s == 0);
    return 1;
}
int b_coalesce(lua_State* L) {
    int n = lua_gettop(L);
    for (int i = 1; i <= n; ++i) {
        if (!lua_isnil(L, i)) { lua_pushvalue(L, i); return 1; }
    }
    lua_pushnil(L);
    return 1;
}
int b_parse_date(lua_State* L) {
    const char* s = luaL_checkstring(L, 1);
    if (std::strlen(s) < 10) { lua_pushnil(L); return 1; }
    int y, m, d;
    if (std::sscanf(s, "%d-%d-%d", &y, &m, &d) != 3) { lua_pushnil(L); return 1; }
    lua_createtable(L, 0, 3);
    lua_pushinteger(L, y); lua_setfield(L, -2, "year");
    lua_pushinteger(L, m); lua_setfield(L, -2, "month");
    lua_pushinteger(L, d); lua_setfield(L, -2, "day");
    return 1;
}
int b_year(lua_State* L) {
    const char* s = luaL_checkstring(L, 1);
    int y; if (std::sscanf(s, "%d", &y) != 1) { lua_pushnil(L); return 1; }
    lua_pushinteger(L, y); return 1;
}
int b_month(lua_State* L) {
    const char* s = luaL_checkstring(L, 1);
    int y, m; if (std::sscanf(s, "%d-%d", &y, &m) != 2) { lua_pushnil(L); return 1; }
    lua_pushinteger(L, m); return 1;
}
int b_day(lua_State* L) {
    const char* s = luaL_checkstring(L, 1);
    int y, m, d; if (std::sscanf(s, "%d-%d-%d", &y, &m, &d) != 3) { lua_pushnil(L); return 1; }
    lua_pushinteger(L, d); return 1;
}

void apply_sandbox(lua_State* L) {
    const char* nuke[] = { "io", "require", "loadfile", "dofile", "load",
                           "package", "debug", nullptr };
    for (int i = 0; nuke[i]; ++i) {
        lua_pushnil(L);
        lua_setglobal(L, nuke[i]);
    }
    lua_getglobal(L, "os");
    if (lua_istable(L, -1)) {
        lua_createtable(L, 0, 4);
        const char* keep[] = {"date", "time", "difftime", "clock", nullptr};
        for (int i = 0; keep[i]; ++i) {
            lua_getfield(L, -2, keep[i]);
            lua_setfield(L, -2, keep[i]);
        }
        lua_setglobal(L, "os");
    }
    lua_pop(L, 1);
}

void register_builtins(lua_State* L) {
    lua_createtable(L, 0, 24);
    #define R(name, fn) lua_pushcfunction(L, fn); lua_setfield(L, -2, name);
    R("upper",       b_upper);
    R("lower",       b_lower);
    R("length",      b_length);
    R("substring",   b_substring);
    R("contains",    b_contains);
    R("starts_with", b_starts_with);
    R("ends_with",   b_ends_with);
    R("replace",     b_replace);
    R("trim",        b_trim);
    R("concat",      b_concat);
    R("to_number",   b_to_number);
    R("to_string",   b_to_string);
    R("to_bool",     b_to_bool);
    R("is_null",     b_is_null);
    R("is_empty",    b_is_empty);
    R("coalesce",    b_coalesce);
    R("parse_date",  b_parse_date);
    R("year",        b_year);
    R("month",       b_month);
    R("day",         b_day);
    #undef R
    lua_setglobal(L, "fn");
}

void install_row_metatable(lua_State* L) {
    luaL_newmetatable(L, "fn_row_meta");
    lua_pushcfunction(L, row_index);
    lua_setfield(L, -2, "__index");
    lua_pop(L, 1);
}

// ---------------------------------------------------------------------------
// Preprocesador: [col] -> row["col"] respetando strings y comentarios.
// Auto-prepend `return` si la formula es expresion suelta.
// ---------------------------------------------------------------------------
bool ident_start(unsigned char c) {
    return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c >= 0x80;
}
// Para nombres de cols dentro de [name]: permite espacios para "col with space"
// y '.' para futuro `alias.col` post-join.
bool ident_cont(unsigned char c) {
    return ident_start(c) || (c >= '0' && c <= '9') || c == ' ' || c == '.';
}
// Para boundary de keywords Lua: NO permite espacio.
bool word_char(unsigned char c) {
    return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
           (c >= '0' && c <= '9') || c == '_' || c >= 0x80;
}

bool kw_at(const std::string& s, size_t i, const char* kw) {
    size_t k = std::strlen(kw);
    if (i + k > s.size()) return false;
    if (s.compare(i, k, kw) != 0) return false;
    if (i + k == s.size()) return true;
    unsigned char nc = (unsigned char)s[i + k];
    return !word_char(nc);
}

bool needs_auto_return(const std::string& body) {
    size_t i = 0;
    while (i < body.size()) {
        char c = body[i];
        if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { ++i; continue; }
        // skip short comment
        if (c == '-' && i + 1 < body.size() && body[i+1] == '-') {
            // long comment?
            if (i + 3 < body.size() && body[i+2] == '[' && body[i+3] == '[') {
                size_t j = i + 4;
                while (j + 1 < body.size() && !(body[j] == ']' && body[j+1] == ']')) ++j;
                i = (j + 1 < body.size()) ? j + 2 : body.size();
                continue;
            }
            while (i < body.size() && body[i] != '\n') ++i;
            continue;
        }
        break;
    }
    if (i >= body.size()) return false;
    const char* kws[] = {"return","if","for","while","do","local","repeat","function", nullptr};
    for (int k = 0; kws[k]; ++k) if (kw_at(body, i, kws[k])) return false;
    return true;
}

std::string brackets_pass(const std::string& src) {
    std::string out;
    out.reserve(src.size() + 16);
    size_t i = 0;
    while (i < src.size()) {
        char c = src[i];
        // strings
        if (c == '"' || c == '\'') {
            char q = c;
            out += c; ++i;
            while (i < src.size()) {
                char d = src[i];
                out += d; ++i;
                if (d == '\\' && i < src.size()) { out += src[i++]; continue; }
                if (d == q) break;
                if (d == '\n') break;
            }
            continue;
        }
        // comentario corto / largo
        if (c == '-' && i + 1 < src.size() && src[i+1] == '-') {
            // long: --[[ ... ]]
            if (i + 3 < src.size() && src[i+2] == '[' && src[i+3] == '[') {
                out.append(src, i, 4); i += 4;
                while (i + 1 < src.size() && !(src[i] == ']' && src[i+1] == ']')) {
                    out += src[i++];
                }
                if (i + 1 < src.size()) { out += src[i++]; out += src[i++]; }
                continue;
            }
            // short
            while (i < src.size() && src[i] != '\n') { out += src[i++]; }
            continue;
        }
        // long string [[ ... ]]
        if (c == '[' && i + 1 < src.size() && src[i+1] == '[') {
            out.append(src, i, 2); i += 2;
            while (i + 1 < src.size() && !(src[i] == ']' && src[i+1] == ']')) {
                out += src[i++];
            }
            if (i + 1 < src.size()) { out += src[i++]; out += src[i++]; }
            continue;
        }
        // bracket col-ref [name]
        if (c == '[') {
            // peek if next is valid ident_start
            if (i + 1 < src.size() && ident_start((unsigned char)src[i+1])) {
                size_t j = i + 1;
                while (j < src.size() && src[j] != ']' && src[j] != '\n') {
                    if (!ident_cont((unsigned char)src[j])) { j = std::string::npos; break; }
                    ++j;
                }
                if (j != std::string::npos && j < src.size() && src[j] == ']') {
                    std::string name(src, i + 1, j - i - 1);
                    // trim trailing space
                    while (!name.empty() && name.back() == ' ') name.pop_back();
                    out += "row[\"";
                    out += name;
                    out += "\"]";
                    i = j + 1;
                    continue;
                }
            }
        }
        out += c;
        ++i;
    }
    return out;
}

} // anon

std::string preprocess(const std::string& body) {
    std::string pre = brackets_pass(body);
    if (needs_auto_return(pre)) return "return " + pre;
    return pre;
}

namespace {

std::string eval_internal(Engine* e, int id, const RowCtx& ctx, std::string* err_out) {
    if (!e || !e->L || id < 0) {
        if (err_out) *err_out = "invalid handle";
        return "";
    }
    lua_State* L = e->L;
    e->ctx_stack.push_back(const_cast<RowCtx*>(&ctx));
    lua_rawgeti(L, LUA_REGISTRYINDEX, id);
    lua_newuserdata(L, 1);
    luaL_setmetatable(L, "fn_row_meta");
    int rc = lua_pcall(L, 1, 1, 0);
    e->ctx_stack.pop_back();
    if (rc != LUA_OK) {
        if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "runtime error";
        lua_pop(L, 1);
        return "";
    }
    std::string out;
    if (lua_isnil(L, -1)) out = "";
    else {
        size_t n = 0;
        const char* s = luaL_tolstring(L, -1, &n);
        out.assign(s, n);
        lua_pop(L, 1);
    }
    lua_pop(L, 1);
    return out;
}

} // anon

Engine* get() {
    if (g_engine) return g_engine;
    g_engine = new Engine();
    g_engine->L = luaL_newstate();
    luaL_openlibs(g_engine->L);
    *static_cast<Engine**>(lua_getextraspace(g_engine->L)) = g_engine;
    apply_sandbox(g_engine->L);
    register_builtins(g_engine->L);
    install_row_metatable(g_engine->L);
    return g_engine;
}

void shutdown() {
    if (!g_engine) return;
    lua_close(g_engine->L);
    delete g_engine;
    g_engine = nullptr;
}

int compile(Engine* e, const std::string& body, std::string* err_out) {
    if (!e || !e->L) { if (err_out) *err_out = "engine null"; return -1; }
    lua_State* L = e->L;
    std::string final_body = preprocess(body);
    std::string wrapped = "return function(row)\n" + final_body + "\nend";
    if (luaL_loadbufferx(L, wrapped.data(), wrapped.size(), "formula", "t") != LUA_OK) {
        if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "parse error";
        lua_pop(L, 1);
        return -1;
    }
    if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
        if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "compile error";
        lua_pop(L, 1);
        return -1;
    }
    if (!lua_isfunction(L, -1)) {
        if (err_out) *err_out = "formula did not produce a function";
        lua_pop(L, 1);
        return -1;
    }
    int ref = luaL_ref(L, LUA_REGISTRYINDEX);
    return ref;
}

void release(Engine* e, int id) {
    if (!e || !e->L || id < 0) return;
    luaL_unref(e->L, LUA_REGISTRYINDEX, id);
}

std::string eval(Engine* e, int id, const RowCtx& ctx, std::string* err_out) {
    return eval_internal(e, id, ctx, err_out);
}

lua_State* raw_state() {
    Engine* e = get();
    return e ? e->L : nullptr;
}

} // namespace lua_engine
