#define _POSIX_C_SOURCE 200809L

#include "http_api.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>

#include "agent.h"
#include "llm.h"
#include "main.h"
#include "nostr_handler.h"
#include "trigger_manager.h"
#include "tools/tools.h"
#include "cjson/cJSON.h"
#include "debug.h"
#include "mongoose.h"

static http_api_context_t g_api_ctx;
static int g_api_initialized = 0;
static struct mg_mgr g_mgr;
static struct mg_connection* g_listener = NULL;
static int g_api_tls_enabled = 0;
static struct mg_str g_tls_cert = {NULL, 0};
static struct mg_str g_tls_key = {NULL, 0};

static const char* k_json_headers =
    "Content-Type: application/json\r\n"
    "Access-Control-Allow-Origin: *\r\n"
    "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n"
    "Access-Control-Allow-Headers: Content-Type\r\n"
    "Access-Control-Allow-Private-Network: true\r\n";

static int method_is(const struct mg_http_message* hm, const char* method) {
    size_t n = strlen(method);
    return hm && hm->method.len == n && strncmp(hm->method.buf, method, n) == 0;
}

static void reply_json(struct mg_connection* c, int status, cJSON* root) {
    char* out = cJSON_PrintUnformatted(root);
    if (!out) {
        mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"json serialization failed\"}");
        return;
    }
    mg_http_reply(c, status, k_json_headers, "%s", out);
    free(out);
}

static void reply_error(struct mg_connection* c, int status, const char* err) {
    cJSON* root = cJSON_CreateObject();
    if (!root) {
        mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"oom\"}");
        return;
    }
    cJSON_AddBoolToObject(root, "success", 0);
    cJSON_AddStringToObject(root, "error", err ? err : "error");
    reply_json(c, status, root);
    cJSON_Delete(root);
}

static cJSON* parse_body_json(const struct mg_http_message* hm) {
    if (!hm || hm->body.len == 0) {
        return cJSON_CreateObject();
    }

    char* buf = (char*)malloc(hm->body.len + 1U);
    if (!buf) {
        return NULL;
    }
    memcpy(buf, hm->body.buf, hm->body.len);
    buf[hm->body.len] = '\0';

    cJSON* root = cJSON_Parse(buf);
    free(buf);
    return root;
}

static int estimate_tokens_from_chars(int chars) {
    if (chars <= 0) return 0;
    return (chars + 3) / 4;
}

static uint64_t fnv1a64_local(const char* s) {
    uint64_t h = 1469598103934665603ULL;
    if (!s) return h;
    while (*s) {
        h ^= (unsigned char)(*s++);
        h *= 1099511628211ULL;
    }
    return h;
}

static uint64_t tool_calls_fingerprint_local(const llm_response_t* resp) {
    if (!resp || resp->tool_call_count <= 0 || !resp->tool_calls) {
        return 1469598103934665603ULL;
    }

    uint64_t h = 1469598103934665603ULL;
    for (int i = 0; i < resp->tool_call_count; i++) {
        const llm_tool_call_t* tc = &resp->tool_calls[i];
        uint64_t name_h = fnv1a64_local(tc->name ? tc->name : "");
        uint64_t args_h = fnv1a64_local(tc->arguments_json ? tc->arguments_json : "{}");
        uint64_t part = name_h ^ (args_h + 0x9e3779b97f4a7c15ULL + (name_h << 6) + (name_h >> 2));
        h ^= part + (uint64_t)(i + 1) * 1099511628211ULL;
        h *= 1099511628211ULL;
    }

    h ^= (uint64_t)resp->tool_call_count;
    h *= 1099511628211ULL;
    return h;
}

static void notify_admin_limit_diagnostic_http(const char* source,
                                               const char* reason,
                                               const char* subject,
                                               int max_turns,
                                               int turns_run,
                                               int stall_repeat_threshold,
                                               int repeated_tool_turns,
                                               int current_max_tokens,
                                               const char* possible_cause,
                                               const char* hint,
                                               const char* final_answer) {
    if (!g_api_ctx.cfg || g_api_ctx.cfg->admin.pubkey[0] == '\0') {
        return;
    }

    char diag[1800];
    snprintf(diag,
             sizeof(diag),
             "⚠️ Didactyl limit diagnostic\n"
             "source=%s\n"
             "reason=%s\n"
             "subject=%s\n"
             "max_turns=%d\n"
             "turns_run=%d\n"
             "stall_repeat_threshold=%d\n"
             "repeated_tool_turns=%d\n"
             "current_max_tokens=%d\n"
             "possible_cause=%s\n"
             "hint=%s\n"
             "final_answer_preview=%.*s",
             source ? source : "unknown",
             reason ? reason : "unknown",
             subject ? subject : "n/a",
             max_turns,
             turns_run,
             stall_repeat_threshold,
             repeated_tool_turns,
             current_max_tokens,
             possible_cause ? possible_cause : "n/a",
             hint ? hint : "n/a",
             360,
             final_answer ? final_answer : "");

    (void)nostr_handler_send_dm_auto(g_api_ctx.cfg->admin.pubkey, diag);
}

static int uri_extract_after_prefix(const struct mg_str* uri,
                                    const char* prefix,
                                    char* out,
                                    size_t out_sz) {
    if (!uri || !prefix || !out || out_sz == 0) {
        return -1;
    }
    out[0] = '\0';

    size_t prefix_len = strlen(prefix);
    if (uri->len <= prefix_len || strncmp(uri->buf, prefix, prefix_len) != 0) {
        return -1;
    }

    size_t rem = uri->len - prefix_len;
    if (rem == 0 || rem >= out_sz) {
        return -1;
    }

    memcpy(out, uri->buf + prefix_len, rem);
    out[rem] = '\0';
    return 0;
}


