/**
 * @file addons/rest.c
 * @brief Rest addon.
 */

#include "../private_api.h"

#ifdef FLECS_REST

static ECS_TAG_DECLARE(EcsRestPlecs);

typedef struct {
    ecs_world_t *world;
    ecs_http_server_t *srv;
    int32_t rc;
} ecs_rest_ctx_t;

/* Global statistics */
int64_t ecs_rest_request_count = 0;
int64_t ecs_rest_entity_count = 0;
int64_t ecs_rest_entity_error_count = 0;
int64_t ecs_rest_query_count = 0;
int64_t ecs_rest_query_error_count = 0;
int64_t ecs_rest_query_name_count = 0;
int64_t ecs_rest_query_name_error_count = 0;
int64_t ecs_rest_query_name_from_cache_count = 0;
int64_t ecs_rest_enable_count = 0;
int64_t ecs_rest_enable_error_count = 0;
int64_t ecs_rest_delete_count = 0;
int64_t ecs_rest_delete_error_count = 0;
int64_t ecs_rest_world_stats_count = 0;
int64_t ecs_rest_pipeline_stats_count = 0;
int64_t ecs_rest_stats_error_count = 0;

static ECS_COPY(EcsRest, dst, src, {
    ecs_rest_ctx_t *impl = src->impl;
    if (impl) {
        impl->rc ++;
    }

    ecs_os_strset(&dst->ipaddr, src->ipaddr);
    dst->port = src->port;
    dst->impl = impl;
})

static ECS_MOVE(EcsRest, dst, src, {
    *dst = *src;
    src->ipaddr = NULL;
    src->impl = NULL;
})

static ECS_DTOR(EcsRest, ptr, { 
    ecs_rest_ctx_t *impl = ptr->impl;
    if (impl) {
        impl->rc --;
        if (!impl->rc) {
            ecs_http_server_fini(impl->srv);
            ecs_os_free(impl);
        }
    }
    ecs_os_free(ptr->ipaddr);
})

static char *rest_last_err;
static ecs_os_api_log_t rest_prev_log;

static 
void flecs_rest_capture_log(
    int32_t level, 
    const char *file, 
    int32_t line, 
    const char *msg)
{
    (void)file; (void)line;

    if (level < 0) {
        if (rest_prev_log) {
            // Also log to previous log function
            ecs_log_enable_colors(true);
            rest_prev_log(level, file, line, msg);
            ecs_log_enable_colors(false);
        }
    }

    if (!rest_last_err && level < 0) {
        rest_last_err = ecs_os_strdup(msg);
    }
}

static
char* flecs_rest_get_captured_log(void) {
    char *result = rest_last_err;
    rest_last_err = NULL;
    return result;
}

static
void flecs_reply_verror(
    ecs_http_reply_t *reply,
    const char *fmt,
    va_list args)
{
    ecs_strbuf_appendlit(&reply->body, "{\"error\":\"");
    ecs_strbuf_vappend(&reply->body, fmt, args);
    ecs_strbuf_appendlit(&reply->body, "\"}");
}

static
void flecs_reply_error(
    ecs_http_reply_t *reply,
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);
    flecs_reply_verror(reply, fmt, args);
    va_end(args);
}

static
void flecs_rest_bool_param(
    const ecs_http_request_t *req,
    const char *name,
    bool *value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        if (!ecs_os_strcmp(value, "true")) {
            value_out[0] = true;
        } else {
            value_out[0] = false;
        }
    }
}

static
void flecs_rest_int_param(
    const ecs_http_request_t *req,
    const char *name,
    int32_t *value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        *value_out = atoi(value);
    }
}

static
void flecs_rest_string_param(
    const ecs_http_request_t *req,
    const char *name,
    char **value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        *value_out = ECS_CONST_CAST(char*, value);
    }
}

static
void flecs_rest_parse_json_ser_entity_params(
    ecs_world_t *world,
    ecs_entity_to_json_desc_t *desc,
    const ecs_http_request_t *req)
{
    flecs_rest_bool_param(req, "path", &desc->serialize_path);
    flecs_rest_bool_param(req, "label", &desc->serialize_label);
    flecs_rest_bool_param(req, "brief", &desc->serialize_brief);
    flecs_rest_bool_param(req, "link", &desc->serialize_link);
    flecs_rest_bool_param(req, "color", &desc->serialize_color);
    flecs_rest_bool_param(req, "ids", &desc->serialize_ids);
    flecs_rest_bool_param(req, "id_labels", &desc->serialize_id_labels);
    flecs_rest_bool_param(req, "base", &desc->serialize_base);
    flecs_rest_bool_param(req, "values", &desc->serialize_values);
    flecs_rest_bool_param(req, "private", &desc->serialize_private);
    flecs_rest_bool_param(req, "type_info", &desc->serialize_type_info);
    flecs_rest_bool_param(req, "matches", &desc->serialize_matches);
    flecs_rest_bool_param(req, "alerts", &desc->serialize_alerts);

    char *rel = NULL;
    flecs_rest_string_param(req, "refs", &rel);
    if (rel) {
        desc->serialize_refs = ecs_lookup_fullpath(world, rel);
    }
}

