#include "entity_ops.h"

#include "../../../../cpp/vendor/sqlite3/sqlite3.h"

#include <chrono>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cctype>

namespace ge {

// ----------------------------------------------------------------------------
// FNV1a-64 — debe coincidir con graph_sources.cpp
// ----------------------------------------------------------------------------
static uint64_t fnv1a64(const char* s) {
    uint64_t h = 1469598103934665603ULL;
    for (; s && *s; ++s) {
        h ^= (uint8_t)*s;
        h *= 1099511628211ULL;
    }
    return h;
}

// ----------------------------------------------------------------------------
// Heuristicas — sin <regex> para evitar peso. Inspeccion lineal del string.
// ----------------------------------------------------------------------------

static bool is_email(const char* s) {
    if (!s || !*s) return false;
    const char* at = std::strchr(s, '@');
    if (!at || at == s) return false;
    if (std::strchr(at + 1, '@')) return false;          // dos @
    const char* dot = std::strchr(at + 1, '.');
    if (!dot || dot[1] == 0) return false;
    if (std::strchr(s, ' ')) return false;
    return true;
}

static bool is_ipv4(const char* s) {
    if (!s || !*s) return false;
    int parts = 0, digits = 0, n = 0;
    for (const char* p = s; ; ++p) {
        if (*p >= '0' && *p <= '9') {
            n = n * 10 + (*p - '0');
            if (++digits > 3 || n > 255) return false;
        } else if (*p == '.' || *p == 0) {
            if (digits == 0) return false;
            ++parts; digits = 0; n = 0;
            if (*p == 0) break;
        } else {
            return false;
        }
    }
    return parts == 4;
}

static bool is_url(const char* s) {
    if (!s) return false;
    return std::strncmp(s, "http://", 7) == 0 || std::strncmp(s, "https://", 8) == 0;
}

static bool is_domain(const char* s) {
    if (!s || !*s) return false;
    if (std::strchr(s, ' ') || std::strchr(s, '@') || std::strchr(s, '/')) return false;
    const char* dot = std::strchr(s, '.');
    if (!dot || dot == s || dot[1] == 0) return false;
    // El TLD debe ser al menos 2 caracteres alfabeticos
    int tld = 0;
    for (const char* p = std::strrchr(s, '.') + 1; *p; ++p) {
        if (!std::isalpha((unsigned char)*p)) return false;
        ++tld;
    }
    return tld >= 2;
}

static bool is_phone(const char* s) {
    if (!s || !*s) return false;
    int digits = 0;
    for (const char* p = s; *p; ++p) {
        if (*p >= '0' && *p <= '9') ++digits;
        else if (*p == '+' || *p == ' ' || *p == '-' || *p == '(' || *p == ')' || *p == '.') {}
        else return false;
    }
    return digits >= 7 && digits <= 15;
}

DetectedType detect_type(const char* text) {
    if (!text || !*text) return DT_TEXT;
    if (is_email(text))  return DT_EMAIL;
    if (is_ipv4(text))   return DT_IP_ADDRESS;
    if (is_url(text))    return DT_URL;
    if (is_phone(text))  return DT_PHONE;  // antes que domain (numeros con puntos)
    if (is_domain(text)) return DT_DOMAIN;
    return DT_TEXT;
}

const char* detected_type_name(DetectedType dt) {
    switch (dt) {
        case DT_EMAIL:      return "email";
        case DT_IP_ADDRESS: return "ip_address";
        case DT_URL:        return "url";
        case DT_DOMAIN:     return "domain";
        case DT_PHONE:      return "phone";
        case DT_TEXT:
        default:            return "text";
    }
}

// ----------------------------------------------------------------------------
// SQLite helpers
// ----------------------------------------------------------------------------

static bool exec_one(sqlite3* db, const char* sql,
                     const char** params, int n_params)
{
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) return false;
    for (int i = 0; i < n_params; ++i) {
        sqlite3_bind_text(st, i + 1, params[i] ? params[i] : "", -1, SQLITE_TRANSIENT);
    }
    int rc = sqlite3_step(st);
    sqlite3_finalize(st);
    return rc == SQLITE_DONE;
}

static std::string now_iso() {
    using namespace std::chrono;
    auto t = system_clock::to_time_t(system_clock::now());
    char buf[32];
    std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", std::gmtime(&t));
    return std::string(buf) + "Z";
}