static void handle_trigger_webhook(struct mg_connection* c,
                                   const struct mg_http_message* hm,
                                   const char* d_tag) {
    if (!g_api_ctx.trigger_manager) {
        reply_error(c, 503, "trigger manager unavailable");
        return;
    }

    active_trigger_t trigger;
    if (trigger_manager_find(g_api_ctx.trigger_manager, d_tag, &trigger) != 0) {
        reply_error(c, 404, "active trigger not found for d_tag");
        return;
    }

    if (trigger.trigger_type != TRIGGER_TYPE_WEBHOOK) {
        reply_error(c, 400, "trigger type is not webhook");
        return;
    }

    if (!trigger.enabled) {
        reply_error(c, 400, "webhook trigger is disabled");
        return;
    }

    cJSON* payload = parse_body_json(hm);
    if (!payload || !cJSON_IsObject(payload)) {
        cJSON_Delete(payload);
        reply_error(c, 400, "invalid JSON body");
        return;
    }

    cJSON* event = cJSON_CreateObject();
    if (!event) {
        cJSON_Delete(payload);
        reply_error(c, 500, "oom");
        return;
    }

    cJSON_AddStringToObject(event, "type", "webhook");
    cJSON_AddStringToObject(event, "skill_d_tag", d_tag);
    cJSON_AddNumberToObject(event, "created_at", (double)time(NULL));
    cJSON_AddItemToObject(event, "payload", payload);

    int fired = trigger_manager_fire(g_api_ctx.trigger_manager, d_tag, event, "webhook");

    cJSON_Delete(event);

    if (fired < 0) {
        reply_error(c, 404, "active trigger not found for d_tag");
        return;
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        reply_error(c, 500, "oom");
        return;
    }

    cJSON_AddBoolToObject(out, "success", 1);
    cJSON_AddStringToObject(out, "d_tag", d_tag);
    cJSON_AddBoolToObject(out, "fired", fired > 0 ? 1 : 0);
    reply_json(c, 200, out);
    cJSON_Delete(out);
}

static char* maybe_model_override_begin(cJSON* body, llm_config_t* out_old_cfg, int* out_overridden) {
    if (!body || !out_old_cfg || !out_overridden) return NULL;
    *out_overridden = 0;

    cJSON* model = cJSON_GetObjectItemCaseSensitive(body, "model");
    if (!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0') {
        return NULL;
    }

    if (llm_get_config(out_old_cfg) != 0) {
        return "failed to read current llm config";
    }

    llm_config_t next = *out_old_cfg;
    snprintf(next.model, sizeof(next.model), "%s", model->valuestring);
    if (llm_set_config(&next) != 0) {
        return "failed to set temporary model";
    }

    *out_overridden = 1;
    return NULL;
}

static void maybe_model_override_end(const llm_config_t* old_cfg, int overridden) {
    if (overridden && old_cfg) {
        (void)llm_set_config(old_cfg);
    }
}

static const char* classify_part_name(cJSON* msg, int idx) {
    return agent_classify_message_part(msg, idx);
}

static int build_context_parts_response(cJSON* out_root) {
    if (!out_root) return -1;

    char* messages_json = NULL;
    if (agent_build_admin_messages_json("", DIDACTYL_SENDER_ADMIN, &messages_json) != 0 || !messages_json) {
        return -1;
    }

    cJSON* arr = cJSON_Parse(messages_json);
    free(messages_json);
    if (!arr || !cJSON_IsArray(arr)) {
        cJSON_Delete(arr);
        return -1;
    }

    cJSON* parts = cJSON_CreateArray();
    if (!parts) {
        cJSON_Delete(arr);
        return -1;
    }

    int total_chars = 0;
    int n = cJSON_GetArraySize(arr);
    for (int i = 0; i < n; i++) {
        cJSON* msg = cJSON_GetArrayItem(arr, i);
        if (!msg || !cJSON_IsObject(msg)) continue;

        cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role");
        cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
        const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "unknown";
        const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";

        int chars = (int)strlen(content_s);
        total_chars += chars;

        cJSON* part = cJSON_CreateObject();
        if (!part) continue;
        cJSON_AddStringToObject(part, "name", classify_part_name(msg, i));
        cJSON_AddStringToObject(part, "role", role_s);
        cJSON_AddNumberToObject(part, "chars", chars);
        cJSON_AddNumberToObject(part, "estimated_tokens", estimate_tokens_from_chars(chars));
        cJSON_AddStringToObject(part, "content", content_s);
        cJSON_AddItemToArray(parts, part);
    }

    cJSON_AddBoolToObject(out_root, "success", 1);
    cJSON_AddNumberToObject(out_root, "total_chars", total_chars);
    cJSON_AddNumberToObject(out_root, "total_estimated_tokens", estimate_tokens_from_chars(total_chars));
    cJSON_AddItemToObject(out_root, "parts", parts);

    cJSON_AddItemToObject(out_root, "messages", arr);
    return 0;
}

static int append_simple_message_local(cJSON* messages, const char* role, const char* content) {
    if (!messages || !role) return -1;
    cJSON* msg = cJSON_CreateObject();
    if (!msg) return -1;
    cJSON_AddStringToObject(msg, "role", role);
    cJSON_AddStringToObject(msg, "content", content ? content : "");
    cJSON_AddItemToArray(messages, msg);
    return 0;
}