static
void flecs_rest_parse_json_ser_iter_params(
    ecs_iter_to_json_desc_t *desc,
    const ecs_http_request_t *req)
{
    flecs_rest_bool_param(req, "term_ids", &desc->serialize_term_ids);
    flecs_rest_bool_param(req, "term_labels", &desc->serialize_term_labels);
    flecs_rest_bool_param(req, "ids", &desc->serialize_ids);
    flecs_rest_bool_param(req, "id_labels", &desc->serialize_id_labels);
    flecs_rest_bool_param(req, "sources", &desc->serialize_sources);
    flecs_rest_bool_param(req, "variables", &desc->serialize_variables);
    flecs_rest_bool_param(req, "is_set", &desc->serialize_is_set);
    flecs_rest_bool_param(req, "values", &desc->serialize_values);
    flecs_rest_bool_param(req, "private", &desc->serialize_private);
    flecs_rest_bool_param(req, "entities", &desc->serialize_entities);
    flecs_rest_bool_param(req, "entity_labels", &desc->serialize_entity_labels);
    flecs_rest_bool_param(req, "variable_labels", &desc->serialize_variable_labels);
    flecs_rest_bool_param(req, "variable_ids", &desc->serialize_variable_ids);
    flecs_rest_bool_param(req, "colors", &desc->serialize_colors);
    flecs_rest_bool_param(req, "duration", &desc->measure_eval_duration);
    flecs_rest_bool_param(req, "type_info", &desc->serialize_type_info);
    flecs_rest_bool_param(req, "table", &desc->serialize_table);
}

static
bool flecs_rest_reply_entity(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    char *path = &req->path[7];
    ecs_dbg_2("rest: request entity '%s'", path);

    ecs_os_linc(&ecs_rest_entity_count);

    ecs_entity_t e = ecs_lookup_path_w_sep(
        world, 0, path, "/", NULL, false);
    if (!e) {
        ecs_dbg_2("rest: entity '%s' not found", path);
        flecs_reply_error(reply, "entity '%s' not found", path);
        reply->code = 404;
        ecs_os_linc(&ecs_rest_entity_error_count);
        return true;
    }

    ecs_entity_to_json_desc_t desc = ECS_ENTITY_TO_JSON_INIT;
    flecs_rest_parse_json_ser_entity_params(world, &desc, req);
    if (ecs_entity_to_json_buf(world, e, &reply->body, &desc) != 0) {
        ecs_strbuf_reset(&reply->body);
        reply->code = 500;
        reply->status = "Internal server error";
        return true;
    }
    return true;
}

static
bool flecs_rest_reply_world(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)req;
    if (ecs_world_to_json_buf(world, &reply->body, NULL) != 0) {
        ecs_strbuf_reset(&reply->body);
        reply->code = 500;
        reply->status = "Internal server error";
        return true;
    }
    return true;
}

static
ecs_entity_t flecs_rest_entity_from_path(
    ecs_world_t *world,
    ecs_http_reply_t *reply,
    const char *path)
{
    ecs_entity_t e = ecs_lookup_path_w_sep(
        world, 0, path, "/", NULL, false);
    if (!e) {
        flecs_reply_error(reply, "entity '%s' not found", path);
        reply->code = 404;
    }
    return e;
}

static
bool flecs_rest_set(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    const char *path)
{
    ecs_entity_t e;
    if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
        return true;
    }

    const char *data = ecs_http_get_param(req, "data");
    ecs_from_json_desc_t desc = {0};
    desc.expr = data;
    desc.name = path;
    if (ecs_entity_from_json(world, e, data, &desc) == NULL) {
        flecs_reply_error(reply, "invalid request");
        reply->code = 400;
        return true;
    }
    
    return true;
}

static
bool flecs_rest_delete(
    ecs_world_t *world,
    ecs_http_reply_t *reply,
    const char *path)
{
    ecs_entity_t e;
    if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
        ecs_os_linc(&ecs_rest_delete_error_count);
        return true;
    }

    ecs_delete(world, e);
    
    return true;
}

static
bool flecs_rest_enable(
    ecs_world_t *world,
    ecs_http_reply_t *reply,
    const char *path,
    bool enable)
{
    ecs_os_linc(&ecs_rest_enable_count);

    ecs_entity_t e;
    if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
        ecs_os_linc(&ecs_rest_enable_error_count);
        return true;
    }

    ecs_enable(world, e, enable);
    
    return true;
}