static long long now_ms() {
    using namespace std::chrono;
    return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}

// ----------------------------------------------------------------------------
// CRUD
// ----------------------------------------------------------------------------

bool entity_insert(const char* db_path, const char* name, const char* type_ref,
                   char* out_id, size_t out_id_n)
{
    if (!db_path || !name || !*name || !out_id || out_id_n < 32) return false;

    std::string tref;
    if (type_ref && *type_ref) {
        tref = type_ref;
    } else {
        tref = detected_type_name(detect_type(name));
    }

    std::snprintf(out_id, out_id_n, "%s_%lld", tref.c_str(), now_ms());
    std::string ts = now_iso();

    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    const char* sql =
        "INSERT INTO entities (id, name, type_ref, source, created_at, updated_at) "
        "VALUES (?, ?, ?, 'manual', ?, ?)";
    const char* params[5] = { out_id, name, tref.c_str(), ts.c_str(), ts.c_str() };
    bool ok = exec_one(db, sql, params, 5);
    sqlite3_close(db);
    return ok;
}

bool entity_delete(const char* db_path, const char* id) {
    if (!db_path || !id) return false;
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    const char* p_rel[2] = { id, id };
    const char* p_ent[1] = { id };
    exec_one(db, "DELETE FROM relations WHERE from_entity = ? OR to_entity = ?", p_rel, 2);
    bool ok = exec_one(db, "DELETE FROM entities WHERE id = ?", p_ent, 1);
    sqlite3_close(db);
    return ok;
}

bool entity_update_type(const char* db_path, const char* id, const char* new_type) {
    if (!db_path || !id || !new_type) return false;
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    std::string ts = now_iso();
    const char* p[3] = { new_type, ts.c_str(), id };
    bool ok = exec_one(db, "UPDATE entities SET type_ref = ?, updated_at = ? WHERE id = ?", p, 3);
    sqlite3_close(db);
    return ok;
}

bool entity_clear_group_id(const char* db_path, const char* entity_id) {
    if (!db_path || !entity_id || !*entity_id) return false;
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }

    // Detectar si la columna `group_id` existe en `entities`. En BDs viejas
    // (pre-issue 0035a) la columna no esta — el clear es no-op coherente:
    // la entidad no puede pertenecer a un grupo si la columna no existe.
    bool has_group_id = false;
    {
        sqlite3_stmt* pst = nullptr;
        if (sqlite3_prepare_v2(db, "PRAGMA table_info(entities)", -1, &pst, nullptr) == SQLITE_OK) {
            while (sqlite3_step(pst) == SQLITE_ROW) {
                const unsigned char* col = sqlite3_column_text(pst, 1);
                if (col && std::strcmp((const char*)col, "group_id") == 0) {
                    has_group_id = true;
                    break;
                }
            }
            sqlite3_finalize(pst);
        }
    }

    // Verificar que la entidad existe (independiente del path con/sin
    // columna). Si no existe, devolvemos false (mismo contrato que
    // entity_update_type).
    {
        sqlite3_stmt* est = nullptr;
        if (sqlite3_prepare_v2(db, "SELECT 1 FROM entities WHERE id = ?", -1,
                                &est, nullptr) != SQLITE_OK) {
            sqlite3_close(db);
            return false;
        }
        sqlite3_bind_text(est, 1, entity_id, -1, SQLITE_TRANSIENT);
        bool exists = (sqlite3_step(est) == SQLITE_ROW);
        sqlite3_finalize(est);
        if (!exists) {
            sqlite3_close(db);
            return false;
        }
    }

    if (!has_group_id) {
        // No-op valido: la entidad existe pero no puede tener group_id.
        sqlite3_close(db);
        return true;
    }

    std::string ts = now_iso();
    const char* p[2] = { ts.c_str(), entity_id };
    bool ok = exec_one(db,
        "UPDATE entities SET group_id = NULL, updated_at = ? WHERE id = ?",
        p, 2);
    sqlite3_close(db);
    return ok;
}