static int append_assistant_tool_calls_message_local(cJSON* messages, const llm_response_t* resp) {
    if (!messages || !resp || resp->tool_call_count <= 0) return -1;

    cJSON* assistant = cJSON_CreateObject();
    cJSON* tool_calls = cJSON_CreateArray();
    if (!assistant || !tool_calls) {
        cJSON_Delete(assistant);
        cJSON_Delete(tool_calls);
        return -1;
    }

    cJSON_AddStringToObject(assistant, "role", "assistant");
    if (resp->content) cJSON_AddStringToObject(assistant, "content", resp->content);
    else cJSON_AddNullToObject(assistant, "content");

    for (int i = 0; i < resp->tool_call_count; i++) {
        const llm_tool_call_t* tc = &resp->tool_calls[i];
        cJSON* tc_obj = cJSON_CreateObject();
        cJSON* fn_obj = cJSON_CreateObject();
        if (!tc_obj || !fn_obj) {
            cJSON_Delete(tc_obj);
            cJSON_Delete(fn_obj);
            cJSON_Delete(assistant);
            cJSON_Delete(tool_calls);
            return -1;
        }

        cJSON_AddStringToObject(tc_obj, "id", tc->id ? tc->id : "");
        cJSON_AddStringToObject(tc_obj, "type", "function");
        cJSON_AddStringToObject(fn_obj, "name", tc->name ? tc->name : "");
        cJSON_AddStringToObject(fn_obj, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
        cJSON_AddItemToObject(tc_obj, "function", fn_obj);
        cJSON_AddItemToArray(tool_calls, tc_obj);
    }

    cJSON_AddItemToObject(assistant, "tool_calls", tool_calls);
    cJSON_AddItemToArray(messages, assistant);
    return 0;
}

static int append_tool_result_message_local(cJSON* messages, const char* tool_call_id, const char* tool_result_json) {
    if (!messages || !tool_call_id) return -1;

    cJSON* msg = cJSON_CreateObject();
    if (!msg) return -1;

    cJSON_AddStringToObject(msg, "role", "tool");
    cJSON_AddStringToObject(msg, "tool_call_id", tool_call_id);
    cJSON_AddStringToObject(msg,
                            "content",
                            tool_result_json ? tool_result_json : "{\"success\":false,\"error\":\"empty tool result\"}");
    cJSON_AddItemToArray(messages, msg);
    return 0;
}

static int is_space_char_local(char c) {
    return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}

static const char* skip_spaces_local(const char* s) {
    while (s && *s && is_space_char_local(*s)) s++;
    return s ? s : "";
}

static char* json_error_string_local(const char* msg) {
    cJSON* out = cJSON_CreateObject();
    if (!out) return NULL;
    cJSON_AddBoolToObject(out, "success", 0);
    cJSON_AddStringToObject(out, "error", msg ? msg : "error");
    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    return json;
}

static char* build_slash_args_json_local(const char* raw_args) {
    const char* s = skip_spaces_local(raw_args);
    if (!s || s[0] == '\0') {
        return strdup("{}");
    }

    cJSON* parsed = cJSON_Parse(s);
    if (parsed) {
        char* out = cJSON_PrintUnformatted(parsed);
        cJSON_Delete(parsed);
        return out;
    }

    cJSON* obj = cJSON_CreateObject();
    if (!obj) return NULL;
    cJSON_AddStringToObject(obj, "input", s);
    char* out = cJSON_PrintUnformatted(obj);
    cJSON_Delete(obj);
    return out;
}

static int append_textf_http(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
    if (!buf || !cap || !used || !fmt) return -1;

    while (1) {
        if (*used + 128U >= *cap) {
            size_t next_cap = (*cap) * 2U;
            char* grown = (char*)realloc(*buf, next_cap);
            if (!grown) return -1;
            *buf = grown;
            *cap = next_cap;
        }

        va_list ap;
        va_start(ap, fmt);
        int n = vsnprintf(*buf + *used, *cap - *used, fmt, ap);
        va_end(ap);

        if (n < 0) return -1;
        if ((size_t)n < (*cap - *used)) {
            *used += (size_t)n;
            return 0;
        }

        size_t next_cap = (*cap) * 2U + (size_t)n + 64U;
        char* grown = (char*)realloc(*buf, next_cap);
        if (!grown) return -1;
        *buf = grown;
        *cap = next_cap;
    }
}

static int append_markdown_indent_http(char** buf, size_t* cap, size_t* used, int depth) {
    for (int i = 0; i < depth; i++) {
        if (append_textf_http(buf, cap, used, "  ") != 0) {
            return -1;
        }
    }
    return 0;
}

static int append_cjson_markdown_http(char** buf, size_t* cap, size_t* used, cJSON* node, int depth) {
    if (!node) {
        return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
               append_textf_http(buf, cap, used, "- null\n") == 0 ? 0 : -1;
    }

    if (cJSON_IsObject(node)) {
        cJSON* child = NULL;
        cJSON_ArrayForEach(child, node) {
            const char* key = child->string ? child->string : "item";
            if (cJSON_IsString(child)) {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- **%s:** %s\n", key,
                                      child->valuestring ? child->valuestring : "") != 0) {
                    return -1;
                }
            } else if (cJSON_IsBool(child)) {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- **%s:** %s\n", key,
                                      cJSON_IsTrue(child) ? "true" : "false") != 0) {
                    return -1;
                }
            } else if (cJSON_IsNumber(child)) {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- **%s:** %g\n", key, child->valuedouble) != 0) {
                    return -1;
                }
            } else if (cJSON_IsNull(child)) {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- **%s:** null\n", key) != 0) {
                    return -1;
                }
            } else {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- **%s:**\n", key) != 0) {
                    return -1;
                }
                if (append_cjson_markdown_http(buf, cap, used, child, depth + 1) != 0) {
                    return -1;
                }
            }
        }
        return 0;
    }

    if (cJSON_IsArray(node)) {
        int n = cJSON_GetArraySize(node);
        for (int i = 0; i < n; i++) {
            cJSON* item = cJSON_GetArrayItem(node, i);
            if (cJSON_IsString(item)) {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- %s\n", item->valuestring ? item->valuestring : "") != 0) {
                    return -1;
                }
            } else if (cJSON_IsBool(item)) {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- %s\n", cJSON_IsTrue(item) ? "true" : "false") != 0) {
                    return -1;
                }
            } else if (cJSON_IsNumber(item)) {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- %g\n", item->valuedouble) != 0) {
                    return -1;
                }
            } else if (cJSON_IsNull(item)) {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "- null\n") != 0) {
                    return -1;
                }
            } else {
                if (append_markdown_indent_http(buf, cap, used, depth) != 0 ||
                    append_textf_http(buf, cap, used, "-\n") != 0) {
                    return -1;
                }
                if (append_cjson_markdown_http(buf, cap, used, item, depth + 1) != 0) {
                    return -1;
                }
            }
        }
        return 0;
    }

    if (cJSON_IsString(node)) {
        return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
               append_textf_http(buf, cap, used, "- %s\n", node->valuestring ? node->valuestring : "") == 0 ? 0 : -1;
    }
    if (cJSON_IsBool(node)) {
        return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
               append_textf_http(buf, cap, used, "- %s\n", cJSON_IsTrue(node) ? "true" : "false") == 0 ? 0 : -1;
    }
    if (cJSON_IsNumber(node)) {
        return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
               append_textf_http(buf, cap, used, "- %g\n", node->valuedouble) == 0 ? 0 : -1;
    }

    return append_markdown_indent_http(buf, cap, used, depth) == 0 &&
           append_textf_http(buf, cap, used, "- null\n") == 0 ? 0 : -1;
}

static char* format_result_markdown_http(const char* raw_result) {
    if (!raw_result) {
        return strdup("No output.");
    }

    cJSON* root = cJSON_Parse(raw_result);
    if (!root) {
        return strdup(raw_result);
    }

    size_t cap = strlen(raw_result) * 2U + 512U;
    if (cap < 2048U) cap = 2048U;
    size_t used = 0U;
    char* out = (char*)malloc(cap);
    if (!out) {
        cJSON_Delete(root);
        return strdup(raw_result);
    }
    out[0] = '\0';

    if (append_cjson_markdown_http(&out, &cap, &used, root, 0) != 0) {
        cJSON_Delete(root);
        free(out);
        return strdup(raw_result);
    }

    cJSON_Delete(root);
    return out;
}