static
bool flecs_rest_script(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)world;
    (void)req;
    (void)reply;
#ifdef FLECS_PLECS
    const char *data = ecs_http_get_param(req, "data");
    if (!data) {
        flecs_reply_error(reply, "missing data parameter");
        return true;
    }

    bool prev_color = ecs_log_enable_colors(false);
    rest_prev_log = ecs_os_api.log_;
    ecs_os_api.log_ = flecs_rest_capture_log;

    ecs_entity_t script = ecs_script(world, {
        .entity = ecs_entity(world, { .name = "scripts.main" }),
        .str = data
    });

    if (!script) {
        char *err = flecs_rest_get_captured_log();
        char *escaped_err = ecs_astresc('"', err);
        flecs_reply_error(reply, escaped_err);
        ecs_os_linc(&ecs_rest_query_error_count);
        reply->code = 400; /* bad request */
        ecs_os_free(escaped_err);
        ecs_os_free(err);
    }

    ecs_os_api.log_ = rest_prev_log;
    ecs_log_enable_colors(prev_color);

    return true;
#else
    return false;
#endif
}

static
void flecs_rest_reply_set_captured_log(
    ecs_http_reply_t *reply)
{
    char *err = flecs_rest_get_captured_log();
    if (err) {
        char *escaped_err = ecs_astresc('"', err);
        flecs_reply_error(reply, escaped_err);
        reply->code = 400;
        ecs_os_free(escaped_err);
        ecs_os_free(err);
    }
}

static
int flecs_rest_iter_to_reply(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    ecs_iter_t *it)
{
    ecs_iter_to_json_desc_t desc = {0};
    desc.serialize_entities = true;
    desc.serialize_variables = true;
    flecs_rest_parse_json_ser_iter_params(&desc, req);

    int32_t offset = 0;
    int32_t limit = 1000;

    flecs_rest_int_param(req, "offset", &offset);
    flecs_rest_int_param(req, "limit", &limit);

    if (offset < 0 || limit < 0) {
        flecs_reply_error(reply, "invalid offset/limit parameter");
        reply->code = 400;
        return -1;
    }

    ecs_iter_t pit = ecs_page_iter(it, offset, limit);
    if (ecs_iter_to_json_buf(world, &pit, &reply->body, &desc)) {
        flecs_rest_reply_set_captured_log(reply);
        return -1;
    }

    return 0;
}

static
bool flecs_rest_reply_existing_query(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    const char *name)
{
    ecs_os_linc(&ecs_rest_query_name_count);

    ecs_entity_t q = ecs_lookup_fullpath(world, name);
    if (!q) {
        flecs_reply_error(reply, "unresolved identifier '%s'", name);
        reply->code = 404;
        ecs_os_linc(&ecs_rest_query_name_error_count);
        return true;
    }

    const EcsPoly *poly = ecs_get_pair(world, q, EcsPoly, EcsQuery);
    if (!poly) {
        flecs_reply_error(reply, 
            "resolved identifier '%s' is not a query", name);
        reply->code = 400;
        ecs_os_linc(&ecs_rest_query_name_error_count);
        return true;
    }

    ecs_iter_t it;
    ecs_iter_poly(world, poly->poly, &it, NULL);

    ecs_dbg_2("rest: request query '%s'", q);
    bool prev_color = ecs_log_enable_colors(false);
    rest_prev_log = ecs_os_api.log_;
    ecs_os_api.log_ = flecs_rest_capture_log;

    const char *vars = ecs_http_get_param(req, "vars");
    if (vars) {
        if (!ecs_poly_is(poly->poly, ecs_rule_t)) {
            flecs_reply_error(reply, 
                "variables are only supported for rule queries");
            reply->code = 400;
            ecs_os_linc(&ecs_rest_query_name_error_count);
            return true;
        }
        if (ecs_rule_parse_vars(poly->poly, &it, vars) == NULL) {
            flecs_rest_reply_set_captured_log(reply);
            ecs_os_linc(&ecs_rest_query_name_error_count);
            return true;
        }
    }

    flecs_rest_iter_to_reply(world, req, reply, &it);

    ecs_os_api.log_ = rest_prev_log;
    ecs_log_enable_colors(prev_color);    

    return true;
}