bool entity_duplicate(const char* db_path, const char* id,
                      char* out_id, size_t out_id_n)
{
    if (!db_path || !id || !out_id || out_id_n < 32) return false;
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    sqlite3_stmt* st = nullptr;
    const char* sel = "SELECT name, type_ref, description, domain, tags, source, metadata, notes "
                      "FROM entities WHERE id = ?";
    if (sqlite3_prepare_v2(db, sel, -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    sqlite3_bind_text(st, 1, id, -1, SQLITE_TRANSIENT);
    if (sqlite3_step(st) != SQLITE_ROW) {
        sqlite3_finalize(st);
        sqlite3_close(db);
        return false;
    }
    auto col = [&](int i) {
        const unsigned char* p = sqlite3_column_text(st, i);
        return std::string(p ? (const char*)p : "");
    };
    std::string name  = col(0) + " (copia)";
    std::string tref  = col(1);
    std::string desc  = col(2);
    std::string dom   = col(3);
    std::string tags  = col(4);
    std::string src   = col(5);
    std::string meta  = col(6);
    std::string notes = col(7);
    sqlite3_finalize(st);

    std::snprintf(out_id, out_id_n, "%s_%lld", tref.c_str(), now_ms());
    std::string ts = now_iso();

    const char* ins =
        "INSERT INTO entities (id, name, type_ref, description, domain, tags, source, "
        "metadata, notes, created_at, updated_at) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
    sqlite3_stmt* ist = nullptr;
    if (sqlite3_prepare_v2(db, ins, -1, &ist, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    sqlite3_bind_text(ist,  1, out_id,       -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist,  2, name.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist,  3, tref.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist,  4, desc.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist,  5, dom.c_str(),  -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist,  6, tags.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist,  7, src.c_str(),  -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist,  8, meta.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist,  9, notes.c_str(),-1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist, 10, ts.c_str(),   -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(ist, 11, ts.c_str(),   -1, SQLITE_TRANSIENT);
    bool ok = sqlite3_step(ist) == SQLITE_DONE;
    sqlite3_finalize(ist);
    sqlite3_close(db);
    return ok;
}

bool relation_insert(const char* db_path, const char* from_id, const char* to_id,
                     const char* name)
{
    if (!db_path || !from_id || !to_id) return false;
    const char* rel = (name && *name) ? name : k_default_relation_name;
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    char id[64];
    std::snprintf(id, sizeof(id), "rel_%lld", now_ms());
    std::string ts = now_iso();
    const char* sql =
        "INSERT INTO relations (id, name, from_entity, to_entity, created_at, updated_at) "
        "VALUES (?, ?, ?, ?, ?, ?)";
    const char* params[6] = { id, rel, from_id, to_id, ts.c_str(), ts.c_str() };
    bool ok = exec_one(db, sql, params, 6);
    sqlite3_close(db);
    return ok;
}

// ----------------------------------------------------------------------------
// Notes (markdown)
// ----------------------------------------------------------------------------

bool entity_get_notes(const char* db_path, const char* id, std::string* out) {
    if (!db_path || !id || !out) return false;
    out->clear();
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, "SELECT notes FROM entities WHERE id = ?", -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    sqlite3_bind_text(st, 1, id, -1, SQLITE_TRANSIENT);
    bool ok = false;
    if (sqlite3_step(st) == SQLITE_ROW) {
        const unsigned char* p = sqlite3_column_text(st, 0);
        if (p) *out = (const char*)p;
        ok = true;
    }
    sqlite3_finalize(st);
    sqlite3_close(db);
    return ok;
}

bool entity_set_notes(const char* db_path, const char* id, const char* notes) {
    if (!db_path || !id) return false;
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    std::string ts = now_iso();
    const char* p[3] = { notes ? notes : "", ts.c_str(), id };
    bool ok = exec_one(db, "UPDATE entities SET notes = ?, updated_at = ? WHERE id = ?", p, 3);
    sqlite3_close(db);
    return ok;
}

// ----------------------------------------------------------------------------
// JSON helpers — parser + writer minimal para objetos planos y arrays de
// strings. NO es un parser JSON general (no soporta objetos anidados, ni
// arrays anidados, ni escapes Unicode \uXXXX). Suficiente para tags JSON
// y metadata flat del Inspector.
// ----------------------------------------------------------------------------

namespace {

void skip_ws(const char*& p, const char* end) {
    while (p < end) {
        char c = *p;
        if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { ++p; continue; }
        break;
    }
}

// Parse un string JSON: comillas + contenido con escapes basicos. Devuelve
// el contenido decodificado en `out`. Avanza `p`. Retorna false si malformado.
bool parse_json_string(const char*& p, const char* end, std::string* out) {
    if (p >= end || *p != '"') return false;
    ++p;
    out->clear();
    while (p < end) {
        char c = *p++;
        if (c == '"') return true;
        if (c == '\\' && p < end) {
            char e = *p++;
            switch (e) {
                case 'n': out->push_back('\n'); break;
                case 't': out->push_back('\t'); break;
                case 'r': out->push_back('\r'); break;
                case '"':  out->push_back('"');  break;
                case '\\': out->push_back('\\'); break;
                case '/':  out->push_back('/');  break;
                case 'b':  out->push_back('\b'); break;
                case 'f':  out->push_back('\f'); break;
                case 'u':
                    // Skip 4 hex chars; emit '?' (no soportamos Unicode escapes).
                    for (int i = 0; i < 4 && p < end; ++i) ++p;
                    out->push_back('?');
                    break;
                default:
                    out->push_back(e);
                    break;
            }
        } else {
            out->push_back(c);
        }
    }
    return false;
}

// Parse un literal: numero, true, false, null. Avanza `p`. Devuelve raw
// (lo que aparece literalmente en el JSON, sin comillas).
bool parse_json_literal(const char*& p, const char* end, std::string* out) {
    out->clear();
    while (p < end) {
        char c = *p;
        if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' ||
            c == '\n' || c == '\r')
            break;
        out->push_back(c);
        ++p;
    }
    return !out->empty();
}

void escape_json_string(const std::string& s, std::string* out) {
    out->push_back('"');
    for (char c : s) {
        switch (c) {
            case '"':  *out += "\\\""; break;
            case '\\': *out += "\\\\"; break;
            case '\n': *out += "\\n";  break;
            case '\r': *out += "\\r";  break;
            case '\t': *out += "\\t";  break;
            default:
                if ((unsigned char)c < 0x20) {
                    char buf[8];
                    std::snprintf(buf, sizeof(buf), "\\u%04x", (unsigned)(unsigned char)c);
                    *out += buf;
                } else {
                    out->push_back(c);
                }
        }
    }
    out->push_back('"');
}

// Parsea `["a","b",...]` (array plano de strings). Devuelve true si OK.
// Tolerante: si no es un array, devuelve false con `out` vacio.
bool parse_string_array(const std::string& src, std::vector<std::string>* out) {
    out->clear();
    if (src.empty()) return true;
    const char* p = src.c_str();
    const char* end = p + src.size();
    skip_ws(p, end);
    if (p >= end || *p != '[') return false;
    ++p;
    skip_ws(p, end);
    if (p < end && *p == ']') return true;  // []
    while (p < end) {
        skip_ws(p, end);
        std::string s;
        if (*p == '"') {
            if (!parse_json_string(p, end, &s)) return false;
        } else {
            if (!parse_json_literal(p, end, &s)) return false;
        }
        out->push_back(s);
        skip_ws(p, end);
        if (p >= end) return false;
        if (*p == ',') { ++p; continue; }
        if (*p == ']') { ++p; return true; }
        return false;
    }
    return false;
}

// Parsea `{"k":"v","k2":42,"k3":true}` (objeto plano). Devuelve los pares
// en orden, con `is_string=true` si el valor venia entre comillas.
bool parse_flat_object(const std::string& src, std::vector<MetadataField>* out) {
    out->clear();
    if (src.empty()) return true;
    const char* p = src.c_str();
    const char* end = p + src.size();
    skip_ws(p, end);
    if (p >= end || *p != '{') return false;
    ++p;
    skip_ws(p, end);
    if (p < end && *p == '}') return true;  // {}
    while (p < end) {
        skip_ws(p, end);
        std::string key;
        if (*p != '"' || !parse_json_string(p, end, &key)) return false;
        skip_ws(p, end);
        if (p >= end || *p != ':') return false;
        ++p;
        skip_ws(p, end);
        if (p >= end) return false;
        MetadataField f;
        f.key = std::move(key);
        if (*p == '"') {
            f.is_string = true;
            if (!parse_json_string(p, end, &f.value_str)) return false;
        } else if (*p == '{' || *p == '[') {
            // Objeto/array anidado: consumir como literal hasta el cierre balanceado.
            int depth = 0;
            while (p < end) {
                char c = *p++;
                f.value_str.push_back(c);
                if (c == '{' || c == '[') ++depth;
                else if (c == '}' || c == ']') {
                    --depth;
                    if (depth == 0) break;
                }
            }
            f.is_string = false;
        } else {
            f.is_string = false;
            if (!parse_json_literal(p, end, &f.value_str)) return false;
        }
        out->push_back(std::move(f));
        skip_ws(p, end);
        if (p >= end) return false;
        if (*p == ',') { ++p; continue; }
        if (*p == '}') { ++p; return true; }
        return false;
    }
    return false;
}

void build_string_array(const std::vector<std::string>& xs, std::string* out) {
    out->push_back('[');
    for (size_t i = 0; i < xs.size(); ++i) {
        if (i) out->push_back(',');
        escape_json_string(xs[i], out);
    }
    out->push_back(']');
}

void build_flat_object(const std::vector<MetadataField>& fs, std::string* out) {
    out->push_back('{');
    bool first = true;
    for (const auto& f : fs) {
        if (f.key.empty()) continue;
        // Saltar fields con valor vacio para no ensuciar el JSON con
        // claves "" / null. La excepcion es bool/string vacio explicito —
        // pero en v1 las claves vacias se omiten.
        if (f.is_string && f.value_str.empty()) continue;
        if (!f.is_string && f.value_str.empty()) continue;
        if (!first) out->push_back(',');
        first = false;
        escape_json_string(f.key, out);
        out->push_back(':');
        if (f.is_string) {
            escape_json_string(f.value_str, out);
        } else {
            // Validar literal minimamente: si no es number/bool/null/array/object,
            // re-emitir como string para no producir JSON invalido.
            const std::string& v = f.value_str;
            char first_c = v[0];
            bool ok_literal =
                (first_c == '-' || (first_c >= '0' && first_c <= '9')) ||
                v == "true" || v == "false" || v == "null" ||
                first_c == '{' || first_c == '[';
            if (ok_literal) {
                *out += v;
            } else {
                escape_json_string(v, out);
            }
        }
    }
    out->push_back('}');
}

} // namespace

bool entity_load_full(const char* db_path, const char* id, EntityRecord* out) {
    if (!db_path || !id || !out) return false;
    *out = EntityRecord{};
    out->id = id;
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    sqlite3_stmt* st = nullptr;
    const char* sql =
        "SELECT name, type_ref, description, status, tags, metadata "
        "FROM entities WHERE id = ?";
    if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    sqlite3_bind_text(st, 1, id, -1, SQLITE_TRANSIENT);
    bool ok = false;
    if (sqlite3_step(st) == SQLITE_ROW) {
        auto col = [&](int i) {
            const unsigned char* p = sqlite3_column_text(st, i);
            return std::string(p ? (const char*)p : "");
        };
        out->name        = col(0);
        out->type_ref    = col(1);
        out->description = col(2);
        out->status      = col(3);
        std::string tags_json = col(4);
        std::string meta_json = col(5);
        parse_string_array(tags_json, &out->tags);
        parse_flat_object(meta_json, &out->metadata);
        ok = true;
    }
    sqlite3_finalize(st);
    sqlite3_close(db);
    return ok;
}

bool entity_update(const char* db_path, const EntityRecord& rec) {
    if (!db_path || rec.id.empty()) return false;
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    std::string tags_json;
    std::string meta_json;
    build_string_array(rec.tags, &tags_json);
    build_flat_object(rec.metadata, &meta_json);
    std::string ts = now_iso();
    std::string status = rec.status.empty() ? std::string("active") : rec.status;

    const char* sql =
        "UPDATE entities SET name = ?, type_ref = ?, description = ?, "
        "status = ?, tags = ?, metadata = ?, updated_at = ? WHERE id = ?";
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    sqlite3_bind_text(st, 1, rec.name.c_str(),        -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 2, rec.type_ref.c_str(),    -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 3, rec.description.c_str(), -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 4, status.c_str(),          -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 5, tags_json.c_str(),       -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 6, meta_json.c_str(),       -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 7, ts.c_str(),              -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(st, 8, rec.id.c_str(),          -1, SQLITE_TRANSIENT);
    bool ok = sqlite3_step(st) == SQLITE_DONE;
    sqlite3_finalize(st);
    sqlite3_close(db);
    return ok;
}

bool entity_list_distinct_tags(const char* db_path,
                                std::vector<std::string>* out)
{
    if (!db_path || !out) return false;
    out->clear();
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    // json_each esta disponible (SQLITE_ENABLE_JSON1 — ver CMakeLists del
    // amalgamation). Solo strings como tag (kind = 'text').
    const char* sql =
        "SELECT DISTINCT json_each.value FROM entities, json_each(entities.tags) "
        "WHERE json_each.type = 'text' ORDER BY json_each.value";
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    while (sqlite3_step(st) == SQLITE_ROW) {
        const unsigned char* p = sqlite3_column_text(st, 0);
        if (p && *p) out->emplace_back((const char*)p);
    }
    sqlite3_finalize(st);
    sqlite3_close(db);
    return true;
}

// ----------------------------------------------------------------------------
// FTS5 search (issue 0009)
// ----------------------------------------------------------------------------

namespace {

// Sanitiza la query del usuario para FTS5: drop de chars que rompen el
// parser, tokeniza por whitespace y envuelve cada token en comillas dobles
// con prefijo `*`. Asi `"john doe"` -> `"john"* "doe"*`. La presencia de
// varios tokens equivale a AND implicito en FTS5.
std::string fts5_sanitize(const char* q) {
    std::string out;
    if (!q) return out;
    const char* p = q;
    while (*p) {
        while (*p && std::isspace((unsigned char)*p)) ++p;
        if (!*p) break;
        std::string tok;
        while (*p && !std::isspace((unsigned char)*p)) {
            char c = *p++;
            // Drop chars con significado en FTS5.
            if (c == '"' || c == '*' || c == '(' || c == ')' || c == ':' ||
                c == '^' || c == '\'' || c == '!' || c == '+') continue;
            tok += c;
        }
        if (tok.empty()) continue;
        if (!out.empty()) out += ' ';
        out += '"';
        out += tok;
        out += "\"*";
    }
    return out;
}

} // namespace

bool entity_search_fts(const char* db_path, const char* query, int limit,
                       std::vector<EntityHit>* out)
{
    if (!db_path || !out) return false;
    out->clear();
    if (limit < 1) limit = 1;
    if (limit > 200) limit = 200;

    std::string match = fts5_sanitize(query);
    if (match.empty()) return true;

    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }

    // entities_fts.id y entities.id colisionan en JOIN — calificamos siempre.
    const char* sql =
        "SELECT e.id, e.name, e.type_ref, bm25(entities_fts) AS r "
        "FROM entities_fts JOIN entities AS e ON e.rowid = entities_fts.rowid "
        "WHERE entities_fts MATCH ?1 ORDER BY r ASC LIMIT ?2";
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    sqlite3_bind_text(st, 1, match.c_str(), (int)match.size(), SQLITE_TRANSIENT);
    sqlite3_bind_int (st, 2, limit);

    while (sqlite3_step(st) == SQLITE_ROW) {
        EntityHit h;
        const unsigned char* id   = sqlite3_column_text(st, 0);
        const unsigned char* name = sqlite3_column_text(st, 1);
        const unsigned char* tref = sqlite3_column_text(st, 2);
        h.id       = id   ? (const char*)id   : "";
        h.name     = name ? (const char*)name : "";
        h.type_ref = tref ? (const char*)tref : "";
        h.rank     = sqlite3_column_double(st, 3);
        out->push_back(std::move(h));
    }
    sqlite3_finalize(st);
    sqlite3_close(db);
    return true;
}

bool entity_list_by_tags(const char* db_path,
                          const std::vector<std::string>& tags,
                          std::vector<std::string>* out_ids)
{
    if (!db_path || !out_ids) return false;
    out_ids->clear();
    if (tags.empty()) return true;

    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }

    // Una clausula EXISTS por tag, todas en AND.
    std::string sql = "SELECT id FROM entities WHERE ";
    for (size_t i = 0; i < tags.size(); ++i) {
        if (i) sql += " AND ";
        sql += "EXISTS(SELECT 1 FROM json_each(entities.tags) WHERE json_each.value = ?)";
    }
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, sql.c_str(), -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    for (size_t i = 0; i < tags.size(); ++i) {
        sqlite3_bind_text(st, (int)(i + 1), tags[i].c_str(),
                          (int)tags[i].size(), SQLITE_TRANSIENT);
    }
    while (sqlite3_step(st) == SQLITE_ROW) {
        const unsigned char* p = sqlite3_column_text(st, 0);
        if (p && *p) out_ids->emplace_back((const char*)p);
    }
    sqlite3_finalize(st);
    sqlite3_close(db);
    return true;
}

bool entity_list_rows(const char* db_path,
                       std::vector<EntityRowSnapshot>* out)
{
    if (!db_path || !out) return false;
    out->clear();
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    // Detecta si existe la columna `group_id` (issue 0035a). En BDs viejas
    // sin la columna, el campo queda vacio y nada cambia.
    bool has_group_id = false;
    {
        sqlite3_stmt* pst = nullptr;
        if (sqlite3_prepare_v2(db, "PRAGMA table_info(entities)", -1, &pst, nullptr) == SQLITE_OK) {
            while (sqlite3_step(pst) == SQLITE_ROW) {
                const unsigned char* name = sqlite3_column_text(pst, 1);
                if (name && std::strcmp((const char*)name, "group_id") == 0) {
                    has_group_id = true;
                    break;
                }
            }
            sqlite3_finalize(pst);
        }
    }
    const char* sql_with =
        "SELECT id, COALESCE(name,''), COALESCE(type_ref,''), "
        "       COALESCE(status,''), COALESCE(updated_at,''), "
        "       COALESCE(group_id,'') "
        "FROM entities ORDER BY type_ref, name";
    const char* sql_without =
        "SELECT id, COALESCE(name,''), COALESCE(type_ref,''), "
        "       COALESCE(status,''), COALESCE(updated_at,'') "
        "FROM entities ORDER BY type_ref, name";
    const char* sql = has_group_id ? sql_with : sql_without;
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    while (sqlite3_step(st) == SQLITE_ROW) {
        EntityRowSnapshot r;
        const unsigned char* a0 = sqlite3_column_text(st, 0);
        const unsigned char* a1 = sqlite3_column_text(st, 1);
        const unsigned char* a2 = sqlite3_column_text(st, 2);
        const unsigned char* a3 = sqlite3_column_text(st, 3);
        const unsigned char* a4 = sqlite3_column_text(st, 4);
        r.id         = a0 ? (const char*)a0 : "";
        r.name       = a1 ? (const char*)a1 : "";
        r.type_ref   = a2 ? (const char*)a2 : "";
        r.status     = a3 ? (const char*)a3 : "";
        r.updated_at = a4 ? (const char*)a4 : "";
        if (has_group_id) {
            const unsigned char* a5 = sqlite3_column_text(st, 5);
            r.group_id = a5 ? (const char*)a5 : "";
        }
        out->push_back(std::move(r));
    }
    sqlite3_finalize(st);
    sqlite3_close(db);
    return true;
}

// ----------------------------------------------------------------------------
// Index user_data -> sql id
// ----------------------------------------------------------------------------

bool entity_index_build(const char* db_path, EntityIndex* idx) {
    if (!db_path || !idx) return false;
    idx->by_hash.clear();
    sqlite3* db = nullptr;
    if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
        if (db) sqlite3_close(db);
        return false;
    }
    sqlite3_stmt* st = nullptr;
    if (sqlite3_prepare_v2(db, "SELECT id FROM entities", -1, &st, nullptr) != SQLITE_OK) {
        sqlite3_close(db);
        return false;
    }
    while (sqlite3_step(st) == SQLITE_ROW) {
        const unsigned char* p = sqlite3_column_text(st, 0);
        if (!p) continue;
        const char* id = (const char*)p;
        idx->by_hash.emplace(fnv1a64(id), std::string(id));
    }
    sqlite3_finalize(st);
    sqlite3_close(db);
    return true;
}

const char* entity_index_lookup(const EntityIndex& idx, uint64_t user_data) {
    auto it = idx.by_hash.find(user_data);
    return (it == idx.by_hash.end()) ? nullptr : it->second.c_str();
}

} // namespace ge