static char* build_slash_help_all_json_local(void) {
    size_t cap = 2048U;
    size_t used = 0U;
    char* out = (char*)malloc(cap);
    if (!out) return NULL;
    out[0] = '\0';

    if (append_textf_http(&out, &cap, &used, "AVAILABLE TOOLS\n") != 0) {
        free(out);
        return NULL;
    }

    char* tool_list_json = tools_execute(g_api_ctx.tools_ctx, "tool_list", "{}");
    cJSON* tool_root = tool_list_json ? cJSON_Parse(tool_list_json) : NULL;
    cJSON* tools = tool_root ? cJSON_GetObjectItemCaseSensitive(tool_root, "tools") : NULL;

    if (!tools || !cJSON_IsArray(tools) || cJSON_GetArraySize(tools) <= 0) {
        (void)append_textf_http(&out, &cap, &used, "- (none)\n");
    } else {
        int tn = cJSON_GetArraySize(tools);
        int* used_idx = (int*)calloc((size_t)tn, sizeof(int));
        if (!used_idx) {
            cJSON_Delete(tool_root);
            free(tool_list_json);
            free(out);
            return NULL;
        }

        for (int k = 0; k < tn; k++) {
            int best = -1;
            const char* best_name = NULL;

            for (int i = 0; i < tn; i++) {
                if (used_idx[i]) continue;
                cJSON* row = cJSON_GetArrayItem(tools, i);
                cJSON* name = row ? cJSON_GetObjectItemCaseSensitive(row, "name") : NULL;
                const char* name_s = (name && cJSON_IsString(name) && name->valuestring) ? name->valuestring : "unknown";
                if (best < 0 || strcmp(name_s, best_name) < 0) {
                    best = i;
                    best_name = name_s;
                }
            }

            if (best < 0) {
                break;
            }

            used_idx[best] = 1;
            cJSON* row = cJSON_GetArrayItem(tools, best);
            cJSON* name = row ? cJSON_GetObjectItemCaseSensitive(row, "name") : NULL;
            cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
            const char* name_s = (name && cJSON_IsString(name) && name->valuestring) ? name->valuestring : "unknown";
            const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
            if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", name_s, desc_s[0] ? " — " : "", desc_s) != 0) {
                free(used_idx);
                cJSON_Delete(tool_root);
                free(tool_list_json);
                free(out);
                return NULL;
            }
        }

        free(used_idx);
    }

    cJSON_Delete(tool_root);
    free(tool_list_json);

    if (append_textf_http(&out, &cap, &used, "\nAVAILABLE SKILLS\n") != 0) {
        free(out);
        return NULL;
    }

    char* skill_list_json = tools_execute(g_api_ctx.tools_ctx, "skill_list", "{}");
    cJSON* skill_root = skill_list_json ? cJSON_Parse(skill_list_json) : NULL;
    cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;

    int available_count = 0;
    if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
        int sn = cJSON_GetArraySize(skills);
        for (int i = 0; i < sn; i++) {
            cJSON* row = cJSON_GetArrayItem(skills, i);
            cJSON* owner = row ? cJSON_GetObjectItemCaseSensitive(row, "owner") : NULL;
            cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
            cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
            const char* owner_s = (owner && cJSON_IsString(owner) && owner->valuestring) ? owner->valuestring : "";
            const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
            const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";

            if (strcmp(owner_s, "agent") != 0 || !d_tag_s || d_tag_s[0] == '\0') {
                continue;
            }

            if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
                cJSON_Delete(skill_root);
                free(skill_list_json);
                free(out);
                return NULL;
            }
            available_count++;
        }
    }

    if (available_count <= 0) {
        (void)append_textf_http(&out, &cap, &used, "- (none)\n");
    }

    cJSON_Delete(skill_root);
    free(skill_list_json);

    if (append_textf_http(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) {
        free(out);
        return NULL;
    }

    char* adopted_json = tools_execute(g_api_ctx.tools_ctx, "adopted_skills", "{}");
    cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL;
    cJSON* adoption_events_content = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "content") : NULL;
    cJSON* adoption_events_json = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "adoption_events_json") : NULL;
    const char* adoption_events_text = NULL;
    if (adoption_events_content && cJSON_IsString(adoption_events_content) && adoption_events_content->valuestring) {
        adoption_events_text = adoption_events_content->valuestring;
    } else if (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring) {
        adoption_events_text = adoption_events_json->valuestring;
    }
    cJSON* adoption_events = adoption_events_text ? cJSON_Parse(adoption_events_text) : NULL;

    int adopted_count = 0;
    if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
        cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
        cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
        if (tags && cJSON_IsArray(tags)) {
            int tn = cJSON_GetArraySize(tags);
            for (int i = 0; i < tn; i++) {
                cJSON* tag = cJSON_GetArrayItem(tags, i);
                if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
                    continue;
                }

                cJSON* k = cJSON_GetArrayItem(tag, 0);
                cJSON* v = cJSON_GetArrayItem(tag, 1);
                if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
                    !cJSON_IsString(v) || !v->valuestring) {
                    continue;
                }

                if (strcmp(k->valuestring, "a") != 0) {
                    continue;
                }

                const char* addr = v->valuestring;
                const char* d_tag = strrchr(addr, ':');
                const char* label = (d_tag && d_tag[1] != '\0') ? (d_tag + 1) : addr;

                if (append_textf_http(&out, &cap, &used, "- %s\n", label) != 0) {
                    cJSON_Delete(adoption_events);
                    cJSON_Delete(adopted_root);
                    free(adopted_json);
                    free(out);
                    return NULL;
                }
                adopted_count++;
            }
        }
    }

    if (adopted_count <= 0) {
        (void)append_textf_http(&out, &cap, &used, "- (none)\n");
    }

    cJSON_Delete(adoption_events);
    cJSON_Delete(adopted_root);
    free(adopted_json);
    return out;
}

static char* build_slash_help_tool_json_local(const char* tool_name) {
    if (!tool_name || tool_name[0] == '\0') {
        return json_error_string_local("missing tool name");
    }

    char* schema_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx);
    if (!schema_json) return json_error_string_local("failed to build tool schema");

    cJSON* schema = cJSON_Parse(schema_json);
    free(schema_json);
    if (!schema || !cJSON_IsArray(schema)) {
        cJSON_Delete(schema);
        return json_error_string_local("tool schema parse failure");
    }

    cJSON* matched_fn = NULL;
    int n = cJSON_GetArraySize(schema);
    for (int i = 0; i < n; i++) {
        cJSON* item = cJSON_GetArrayItem(schema, i);
        cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL;
        cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL;
        if (name && cJSON_IsString(name) && name->valuestring && strcmp(name->valuestring, tool_name) == 0) {
            matched_fn = fn;
            break;
        }
    }

    if (!matched_fn) {
        cJSON_Delete(schema);
        return json_error_string_local("unknown tool");
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        cJSON_Delete(schema);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", 1);
    cJSON_AddStringToObject(out, "mode", "slash_help_tool");
    cJSON_AddStringToObject(out, "tool", tool_name);
    cJSON_AddItemToObject(out, "schema", cJSON_Duplicate(matched_fn, 1));

    char* result = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    cJSON_Delete(schema);
    return result;
}