static
bool flecs_rest_reply_query(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    const char *q_name = ecs_http_get_param(req, "name");
    if (q_name) {
        return flecs_rest_reply_existing_query(world, req, reply, q_name);
    }

    ecs_os_linc(&ecs_rest_query_count);

    const char *q = ecs_http_get_param(req, "q");
    if (!q) {
        ecs_strbuf_appendlit(&reply->body, "Missing parameter 'q'");
        reply->code = 400; /* bad request */
        ecs_os_linc(&ecs_rest_query_error_count);
        return true;
    }

    ecs_dbg_2("rest: request query '%s'", q);
    bool prev_color = ecs_log_enable_colors(false);
    rest_prev_log = ecs_os_api.log_;
    ecs_os_api.log_ = flecs_rest_capture_log;

    ecs_rule_t *r = ecs_rule_init(world, &(ecs_filter_desc_t){
        .expr = q
    });
    if (!r) {
        flecs_rest_reply_set_captured_log(reply);
        ecs_os_linc(&ecs_rest_query_error_count);
    } else {
        ecs_iter_t it = ecs_rule_iter(world, r);
        flecs_rest_iter_to_reply(world, req, reply, &it);
        ecs_rule_fini(r);
    }

    ecs_os_api.log_ = rest_prev_log;
    ecs_log_enable_colors(prev_color);

    return true;
}

#ifdef FLECS_MONITOR

static
void flecs_rest_array_append_(
    ecs_strbuf_t *reply,
    const char *field,
    int32_t field_len,
    const ecs_float_t *values,
    int32_t t)
{
    ecs_strbuf_list_appendch(reply, '"');
    ecs_strbuf_appendstrn(reply, field, field_len);
    ecs_strbuf_appendlit(reply, "\":");
    ecs_strbuf_list_push(reply, "[", ",");

    int32_t i;
    for (i = t + 1; i <= (t + ECS_STAT_WINDOW); i ++) {
        int32_t index = i % ECS_STAT_WINDOW;
        ecs_strbuf_list_next(reply);
        ecs_strbuf_appendflt(reply, (double)values[index], '"');
    }

    ecs_strbuf_list_pop(reply, "]");
}

#define flecs_rest_array_append(reply, field, values, t)\
    flecs_rest_array_append_(reply, field, sizeof(field) - 1, values, t)