static char* execute_slash_command_http(const char* message) {
    if (!message || message[0] != '/') return NULL;

    const char* p = message + 1;
    while (*p && !is_space_char_local(*p)) p++;

    size_t tool_len = (size_t)(p - (message + 1));
    if (tool_len == 0 || tool_len >= 128U) {
        return strdup("{\"success\":false,\"error\":\"invalid slash command\"}");
    }

    char tool_name[128];
    memcpy(tool_name, message + 1, tool_len);
    tool_name[tool_len] = '\0';

    const char* raw_args = skip_spaces_local(p);

    char* result_json = NULL;
    if (raw_args && (strcmp(raw_args, "-h") == 0 || strcmp(raw_args, "--help") == 0)) {
        result_json = build_slash_help_tool_json_local(tool_name);
    } else if (strcmp(tool_name, "help") == 0) {
        if (!raw_args || raw_args[0] == '\0') {
            result_json = build_slash_help_all_json_local();
        } else {
            char help_tool[128];
            size_t i = 0;
            while (raw_args[i] && !is_space_char_local(raw_args[i]) && i < sizeof(help_tool) - 1U) {
                help_tool[i] = raw_args[i];
                i++;
            }
            help_tool[i] = '\0';
            result_json = build_slash_help_tool_json_local(help_tool);
        }
    } else {
        char* args_json = build_slash_args_json_local(raw_args);
        if (!args_json) {
            result_json = json_error_string_local("failed to parse slash args");
        } else {
            result_json = tools_execute(g_api_ctx.tools_ctx, tool_name, args_json);
            free(args_json);
        }
    }

    if (!result_json) {
        result_json = strdup("{\"success\":false,\"error\":\"direct tool execution failed\"}");
    }

    char* markdown_result = format_result_markdown_http(result_json);
    const char* dm_payload = markdown_result ? markdown_result : result_json;

    size_t log_cap = strlen(message) + strlen(result_json ? result_json : "") + strlen(dm_payload ? dm_payload : "") + 192U;
    char* log_payload = (char*)malloc(log_cap);
    if (log_payload) {
        snprintf(log_payload, log_cap, "slash=%s\nresult_json=%s\nresult_markdown=%s",
                 message,
                 result_json ? result_json : "",
                 dm_payload ? dm_payload : "");
        agent_append_context_log("http_api_agent", "direct_tool_exec", log_payload);
        free(log_payload);
    }

    free(result_json);
    return markdown_result ? markdown_result : strdup("Command executed with no output.");
}

static cJSON* run_prompt_with_tools_convo(cJSON* convo,
                                          int max_turns,
                                          const char* log_sender,
                                          const char* log_phase,
                                          const char* tool_limit_message) {
    if (!convo || !cJSON_IsArray(convo)) return NULL;

    int max_ceiling = 16;
    if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_max_turns_ceiling > 0) {
        max_ceiling = g_api_ctx.cfg->tools.api_max_turns_ceiling;
    }

    if (max_turns < 1) max_turns = 1;
    if (max_turns > max_ceiling) max_turns = max_ceiling;

    char* tools_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx);
    if (!tools_json) {
        return NULL;
    }

    cJSON* root = cJSON_CreateObject();
    cJSON* turns = cJSON_CreateArray();
    if (!root || !turns) {
        cJSON_Delete(root);
        cJSON_Delete(turns);
        free(tools_json);
        return NULL;
    }

    int stall_repeat_threshold = 3;
    if (g_api_ctx.cfg && g_api_ctx.cfg->tools.stall_repeat_threshold > 1) {
        stall_repeat_threshold = g_api_ctx.cfg->tools.stall_repeat_threshold;
    }

    char* final_response = NULL;
    uint64_t last_tool_fp = 0;
    int repeated_tool_turns = 0;
    int exited_on_stall = 0;
    int stall_due_to_length = 0;
    int turns_run = 0;

    for (int turn = 0; turn < max_turns; turn++) {
        turns_run = turn + 1;
        char* messages_json = cJSON_PrintUnformatted(convo);
        if (!messages_json) break;

        agent_append_context_log(log_sender ? log_sender : "http_api",
                                 log_phase ? log_phase : "llm_chat_with_tools_messages_http_api",
                                 messages_json);

        llm_response_t resp;
        int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
        free(messages_json);

        if (rc != 0) {
            final_response = strdup("LLM request failed.");
            break;
        }

        cJSON* turn_obj = cJSON_CreateObject();
        if (turn_obj) {
            cJSON_AddNumberToObject(turn_obj, "turn", turn + 1);
            cJSON* tool_calls = cJSON_CreateArray();
            if (tool_calls) cJSON_AddItemToObject(turn_obj, "tool_calls", tool_calls);
            cJSON_AddItemToArray(turns, turn_obj);
        }

        if (resp.tool_call_count <= 0) {
            final_response = strdup(resp.content ? resp.content : "No response content.");
            llm_response_free(&resp);
            break;
        }

        uint64_t current_tool_fp = tool_calls_fingerprint_local(&resp);
        if (turn == 0 || current_tool_fp != last_tool_fp) {
            repeated_tool_turns = 1;
            last_tool_fp = current_tool_fp;
        } else {
            repeated_tool_turns++;
        }

        if (repeated_tool_turns >= stall_repeat_threshold) {
            exited_on_stall = 1;
            stall_due_to_length = (resp.finish_reason && strcmp(resp.finish_reason, "length") == 0) ? 1 : 0;
            llm_response_free(&resp);
            break;
        }

        if (append_assistant_tool_calls_message_local(convo, &resp) != 0) {
            llm_response_free(&resp);
            final_response = strdup("Failed to append assistant tool calls.");
            break;
        }

        cJSON* current_turn = cJSON_GetArrayItem(turns, cJSON_GetArraySize(turns) - 1);
        cJSON* turn_tool_calls = current_turn ? cJSON_GetObjectItemCaseSensitive(current_turn, "tool_calls") : NULL;

        for (int i = 0; i < resp.tool_call_count; i++) {
            llm_tool_call_t* tc = &resp.tool_calls[i];
            char* tool_result = tools_execute(g_api_ctx.tools_ctx, tc->name, tc->arguments_json);
            if (!tool_result) {
                tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
            }

            if (turn_tool_calls && cJSON_IsArray(turn_tool_calls)) {
                cJSON* tr = cJSON_CreateObject();
                if (tr) {
                    cJSON_AddStringToObject(tr, "name", tc->name ? tc->name : "");
                    cJSON_AddStringToObject(tr, "arguments", tc->arguments_json ? tc->arguments_json : "{}");
                    cJSON_AddStringToObject(tr, "result", tool_result ? tool_result : "{}");
                    cJSON_AddItemToArray(turn_tool_calls, tr);
                }
            }

            if (append_tool_result_message_local(convo,
                                                 tc->id ? tc->id : "",
                                                 tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
                free(tool_result);
                llm_response_free(&resp);
                final_response = strdup("Failed to append tool result.");
                goto done;
            }
            free(tool_result);
        }

        llm_response_free(&resp);
    }

done:;
    int hit_max_turn_limit = (!final_response && !exited_on_stall && turns_run >= max_turns);

    if (!final_response) {
        char* messages_json = cJSON_PrintUnformatted(convo);
        if (messages_json) {
            llm_response_t final_resp;
            int final_rc = llm_chat_with_tools_messages(messages_json, tools_json, "none", &final_resp);
            free(messages_json);
            if (final_rc == 0) {
                const char* forced_answer = final_resp.content ? final_resp.content : "";
                if (forced_answer[0] != '\0') {
                    final_response = strdup(forced_answer);
                }
                llm_response_free(&final_resp);
            }
        }
    }

    if (!final_response) {
        final_response = strdup(exited_on_stall
                                    ? "Stopped repeated tool calls after detecting a loop, and no final summary was produced."
                                    : (tool_limit_message ? tool_limit_message : "I hit my tool-use limit for this prompt run."));
    }

    if (exited_on_stall || hit_max_turn_limit) {
        notify_admin_limit_diagnostic_http("http_api_tool_loop",
                                           exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted",
                                           log_sender,
                                           max_turns,
                                           turns_run,
                                           stall_repeat_threshold,
                                           repeated_tool_turns,
                                           g_api_ctx.cfg ? g_api_ctx.cfg->llm.max_tokens : 0,
                                           (exited_on_stall && stall_due_to_length) ? "max_tokens_truncation" : NULL,
                                           (exited_on_stall && stall_due_to_length) ? "Increase max_tokens (example: /model_set {\"max_tokens\": 4096})" : NULL,
                                           final_response ? final_response : "");
    }

    cJSON_AddBoolToObject(root, "success", 1);
    cJSON_AddStringToObject(root, "final_response", final_response ? final_response : "");
    cJSON_AddItemToObject(root, "turns", turns);

    llm_config_t cfg;
    if (llm_get_config(&cfg) == 0) {
        cJSON_AddStringToObject(root, "model_used", cfg.model);
    }

    int input_chars = 0;
    char* convo_json = cJSON_PrintUnformatted(convo);
    if (convo_json) {
        input_chars = (int)strlen(convo_json);
        free(convo_json);
    }
    cJSON_AddNumberToObject(root, "total_input_tokens_estimate", estimate_tokens_from_chars(input_chars));
    cJSON_AddNumberToObject(root,
                            "total_output_tokens_estimate",
                            estimate_tokens_from_chars((int)strlen(final_response ? final_response : "")));

    free(final_response);
    free(tools_json);
    return root;
}

static cJSON* run_prompt_with_tools(cJSON* body) {
    if (!body || !cJSON_IsObject(body)) return NULL;

    cJSON* messages_node = cJSON_GetObjectItemCaseSensitive(body, "messages");
    if (!messages_node || !cJSON_IsArray(messages_node)) return NULL;

    cJSON* convo = cJSON_Duplicate(messages_node, 1);
    if (!convo || !cJSON_IsArray(convo)) {
        cJSON_Delete(convo);
        return NULL;
    }

    int max_turns = 4;
    if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) {
        max_turns = g_api_ctx.cfg->tools.api_default_max_turns;
    }
    cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
    if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
        max_turns = (int)max_turns_node->valuedouble;
    }

    cJSON* root = run_prompt_with_tools_convo(convo,
                                              max_turns,
                                              "http_api",
                                              "llm_chat_with_tools_messages_http_api",
                                              "I hit my tool-use limit for this prompt run.");
    cJSON_Delete(convo);
    return root;
}

static void handle_status(struct mg_connection* c) {
    cJSON* root = cJSON_CreateObject();
    if (!root) {
        reply_error(c, 500, "oom");
        return;
    }

    cJSON_AddBoolToObject(root, "success", 1);
    cJSON_AddStringToObject(root, "name", DIDACTYL_NAME);
    cJSON_AddStringToObject(root, "version", DIDACTYL_VERSION);

    if (g_api_ctx.cfg) {
        cJSON_AddStringToObject(root, "pubkey", g_api_ctx.cfg->keys.public_key_hex);
        cJSON_AddNumberToObject(root, "relay_count", g_api_ctx.cfg->relay_count);
    }

    cJSON_AddNumberToObject(root, "connected_relays", nostr_handler_connected_relay_count());
    if (g_api_ctx.trigger_manager) {
        cJSON_AddNumberToObject(root, "active_triggers", trigger_manager_active_count(g_api_ctx.trigger_manager));
    }

    reply_json(c, 200, root);
    cJSON_Delete(root);
}

static void handle_context_current(struct mg_connection* c) {
    char* messages_json = NULL;
    if (agent_build_admin_messages_json("", DIDACTYL_SENDER_ADMIN, &messages_json) != 0 || !messages_json) {
        reply_error(c, 500, "failed to build context messages");
        return;
    }

    cJSON* messages = cJSON_Parse(messages_json);
    cJSON* root = cJSON_CreateObject();
    if (!root || !messages || !cJSON_IsArray(messages)) {
        free(messages_json);
        cJSON_Delete(messages);
        cJSON_Delete(root);
        reply_error(c, 500, "failed to serialize context");
        return;
    }

    int chars = (int)strlen(messages_json);
    cJSON_AddBoolToObject(root, "success", 1);
    cJSON_AddNumberToObject(root, "total_chars", chars);
    cJSON_AddNumberToObject(root, "total_estimated_tokens", estimate_tokens_from_chars(chars));
    cJSON_AddItemToObject(root, "messages", messages);

    reply_json(c, 200, root);

    free(messages_json);
    cJSON_Delete(root);
}

static void handle_context_parts(struct mg_connection* c) {
    cJSON* root = cJSON_CreateObject();
    if (!root) {
        reply_error(c, 500, "oom");
        return;
    }

    if (build_context_parts_response(root) != 0) {
        cJSON_Delete(root);
        reply_error(c, 500, "failed to build context parts");
        return;
    }

    reply_json(c, 200, root);
    cJSON_Delete(root);
}