static
void flecs_rest_gauge_append(
    ecs_strbuf_t *reply,
    const ecs_metric_t *m,
    const char *field,
    int32_t field_len,
    int32_t t,
    const char *brief,
    int32_t brief_len)
{
    ecs_strbuf_list_appendch(reply, '"');
    ecs_strbuf_appendstrn(reply, field, field_len);
    ecs_strbuf_appendlit(reply, "\":");
    ecs_strbuf_list_push(reply, "{", ",");

    flecs_rest_array_append(reply, "avg", m->gauge.avg, t);
    flecs_rest_array_append(reply, "min", m->gauge.min, t);
    flecs_rest_array_append(reply, "max", m->gauge.max, t);

    if (brief) {
        ecs_strbuf_list_appendlit(reply, "\"brief\":\"");
        ecs_strbuf_appendstrn(reply, brief, brief_len);
        ecs_strbuf_appendch(reply, '"');
    }

    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_rest_counter_append(
    ecs_strbuf_t *reply,
    const ecs_metric_t *m,
    const char *field,
    int32_t field_len,
    int32_t t,
    const char *brief,
    int32_t brief_len)
{
    flecs_rest_gauge_append(reply, m, field, field_len, t, brief, brief_len);
}

#define ECS_GAUGE_APPEND_T(reply, s, field, t, brief)\
    flecs_rest_gauge_append(reply, &(s)->field, #field, sizeof(#field) - 1, t, brief, sizeof(brief) - 1)

#define ECS_COUNTER_APPEND_T(reply, s, field, t, brief)\
    flecs_rest_counter_append(reply, &(s)->field, #field, sizeof(#field) - 1, t, brief, sizeof(brief) - 1)

#define ECS_GAUGE_APPEND(reply, s, field, brief)\
    ECS_GAUGE_APPEND_T(reply, s, field, (s)->t, brief)

#define ECS_COUNTER_APPEND(reply, s, field, brief)\
    ECS_COUNTER_APPEND_T(reply, s, field, (s)->t, brief)

static
void flecs_world_stats_to_json(
    ecs_strbuf_t *reply,
    const EcsWorldStats *monitor_stats)
{
    const ecs_world_stats_t *stats = &monitor_stats->stats;

    ecs_strbuf_list_push(reply, "{", ",");
    ECS_GAUGE_APPEND(reply, stats, entities.count, "Alive entity ids in the world");
    ECS_GAUGE_APPEND(reply, stats, entities.not_alive_count, "Not alive entity ids in the world");

    ECS_GAUGE_APPEND(reply, stats, performance.fps, "Frames per second");
    ECS_COUNTER_APPEND(reply, stats, performance.frame_time, "Time spent in frame");
    ECS_COUNTER_APPEND(reply, stats, performance.system_time, "Time spent on running systems in frame");
    ECS_COUNTER_APPEND(reply, stats, performance.emit_time, "Time spent on notifying observers in frame");
    ECS_COUNTER_APPEND(reply, stats, performance.merge_time, "Time spent on merging commands in frame");
    ECS_COUNTER_APPEND(reply, stats, performance.rematch_time, "Time spent on revalidating query caches in frame");

    ECS_COUNTER_APPEND(reply, stats, commands.add_count, "Add commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.remove_count, "Remove commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.delete_count, "Delete commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.clear_count, "Clear commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.set_count, "Set commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.get_mut_count, "Get_mut commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.modified_count, "Modified commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.other_count, "Misc commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.discard_count, "Commands for already deleted entities");
    ECS_COUNTER_APPEND(reply, stats, commands.batched_entity_count, "Entities with batched commands");
    ECS_COUNTER_APPEND(reply, stats, commands.batched_count, "Number of commands batched");

    ECS_COUNTER_APPEND(reply, stats, frame.merge_count, "Number of merges (sync points)");
    ECS_COUNTER_APPEND(reply, stats, frame.pipeline_build_count, "Pipeline rebuilds (happen when systems become active/enabled)");
    ECS_COUNTER_APPEND(reply, stats, frame.systems_ran, "Systems ran in frame");
    ECS_COUNTER_APPEND(reply, stats, frame.observers_ran, "Number of times an observer was invoked in frame");
    ECS_COUNTER_APPEND(reply, stats, frame.event_emit_count, "Events emitted in frame");
    ECS_COUNTER_APPEND(reply, stats, frame.rematch_count, "Number of query cache revalidations");

    ECS_GAUGE_APPEND(reply, stats, tables.count, "Tables in the world (including empty)");
    ECS_GAUGE_APPEND(reply, stats, tables.empty_count, "Empty tables in the world");
    ECS_GAUGE_APPEND(reply, stats, tables.tag_only_count, "Tables with only tags");
    ECS_GAUGE_APPEND(reply, stats, tables.trivial_only_count, "Tables with only trivial types (no hooks)");
    ECS_GAUGE_APPEND(reply, stats, tables.record_count, "Table records registered with search indices");
    ECS_GAUGE_APPEND(reply, stats, tables.storage_count, "Component storages for all tables");
    ECS_COUNTER_APPEND(reply, stats, tables.create_count, "Number of new tables created");
    ECS_COUNTER_APPEND(reply, stats, tables.delete_count, "Number of tables deleted");

    ECS_GAUGE_APPEND(reply, stats, ids.count, "Component, tag and pair ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.tag_count, "Tag ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.component_count, "Component ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.pair_count, "Pair ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.wildcard_count, "Wildcard ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.type_count, "Registered component types");
    ECS_COUNTER_APPEND(reply, stats, ids.create_count, "Number of new component, tag and pair ids created");
    ECS_COUNTER_APPEND(reply, stats, ids.delete_count, "Number of component, pair and tag ids deleted");

    ECS_GAUGE_APPEND(reply, stats, queries.query_count, "Queries in the world");
    ECS_GAUGE_APPEND(reply, stats, queries.observer_count, "Observers in the world");
    ECS_GAUGE_APPEND(reply, stats, queries.system_count, "Systems in the world");

    ECS_COUNTER_APPEND(reply, stats, memory.alloc_count, "Allocations by OS API");
    ECS_COUNTER_APPEND(reply, stats, memory.realloc_count, "Reallocs by OS API");
    ECS_COUNTER_APPEND(reply, stats, memory.free_count, "Frees by OS API");
    ECS_GAUGE_APPEND(reply, stats, memory.outstanding_alloc_count, "Outstanding allocations by OS API");
    ECS_COUNTER_APPEND(reply, stats, memory.block_alloc_count, "Blocks allocated by block allocators");
    ECS_COUNTER_APPEND(reply, stats, memory.block_free_count, "Blocks freed by block allocators");
    ECS_GAUGE_APPEND(reply, stats, memory.block_outstanding_alloc_count, "Outstanding block allocations");
    ECS_COUNTER_APPEND(reply, stats, memory.stack_alloc_count, "Pages allocated by stack allocators");
    ECS_COUNTER_APPEND(reply, stats, memory.stack_free_count, "Pages freed by stack allocators");
    ECS_GAUGE_APPEND(reply, stats, memory.stack_outstanding_alloc_count, "Outstanding page allocations");

    ECS_COUNTER_APPEND(reply, stats, rest.request_count, "Received requests");
    ECS_COUNTER_APPEND(reply, stats, rest.entity_count, "Received entity/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.entity_error_count, "Failed entity/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_count, "Received query/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_error_count, "Failed query/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_name_count, "Received named query/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_name_error_count, "Failed named query/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_name_from_cache_count, "Named query/ requests from cache");
    ECS_COUNTER_APPEND(reply, stats, rest.enable_count, "Received enable/ and disable/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.enable_error_count, "Failed enable/ and disable/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.world_stats_count, "Received world stats requests");
    ECS_COUNTER_APPEND(reply, stats, rest.pipeline_stats_count, "Received pipeline stats requests");
    ECS_COUNTER_APPEND(reply, stats, rest.stats_error_count, "Failed stats requests");

    ECS_COUNTER_APPEND(reply, stats, http.request_received_count, "Received requests");
    ECS_COUNTER_APPEND(reply, stats, http.request_invalid_count, "Received invalid requests");
    ECS_COUNTER_APPEND(reply, stats, http.request_handled_ok_count, "Requests handled successfully");
    ECS_COUNTER_APPEND(reply, stats, http.request_handled_error_count, "Requests handled with error code");
    ECS_COUNTER_APPEND(reply, stats, http.request_not_handled_count, "Requests not handled (unknown endpoint)");
    ECS_COUNTER_APPEND(reply, stats, http.request_preflight_count, "Preflight requests received");
    ECS_COUNTER_APPEND(reply, stats, http.send_ok_count, "Successful replies");
    ECS_COUNTER_APPEND(reply, stats, http.send_error_count, "Unsuccessful replies");
    ECS_COUNTER_APPEND(reply, stats, http.busy_count, "Dropped requests due to full send queue (503)");

    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_system_stats_to_json(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    ecs_entity_t system,
    const ecs_system_stats_t *stats)
{
    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_appendlit(reply, "\"name\":\"");
    ecs_get_path_w_sep_buf(world, 0, system, ".", NULL, reply);
    ecs_strbuf_appendch(reply, '"');

    if (!stats->task) {
        ECS_GAUGE_APPEND(reply, &stats->query, matched_table_count, "");
        ECS_GAUGE_APPEND(reply, &stats->query, matched_entity_count, "");
    }

    ECS_COUNTER_APPEND_T(reply, stats, time_spent, stats->query.t, "");
    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_pipeline_stats_to_json(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const EcsPipelineStats *stats)
{
    ecs_strbuf_list_push(reply, "[", ",");

    int32_t i, count = ecs_vec_count(&stats->stats.systems), sync_cur = 0;
    ecs_entity_t *ids = ecs_vec_first_t(&stats->stats.systems, ecs_entity_t);
    for (i = 0; i < count; i ++) {
        ecs_entity_t id = ids[i];
        
        ecs_strbuf_list_next(reply);

        if (id) {
            ecs_system_stats_t *sys_stats = ecs_map_get_deref(
                &stats->stats.system_stats, ecs_system_stats_t, id);
            flecs_system_stats_to_json(world, reply, id, sys_stats);
        } else {
            /* Sync point */
            ecs_strbuf_list_push(reply, "{", ",");
            ecs_sync_stats_t *sync_stats = ecs_vec_get_t(
                &stats->stats.sync_points, ecs_sync_stats_t, sync_cur);

            ecs_strbuf_list_appendlit(reply, "\"system_count\":");
            ecs_strbuf_appendint(reply, sync_stats->system_count);

            ecs_strbuf_list_appendlit(reply, "\"multi_threaded\":");
            ecs_strbuf_appendbool(reply, sync_stats->multi_threaded);

            ecs_strbuf_list_appendlit(reply, "\"no_readonly\":");
            ecs_strbuf_appendbool(reply, sync_stats->no_readonly);

            ECS_GAUGE_APPEND_T(reply, sync_stats, 
                time_spent, stats->stats.t, "");
            ECS_GAUGE_APPEND_T(reply, sync_stats, 
                commands_enqueued, stats->stats.t, "");

            ecs_strbuf_list_pop(reply, "}");
            sync_cur ++;
        }
    }

    ecs_strbuf_list_pop(reply, "]");
}

static
bool flecs_rest_reply_stats(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    char *period_str = NULL;
    flecs_rest_string_param(req, "period", &period_str);
    char *category = &req->path[6];

    ecs_entity_t period = EcsPeriod1s;
    if (period_str) {
        char *period_name = ecs_asprintf("Period%s", period_str);
        period = ecs_lookup_child(world, ecs_id(FlecsMonitor), period_name);
        ecs_os_free(period_name);
        if (!period) {
            flecs_reply_error(reply, "bad request (invalid period string)");
            reply->code = 400;
            ecs_os_linc(&ecs_rest_stats_error_count);
            return false;
        }
    }

    if (!ecs_os_strcmp(category, "world")) {
        const EcsWorldStats *stats = ecs_get_pair(world, EcsWorld, 
            EcsWorldStats, period);
        flecs_world_stats_to_json(&reply->body, stats);
        ecs_os_linc(&ecs_rest_world_stats_count);
        return true;

    } else if (!ecs_os_strcmp(category, "pipeline")) {
        const EcsPipelineStats *stats = ecs_get_pair(world, EcsWorld, 
            EcsPipelineStats, period);
        flecs_pipeline_stats_to_json(world, &reply->body, stats);
        ecs_os_linc(&ecs_rest_pipeline_stats_count);
        return true;

    } else {
        flecs_reply_error(reply, "bad request (unsupported category)");
        reply->code = 400;
        ecs_os_linc(&ecs_rest_stats_error_count);
        return false;
    }
}
#else
static
bool flecs_rest_reply_stats(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)world;
    (void)req;
    (void)reply;
    return false;
}
#endif

static
void flecs_rest_reply_table_append_type(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    ecs_strbuf_list_push(reply, "[", ",");
    int32_t i, count = table->type.count;
    ecs_id_t *ids = table->type.array;
    for (i = 0; i < count; i ++) {
        ecs_strbuf_list_next(reply);
        ecs_strbuf_appendch(reply, '"');
        ecs_id_str_buf(world, ids[i], reply);
        ecs_strbuf_appendch(reply, '"');
    }
    ecs_strbuf_list_pop(reply, "]");
}

static
void flecs_rest_reply_table_append_memory(
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    int32_t used = 0, allocated = 0;

    used += table->data.entities.count * ECS_SIZEOF(ecs_entity_t);
    allocated += table->data.entities.size * ECS_SIZEOF(ecs_entity_t);

    int32_t i, storage_count = table->column_count;
    ecs_column_t *columns = table->data.columns;

    for (i = 0; i < storage_count; i ++) {
        used += columns[i].data.count * columns[i].ti->size;
        allocated += columns[i].data.size * columns[i].ti->size;
    }

    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_append(reply, "\"used\":%d", used);
    ecs_strbuf_list_append(reply, "\"allocated\":%d", allocated);
    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_rest_reply_table_append(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    ecs_strbuf_list_next(reply);
    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_append(reply, "\"id\":%u", (uint32_t)table->id);
    ecs_strbuf_list_appendstr(reply, "\"type\":");
    flecs_rest_reply_table_append_type(world, reply, table);
    ecs_strbuf_list_append(reply, "\"count\":%d", ecs_table_count(table));
    ecs_strbuf_list_append(reply, "\"memory\":");
    flecs_rest_reply_table_append_memory(reply, table);
    ecs_strbuf_list_pop(reply, "}");
}

static
bool flecs_rest_reply_tables(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)req;

    ecs_strbuf_list_push(&reply->body, "[", ",");
    ecs_sparse_t *tables = &world->store.tables;
    int32_t i, count = flecs_sparse_count(tables);
    for (i = 0; i < count; i ++) {
        ecs_table_t *table = flecs_sparse_get_dense_t(tables, ecs_table_t, i);
        flecs_rest_reply_table_append(world, &reply->body, table);
    }
    ecs_strbuf_list_pop(&reply->body, "]");

    return true;
}

static
bool flecs_rest_reply(
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    void *ctx)
{
    ecs_rest_ctx_t *impl = ctx;
    ecs_world_t *world = impl->world;

    ecs_os_linc(&ecs_rest_request_count);

    if (req->path == NULL) {
        ecs_dbg("rest: bad request (missing path)");
        flecs_reply_error(reply, "bad request (missing path)");
        reply->code = 400;
        return false;
    }

    if (req->method == EcsHttpGet) {
        /* Entity endpoint */
        if (!ecs_os_strncmp(req->path, "entity/", 7)) {
            return flecs_rest_reply_entity(world, req, reply);

        /* Query endpoint */
        } else if (!ecs_os_strcmp(req->path, "query")) {
            return flecs_rest_reply_query(world, req, reply);

        /* World endpoint */
        } else if (!ecs_os_strcmp(req->path, "world")) {
            return flecs_rest_reply_world(world, req, reply);

        /* Stats endpoint */
        } else if (!ecs_os_strncmp(req->path, "stats/", 6)) {
            return flecs_rest_reply_stats(world, req, reply);

        /* Tables endpoint */
        } else if (!ecs_os_strncmp(req->path, "tables", 6)) {
            return flecs_rest_reply_tables(world, req, reply);
        }

    } else if (req->method == EcsHttpPut) {
        /* Set endpoint */
        if (!ecs_os_strncmp(req->path, "set/", 4)) {
            return flecs_rest_set(world, req, reply, &req->path[4]);
        
        /* Delete endpoint */
        } else if (!ecs_os_strncmp(req->path, "delete/", 7)) {
            return flecs_rest_delete(world, reply, &req->path[7]);

        /* Enable endpoint */
        } else if (!ecs_os_strncmp(req->path, "enable/", 7)) {
            return flecs_rest_enable(world, reply, &req->path[7], true);

        /* Disable endpoint */
        } else if (!ecs_os_strncmp(req->path, "disable/", 8)) {
            return flecs_rest_enable(world, reply, &req->path[8], false);

        /* Script endpoint */
        } else if (!ecs_os_strncmp(req->path, "script", 6)) {
            return flecs_rest_script(world, req, reply);
        }
    }

    return false;
}

ecs_http_server_t* ecs_rest_server_init(
    ecs_world_t *world,
    const ecs_http_server_desc_t *desc)
{
    ecs_rest_ctx_t *srv_ctx = ecs_os_calloc_t(ecs_rest_ctx_t);
    ecs_http_server_desc_t private_desc = {0};
    if (desc) {
        private_desc = *desc;
    }
    private_desc.callback = flecs_rest_reply;
    private_desc.ctx = srv_ctx;

    ecs_http_server_t *srv = ecs_http_server_init(&private_desc);
    if (!srv) {
        ecs_os_free(srv_ctx);
        return NULL;
    }

    srv_ctx->world = world;
    srv_ctx->srv = srv;
    srv_ctx->rc = 1;
    srv_ctx->srv = srv;
    return srv;
}

void ecs_rest_server_fini(
    ecs_http_server_t *srv)
{
    ecs_rest_ctx_t *srv_ctx = ecs_http_server_ctx(srv);
    ecs_os_free(srv_ctx);
    ecs_http_server_fini(srv);
}

static
void flecs_on_set_rest(ecs_iter_t *it) {
    EcsRest *rest = it->ptrs[0];

    int i;
    for(i = 0; i < it->count; i ++) {
        if (!rest[i].port) {
            rest[i].port = ECS_REST_DEFAULT_PORT;
        }

        ecs_http_server_t *srv = ecs_rest_server_init(it->real_world,
            &(ecs_http_server_desc_t){ 
                .ipaddr = rest[i].ipaddr, 
                .port = rest[i].port 
            });

        if (!srv) {
            const char *ipaddr = rest[i].ipaddr ? rest[i].ipaddr : "0.0.0.0";
            ecs_err("failed to create REST server on %s:%u", 
                ipaddr, rest[i].port);
            continue;
        }

        rest[i].impl = ecs_http_server_ctx(srv);

        ecs_http_server_start(srv);
    }
}

static
void DequeueRest(ecs_iter_t *it) {
    EcsRest *rest = ecs_field(it, EcsRest, 1);

    if (it->delta_system_time > (ecs_ftime_t)1.0) {
        ecs_warn(
            "detected large progress interval (%.2fs), REST request may timeout",
            (double)it->delta_system_time);
    }

    int32_t i;
    for(i = 0; i < it->count; i ++) {
        ecs_rest_ctx_t *ctx = rest[i].impl;
        if (ctx) {
            ecs_http_server_dequeue(ctx->srv, it->delta_time);
        }
    } 
}

static
void DisableRest(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    ecs_iter_t rit = ecs_term_iter(world, &(ecs_term_t){
        .id = ecs_id(EcsRest),
        .src.flags = EcsSelf
    });

    if (it->event == EcsOnAdd) {
        /* REST module was disabled */
        while (ecs_term_next(&rit)) {
            EcsRest *rest = ecs_field(&rit, EcsRest, 1);
            int i;
            for (i = 0; i < rit.count; i ++) {
                ecs_rest_ctx_t *ctx = rest[i].impl;
                ecs_http_server_stop(ctx->srv);
            }
        }
    } else if (it->event == EcsOnRemove) {
        /* REST module was enabled */
        while (ecs_term_next(&rit)) {
            EcsRest *rest = ecs_field(&rit, EcsRest, 1);
            int i;
            for (i = 0; i < rit.count; i ++) {
                ecs_rest_ctx_t *ctx = rest[i].impl;
                ecs_http_server_start(ctx->srv);
            }
        }
    }
}

void FlecsRestImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsRest);

    ECS_IMPORT(world, FlecsPipeline);
#ifdef FLECS_PLECS
    ECS_IMPORT(world, FlecsScript);
#endif

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsRest);

    ecs_set_hooks(world, EcsRest, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsRest),
        .copy = ecs_copy(EcsRest),
        .dtor = ecs_dtor(EcsRest),
        .on_set = flecs_on_set_rest
    });

    ECS_SYSTEM(world, DequeueRest, EcsPostFrame, EcsRest);

    ecs_system(world, {
        .entity = ecs_id(DequeueRest),
        .no_readonly = true
    });

    ecs_observer(world, {
        .filter = { 
            .terms = {{ .id = EcsDisabled, .src.id = ecs_id(FlecsRest) }}
        },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = DisableRest
    });

    ecs_set_name_prefix(world, "EcsRest");
    ECS_TAG_DEFINE(world, EcsRestPlecs);
}

#endif