static void handle_prompt_run_simple(struct mg_connection* c, const struct mg_http_message* hm) {
    cJSON* body = parse_body_json(hm);
    if (!body || !cJSON_IsObject(body)) {
        cJSON_Delete(body);
        reply_error(c, 400, "invalid JSON body");
        return;
    }

    cJSON* system = cJSON_GetObjectItemCaseSensitive(body, "system");
    cJSON* user = cJSON_GetObjectItemCaseSensitive(body, "user");
    if (!system || !user || !cJSON_IsString(system) || !cJSON_IsString(user) ||
        !system->valuestring || !user->valuestring) {
        cJSON_Delete(body);
        reply_error(c, 400, "system and user strings are required");
        return;
    }

    llm_config_t old_cfg;
    int overridden = 0;
    char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
    if (model_err) {
        cJSON_Delete(body);
        reply_error(c, 400, model_err);
        return;
    }

    char* response = llm_chat(system->valuestring, user->valuestring);
    maybe_model_override_end(&old_cfg, overridden);

    if (!response) {
        cJSON_Delete(body);
        reply_error(c, 500, "llm request failed");
        return;
    }

    cJSON* root = cJSON_CreateObject();
    if (!root) {
        free(response);
        cJSON_Delete(body);
        reply_error(c, 500, "oom");
        return;
    }

    cJSON_AddBoolToObject(root, "success", 1);
    cJSON_AddStringToObject(root, "response", response);

    llm_config_t cfg;
    if (llm_get_config(&cfg) == 0) {
        cJSON_AddStringToObject(root, "model_used", cfg.model);
    }

    int in_chars = (int)strlen(system->valuestring) + (int)strlen(user->valuestring);
    cJSON_AddNumberToObject(root, "input_tokens_estimate", estimate_tokens_from_chars(in_chars));
    cJSON_AddNumberToObject(root, "output_tokens_estimate", estimate_tokens_from_chars((int)strlen(response)));

    free(response);
    cJSON_Delete(body);
    reply_json(c, 200, root);
    cJSON_Delete(root);
}

static void handle_prompt_run(struct mg_connection* c, const struct mg_http_message* hm) {
    cJSON* body = parse_body_json(hm);
    if (!body || !cJSON_IsObject(body)) {
        cJSON_Delete(body);
        reply_error(c, 400, "invalid JSON body");
        return;
    }

    llm_config_t old_cfg;
    int overridden = 0;
    char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
    if (model_err) {
        cJSON_Delete(body);
        reply_error(c, 400, model_err);
        return;
    }

    cJSON* result = run_prompt_with_tools(body);
    maybe_model_override_end(&old_cfg, overridden);
    cJSON_Delete(body);

    if (!result) {
        reply_error(c, 500, "failed to run prompt");
        return;
    }

    reply_json(c, 200, result);
    cJSON_Delete(result);
}

static void handle_prompt_agent(struct mg_connection* c, const struct mg_http_message* hm) {
    cJSON* body = parse_body_json(hm);
    if (!body || !cJSON_IsObject(body)) {
        cJSON_Delete(body);
        reply_error(c, 400, "invalid JSON body");
        return;
    }

    cJSON* message = cJSON_GetObjectItemCaseSensitive(body, "message");
    if (!message || !cJSON_IsString(message) || !message->valuestring) {
        cJSON_Delete(body);
        reply_error(c, 400, "message string is required");
        return;
    }

    char* slash_result = execute_slash_command_http(message->valuestring);
    if (slash_result) {
        cJSON* out = cJSON_CreateObject();
        cJSON* turns = cJSON_CreateArray();
        if (!out || !turns) {
            free(slash_result);
            cJSON_Delete(out);
            cJSON_Delete(turns);
            cJSON_Delete(body);
            reply_error(c, 500, "oom");
            return;
        }
        cJSON_AddBoolToObject(out, "success", 1);
        cJSON_AddStringToObject(out, "final_response", slash_result);
        cJSON_AddItemToObject(out, "turns", turns);
        cJSON_AddNumberToObject(out, "total_input_tokens_estimate", estimate_tokens_from_chars((int)strlen(message->valuestring)));
        cJSON_AddNumberToObject(out, "total_output_tokens_estimate", estimate_tokens_from_chars((int)strlen(slash_result)));
        free(slash_result);
        cJSON_Delete(body);
        reply_json(c, 200, out);
        cJSON_Delete(out);
        return;
    }

    int max_turns = 4;
    if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) {
        max_turns = g_api_ctx.cfg->tools.api_default_max_turns;
    }
    cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns");
    if (max_turns_node && cJSON_IsNumber(max_turns_node)) {
        max_turns = (int)max_turns_node->valuedouble;
    }

    llm_config_t old_cfg;
    int overridden = 0;
    char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden);
    if (model_err) {
        cJSON_Delete(body);
        reply_error(c, 400, model_err);
        return;
    }

    char* base_messages_json = NULL;
    cJSON* convo = NULL;
    cJSON* result = NULL;

    if (agent_build_admin_messages_json(message->valuestring,
                                        DIDACTYL_SENDER_ADMIN,
                                        &base_messages_json) != 0 || !base_messages_json) {
        maybe_model_override_end(&old_cfg, overridden);
        cJSON_Delete(body);
        reply_error(c, 500, "failed to build agent context");
        return;
    }

    convo = cJSON_Parse(base_messages_json);
    free(base_messages_json);
    if (!convo || !cJSON_IsArray(convo)) {
        cJSON_Delete(convo);
        maybe_model_override_end(&old_cfg, overridden);
        cJSON_Delete(body);
        reply_error(c, 500, "failed to initialize conversation messages");
        return;
    }

    if (append_simple_message_local(convo, "user", message->valuestring) != 0) {
        cJSON_Delete(convo);
        maybe_model_override_end(&old_cfg, overridden);
        cJSON_Delete(body);
        reply_error(c, 500, "failed to append user message");
        return;
    }

    cJSON* live_user_msg = cJSON_GetArrayItem(convo, cJSON_GetArraySize(convo) - 1);
    if (live_user_msg && cJSON_IsObject(live_user_msg)) {
        cJSON_AddNumberToObject(live_user_msg, "_ts", (double)time(NULL));
    }

    result = run_prompt_with_tools_convo(convo,
                                         max_turns,
                                         "http_api_agent",
                                         "llm_chat_with_tools_messages_agent_api",
                                         "I hit my tool-use limit for this request.");
    cJSON_Delete(convo);

    maybe_model_override_end(&old_cfg, overridden);
    cJSON_Delete(body);

    if (!result) {
        reply_error(c, 500, "failed to run prompt");
        return;
    }

    reply_json(c, 200, result);
    cJSON_Delete(result);
}

static cJSON* run_variant(cJSON* variant) {
    if (!variant || !cJSON_IsObject(variant)) return NULL;
    cJSON* body_copy = cJSON_Duplicate(variant, 1);
    if (!body_copy) return NULL;

    llm_config_t old_cfg;
    int overridden = 0;
    char* model_err = maybe_model_override_begin(body_copy, &old_cfg, &overridden);
    if (model_err) {
        cJSON* err = cJSON_CreateObject();
        if (err) {
            cJSON_AddBoolToObject(err, "success", 0);
            cJSON_AddStringToObject(err, "error", model_err);
        }
        cJSON_Delete(body_copy);
        return err;
    }

    cJSON* out = run_prompt_with_tools(body_copy);
    maybe_model_override_end(&old_cfg, overridden);
    cJSON_Delete(body_copy);
    return out;
}

static void handle_prompt_compare(struct mg_connection* c, const struct mg_http_message* hm) {
    cJSON* body = parse_body_json(hm);
    if (!body || !cJSON_IsObject(body)) {
        cJSON_Delete(body);
        reply_error(c, 400, "invalid JSON body");
        return;
    }

    cJSON* a = cJSON_GetObjectItemCaseSensitive(body, "variant_a");
    cJSON* b = cJSON_GetObjectItemCaseSensitive(body, "variant_b");
    if (!a || !b || !cJSON_IsObject(a) || !cJSON_IsObject(b)) {
        cJSON_Delete(body);
        reply_error(c, 400, "variant_a and variant_b objects are required");
        return;
    }

    cJSON* out_a = run_variant(a);
    cJSON* out_b = run_variant(b);

    if (!out_a || !out_b) {
        cJSON_Delete(out_a);
        cJSON_Delete(out_b);
        cJSON_Delete(body);
        reply_error(c, 500, "failed to run one or more variants");
        return;
    }

    cJSON* root = cJSON_CreateObject();
    if (!root) {
        cJSON_Delete(out_a);
        cJSON_Delete(out_b);
        cJSON_Delete(body);
        reply_error(c, 500, "oom");
        return;
    }

    cJSON_AddBoolToObject(root, "success", 1);
    cJSON_AddItemToObject(root, "variant_a", out_a);
    cJSON_AddItemToObject(root, "variant_b", out_b);

    cJSON_Delete(body);
    reply_json(c, 200, root);
    cJSON_Delete(root);
}

static void http_handler(struct mg_connection* c, int ev, void* ev_data) {
    if (ev == MG_EV_ACCEPT) {
        if (g_api_tls_enabled) {
            struct mg_tls_opts opts;
            memset(&opts, 0, sizeof(opts));
            opts.cert = g_tls_cert;
            opts.key = g_tls_key;
            mg_tls_init(c, &opts);
        }
        return;
    }

    if (ev != MG_EV_HTTP_MSG) {
        return;
    }

    struct mg_http_message* hm = (struct mg_http_message*)ev_data;

    if (method_is(hm, "OPTIONS")) {
        mg_http_reply(c, 204, k_json_headers, "");
        return;
    }

    if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/status"), NULL)) {
        handle_status(c);
        return;
    }
    if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/current"), NULL)) {
        handle_context_current(c);
        return;
    }
    if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/parts"), NULL)) {
        handle_context_parts(c);
        return;
    }
    if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run-simple"), NULL)) {
        handle_prompt_run_simple(c, hm);
        return;
    }
    if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run"), NULL)) {
        handle_prompt_run(c, hm);
        return;
    }
    if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/agent"), NULL)) {
        handle_prompt_agent(c, hm);
        return;
    }
    if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/compare"), NULL)) {
        handle_prompt_compare(c, hm);
        return;
    }
    if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/trigger/*"), NULL)) {
        char d_tag[TRIGGER_SKILL_D_TAG_MAX] = {0};
        if (uri_extract_after_prefix(&hm->uri, "/api/trigger/", d_tag, sizeof(d_tag)) != 0) {
            reply_error(c, 400, "missing or invalid d_tag");
            return;
        }
        handle_trigger_webhook(c, hm, d_tag);
        return;
    }

    reply_error(c, 404, "not found");
}

int http_api_init(const http_api_context_t* ctx) {
    if (!ctx || !ctx->cfg || !ctx->tools_ctx) {
        return -1;
    }

    g_api_ctx = *ctx;

    mg_mgr_init(&g_mgr);

    g_api_tls_enabled = 0;
    g_tls_cert = mg_file_read(&mg_fs_posix, "/home/teknari/.ssl_for_local_servers/cert.pem");
    g_tls_key = mg_file_read(&mg_fs_posix, "/home/teknari/.ssl_for_local_servers/key.pem");
    if (g_tls_cert.buf && g_tls_cert.len > 0 && g_tls_key.buf && g_tls_key.len > 0) {
        g_api_tls_enabled = 1;
    } else {
        if (g_tls_cert.buf) mg_free((void*)g_tls_cert.buf);
        if (g_tls_key.buf) mg_free((void*)g_tls_key.buf);
        g_tls_cert.buf = NULL;
        g_tls_cert.len = 0;
        g_tls_key.buf = NULL;
        g_tls_key.len = 0;
    }

    char listen_url[320];
    snprintf(listen_url,
             sizeof(listen_url),
             "%s://%s:%d",
             g_api_tls_enabled ? "https" : "http",
             g_api_ctx.cfg->api.bind_address[0] ? g_api_ctx.cfg->api.bind_address : "127.0.0.1",
             g_api_ctx.cfg->api.port > 0 ? g_api_ctx.cfg->api.port : 8484);

    g_listener = mg_http_listen(&g_mgr, listen_url, http_handler, NULL);
    if (!g_listener) {
        mg_mgr_free(&g_mgr);
        memset(&g_mgr, 0, sizeof(g_mgr));
        return -1;
    }

    g_api_initialized = 1;
    DEBUG_INFO("[didactyl] http api listening on %s", listen_url);
    return 0;
}

int http_api_poll(int timeout_ms) {
    if (!g_api_initialized) {
        return 0;
    }

    if (timeout_ms < 0) timeout_ms = 0;
    mg_mgr_poll(&g_mgr, timeout_ms);
    return 0;
}

void http_api_cleanup(void) {
    if (!g_api_initialized) {
        return;
    }

    mg_mgr_free(&g_mgr);
    memset(&g_mgr, 0, sizeof(g_mgr));
    g_listener = NULL;

    if (g_tls_cert.buf) mg_free((void*)g_tls_cert.buf);
    if (g_tls_key.buf) mg_free((void*)g_tls_key.buf);
    g_tls_cert.buf = NULL;
    g_tls_cert.len = 0;
    g_tls_key.buf = NULL;
    g_tls_key.len = 0;
    g_api_tls_enabled = 0;

    memset(&g_api_ctx, 0, sizeof(g_api_ctx));
    g_api_initialized = 0;
}