#define _POSIX_C_SOURCE 200809L

#include "agent.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <pthread.h>
#include <stdarg.h>
#include <ctype.h>

#include "llm.h"
#include "nostr_handler.h"
#include "trigger_manager.h"
#include "tools/tools.h"
#include "prompt_template.h"
#include "cjson/cJSON.h"
#include "debug.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"

static didactyl_config_t* g_cfg = NULL;
static tools_context_t g_tools_ctx;
static struct trigger_manager* g_trigger_manager = NULL;

#define AGENT_CONTEXT_PART_NAMES_MAX 512
static char* g_context_part_names[AGENT_CONTEXT_PART_NAMES_MAX];
static int g_context_part_names_count = 0;
static pthread_mutex_t g_context_part_names_mutex = PTHREAD_MUTEX_INITIALIZER;

#define AGENT_DEBOUNCE_WINDOW_SECONDS 5
#define AGENT_DEBOUNCE_CACHE_SIZE 256
#define AGENT_HISTORY_TURNS 12
#define AGENT_HISTORY_QUERY_LIMIT 200
#define AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS 300
#define AGENT_ADOPTED_SKILLS_MAX 24
#define AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS 1800
#define AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS 7200

typedef struct {
    uint64_t fingerprint;
    time_t seen_at;
} agent_seen_msg_t;

typedef struct {
    int kind;
    char author_pubkey_hex[65];
    char d_tag[65];
    char* content;
    int has_trigger;
    trigger_type_t trigger_type;
    char filter_json[TRIGGER_FILTER_JSON_MAX];
} agent_adopted_skill_t;

static agent_seen_msg_t g_seen_msgs[AGENT_DEBOUNCE_CACHE_SIZE];
static int g_seen_msgs_count = 0;
static int g_seen_msgs_next = 0;
static pthread_mutex_t g_seen_msgs_mutex = PTHREAD_MUTEX_INITIALIZER;

static agent_adopted_skill_t g_adopted_skills[AGENT_ADOPTED_SKILLS_MAX];
static int g_adopted_skills_count = 0;
static time_t g_adopted_skills_last_refresh_at = 0;
static pthread_mutex_t g_adopted_skills_mutex = PTHREAD_MUTEX_INITIALIZER;

static int refresh_adopted_skills_cache_if_needed(void);

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

static uint64_t message_fingerprint(const char* sender_pubkey_hex, const char* message) {
    uint64_t a = fnv1a64(sender_pubkey_hex);
    uint64_t b = fnv1a64(message);
    return a ^ (b + 0x9e3779b97f4a7c15ULL + (a << 6) + (a >> 2));
}

static uint64_t tool_calls_fingerprint(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(tc->name ? tc->name : "");
        uint64_t args_h = fnv1a64(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(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_cfg || g_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_cfg->admin.pubkey, diag);
}

static int agent_message_is_debounced(const char* sender_pubkey_hex, const char* message) {
    time_t now = time(NULL);
    uint64_t fp = message_fingerprint(sender_pubkey_hex, message);
    int duplicate = 0;

    pthread_mutex_lock(&g_seen_msgs_mutex);

    for (int i = 0; i < g_seen_msgs_count; i++) {
        if (g_seen_msgs[i].fingerprint == fp &&
            g_seen_msgs[i].seen_at > 0 &&
            (now - g_seen_msgs[i].seen_at) <= AGENT_DEBOUNCE_WINDOW_SECONDS) {
            duplicate = 1;
            break;
        }
    }

    if (!duplicate) {
        int slot = 0;
        if (g_seen_msgs_count < AGENT_DEBOUNCE_CACHE_SIZE) {
            slot = g_seen_msgs_count;
            g_seen_msgs_count++;
        } else {
            slot = g_seen_msgs_next;
            g_seen_msgs_next = (g_seen_msgs_next + 1) % AGENT_DEBOUNCE_CACHE_SIZE;
        }
        g_seen_msgs[slot].fingerprint = fp;
        g_seen_msgs[slot].seen_at = now;
    }

    pthread_mutex_unlock(&g_seen_msgs_mutex);
    return duplicate;
}

static int append_simple_message(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(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(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 void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload);

static char* local_json_error(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 int is_space_char(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(*s)) s++;
    return s ? s : "";
}

static char* build_slash_args_json(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_local(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_local(char** buf, size_t* cap, size_t* used, int depth) {
    for (int i = 0; i < depth; i++) {
        if (append_textf_local(buf, cap, used, "  ") != 0) {
            return -1;
        }
    }
    return 0;
}

static int append_cjson_markdown_local(char** buf, size_t* cap, size_t* used, cJSON* node, int depth) {
    if (!node) {
        return append_markdown_indent_local(buf, cap, used, depth) == 0 &&
               append_textf_local(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_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- **%s:** %s\n", key,
                                       child->valuestring ? child->valuestring : "") != 0) {
                    return -1;
                }
            } else if (cJSON_IsBool(child)) {
                if (append_markdown_indent_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- **%s:** %s\n", key,
                                       cJSON_IsTrue(child) ? "true" : "false") != 0) {
                    return -1;
                }
            } else if (cJSON_IsNumber(child)) {
                if (append_markdown_indent_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- **%s:** %g\n", key, child->valuedouble) != 0) {
                    return -1;
                }
            } else if (cJSON_IsNull(child)) {
                if (append_markdown_indent_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- **%s:** null\n", key) != 0) {
                    return -1;
                }
            } else {
                if (append_markdown_indent_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- **%s:**\n", key) != 0) {
                    return -1;
                }
                if (append_cjson_markdown_local(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_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- %s\n", item->valuestring ? item->valuestring : "") != 0) {
                    return -1;
                }
            } else if (cJSON_IsBool(item)) {
                if (append_markdown_indent_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- %s\n", cJSON_IsTrue(item) ? "true" : "false") != 0) {
                    return -1;
                }
            } else if (cJSON_IsNumber(item)) {
                if (append_markdown_indent_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- %g\n", item->valuedouble) != 0) {
                    return -1;
                }
            } else if (cJSON_IsNull(item)) {
                if (append_markdown_indent_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "- null\n") != 0) {
                    return -1;
                }
            } else {
                if (append_markdown_indent_local(buf, cap, used, depth) != 0 ||
                    append_textf_local(buf, cap, used, "-\n") != 0) {
                    return -1;
                }
                if (append_cjson_markdown_local(buf, cap, used, item, depth + 1) != 0) {
                    return -1;
                }
            }
        }
        return 0;
    }

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

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

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

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

    if (cJSON_IsObject(root)) {
        cJSON* content = cJSON_GetObjectItemCaseSensitive(root, "content");
        if (content && cJSON_IsString(content) && content->valuestring) {
            char* direct = strdup(content->valuestring);
            cJSON_Delete(root);
            return direct ? direct : 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_local(&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(void) {
    size_t cap = 2048U;
    size_t used = 0U;
    char* out = (char*)malloc(cap);
    if (!out) return NULL;
    out[0] = '\0';

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

    if (append_textf_local(&out, &cap, &used, "(run /<tool> -h for full JSON args template)\n") != 0) {
        free(out);
        return NULL;
    }

    char* tool_list_json = tools_execute(&g_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_local(&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_local(&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_local(&out, &cap, &used, "\nAVAILABLE SKILLS\n") != 0) {
        free(out);
        return NULL;
    }

    char* skill_list_json = tools_execute(&g_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_local(&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_local(&out, &cap, &used, "- (none)\n");
    }

    cJSON_Delete(skill_root);
    free(skill_list_json);

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

    char* adopted_json = tools_execute(&g_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_local(&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_local(&out, &cap, &used, "- (none)\n");
    }

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

static cJSON* build_example_value_for_type_local(const char* type_s) {
    if (!type_s || type_s[0] == '\0') return cJSON_CreateString("<value>");
    if (strcmp(type_s, "string") == 0) return cJSON_CreateString("<string>");
    if (strcmp(type_s, "integer") == 0) return cJSON_CreateNumber(0);
    if (strcmp(type_s, "number") == 0) return cJSON_CreateNumber(0.0);
    if (strcmp(type_s, "boolean") == 0) return cJSON_CreateBool(0);
    if (strcmp(type_s, "array") == 0) return cJSON_CreateArray();
    if (strcmp(type_s, "object") == 0) return cJSON_CreateObject();
    return cJSON_CreateString("<value>");
}

static cJSON* build_example_args_from_parameters_local(cJSON* params) {
    cJSON* example = cJSON_CreateObject();
    if (!example) return NULL;

    cJSON* props = params ? cJSON_GetObjectItemCaseSensitive(params, "properties") : NULL;
    if (!props || !cJSON_IsObject(props)) {
        return example;
    }

    cJSON* prop = NULL;
    cJSON_ArrayForEach(prop, props) {
        if (!prop->string || prop->string[0] == '\0') continue;

        cJSON* enum_vals = cJSON_GetObjectItemCaseSensitive(prop, "enum");
        if (enum_vals && cJSON_IsArray(enum_vals) && cJSON_GetArraySize(enum_vals) > 0) {
            cJSON* first = cJSON_GetArrayItem(enum_vals, 0);
            cJSON* dup = cJSON_Duplicate(first, 1);
            if (dup) {
                cJSON_AddItemToObject(example, prop->string, dup);
                continue;
            }
        }

        cJSON* type = cJSON_GetObjectItemCaseSensitive(prop, "type");
        const char* type_s = (type && cJSON_IsString(type) && type->valuestring) ? type->valuestring : NULL;
        cJSON* val = build_example_value_for_type_local(type_s);
        if (!val) val = cJSON_CreateString("<value>");
        if (!val) {
            cJSON_Delete(example);
            return NULL;
        }
        cJSON_AddItemToObject(example, prop->string, val);
    }

    return example;
}

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

    char* schema_json = tools_build_openai_schema_json(&g_tools_ctx);
    if (!schema_json) {
        return local_json_error("failed to build tool schema");
    }

    cJSON* schema = cJSON_Parse(schema_json);
    free(schema_json);
    if (!schema || !cJSON_IsArray(schema)) {
        cJSON_Delete(schema);
        return local_json_error("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 local_json_error("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));

    cJSON* params = cJSON_GetObjectItemCaseSensitive(matched_fn, "parameters");
    cJSON* example_args = build_example_args_from_parameters_local(params);
    if (example_args) {
        char* example_json = cJSON_PrintUnformatted(example_args);
        if (example_json) {
            cJSON_AddStringToObject(out, "example_args_json", example_json);

            size_t usage_len = strlen(tool_name) + strlen(example_json) + 4U;
            char* usage = (char*)malloc(usage_len);
            if (usage) {
                snprintf(usage, usage_len, "/%s %s", tool_name, example_json);
                cJSON_AddStringToObject(out, "example_slash", usage);
                free(usage);
            }

            free(example_json);
        }
        cJSON_Delete(example_args);
    }

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

static int handle_slash_command(const char* sender_pubkey_hex, const char* message) {
    if (!sender_pubkey_hex || !message || message[0] != '/') {
        return 0;
    }

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

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

    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(tool_name);
    } else if (strcmp(tool_name, "help") == 0) {
        if (!raw_args || raw_args[0] == '\0') {
            result_json = build_slash_help_all_json();
        } else {
            char help_tool[128];
            size_t i = 0;
            while (raw_args[i] && !is_space_char(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(help_tool);
        }
    } else {
        char* args_json = build_slash_args_json(raw_args);
        if (!args_json) {
            result_json = local_json_error("failed to parse slash args");
        } else {
            result_json = tools_execute(&g_tools_ctx, tool_name, args_json);
            free(args_json);
        }
    }

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

    size_t tool_name_len = strlen(tool_name);
    int send_raw_json = (tool_name_len >= 4U && strcmp(tool_name + tool_name_len - 4U, "_get") == 0);

    char* markdown_result = send_raw_json ? NULL : format_result_markdown_local(result_json);
    const char* dm_payload = send_raw_json ? result_json : (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 : "");
        append_context_log(sender_pubkey_hex, "direct_tool_exec", log_payload);
        free(log_payload);
    }

    (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex,
                                               dm_payload ? dm_payload : "Command executed with no output.",
                                               DIDACTYL_DM_HISTORY_TOOL_RESPONSE);
    free(markdown_result);
    free(result_json);
    return 1;
}

static const char* get_context_part_name_copy(int idx);

static const char* detect_context_section(const char* role, const char* content, int idx) {
    const char* c = content ? content : "";
    const char* r = role ? role : "";

    if (idx == 0 && strcmp(r, "system") == 0) return "system_prompt";

    if (strncmp(c, "Administrator Context", 21) == 0) {
        return "admin_context";
    }
    if (strncmp(c, "Agent identity", 14) == 0 ||
        strncmp(c, "Agent Identity", 14) == 0) {
        return "agent_identity";
    }
    if (strncmp(c, "Sender verification", 19) == 0 ||
        strncmp(c, "This message has been cryptographically verified", 47) == 0 ||
        strncmp(c, "This message is from a web-of-trust contact", 43) == 0) {
        return "sender_context";
    }
    if (strncmp(c, "Startup events memory", 21) == 0 ||
        strncmp(c, "Startup Events Memory", 21) == 0) {
        return "startup_events";
    }
    if (strncmp(c, "Adopted skills memory", 21) == 0 ||
        strncmp(c, "Adopted Skills Memory", 21) == 0) {
        return "adopted_skills";
    }
    if (strncmp(c, "### Current Tasks", 17) == 0) {
        return "agent_tasks";
    }
    if (strncmp(c, "Administrator recent public notes", 33) == 0) {
        return "admin_notes";
    }

    if (strcmp(r, "user") == 0 || strcmp(r, "assistant") == 0) {
        return "dm_history";
    }

    return "context_part";
}

const char* agent_classify_message_part(cJSON* msg, int idx) {
    const char* mapped = get_context_part_name_copy(idx);
    if (mapped) {
        return mapped;
    }

    cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
    cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
    const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
    const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
    return detect_context_section(role_s, content_s, idx);
}

static void clear_context_part_names_locked(void) {
    for (int i = 0; i < g_context_part_names_count; i++) {
        free(g_context_part_names[i]);
        g_context_part_names[i] = NULL;
    }
    g_context_part_names_count = 0;
}

static void clear_context_part_names(void) {
    pthread_mutex_lock(&g_context_part_names_mutex);
    clear_context_part_names_locked();
    pthread_mutex_unlock(&g_context_part_names_mutex);
}

static void set_context_part_name(int idx, const char* name) {
    if (idx < 0 || idx >= AGENT_CONTEXT_PART_NAMES_MAX) return;

    pthread_mutex_lock(&g_context_part_names_mutex);
    if (idx >= g_context_part_names_count) {
        for (int i = g_context_part_names_count; i <= idx && i < AGENT_CONTEXT_PART_NAMES_MAX; i++) {
            g_context_part_names[i] = NULL;
        }
        g_context_part_names_count = idx + 1;
    }

    free(g_context_part_names[idx]);
    g_context_part_names[idx] = strdup(name ? name : "context_part");
    pthread_mutex_unlock(&g_context_part_names_mutex);
}

static const char* get_context_part_name_copy(int idx) {
    static __thread char tls_name[128];
    tls_name[0] = '\0';

    pthread_mutex_lock(&g_context_part_names_mutex);
    if (idx >= 0 && idx < g_context_part_names_count && g_context_part_names[idx]) {
        snprintf(tls_name, sizeof(tls_name), "%s", g_context_part_names[idx]);
    }
    pthread_mutex_unlock(&g_context_part_names_mutex);

    return tls_name[0] ? tls_name : NULL;
}

static __attribute__((unused)) void template_emit_hook(const char* section_name, int message_index, void* user_data) {
    (void)user_data;
    set_context_part_name(message_index, section_name ? section_name : "context_part");
}

static void append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
    if (!phase) {
        return;
    }
    if (strcmp(phase, "llm_chat_with_tools_messages") != 0 &&
        strcmp(phase, "llm_trigger_with_tools_messages") != 0) {
        return;
    }

    time_t now = time(NULL);
    struct tm tm_info;
    localtime_r(&now, &tm_info);
    char timestamp[32] = {0};
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", &tm_info);

    const char* safe_phase = phase ? phase : "unknown";
    const char* safe_sender = sender_pubkey_hex ? sender_pubkey_hex : "unknown";
    const char* safe_payload = context_payload ? context_payload : "";
    size_t context_bytes = context_payload ? strlen(context_payload) : 0U;
    size_t approx_tokens = context_bytes / 4U;

    llm_config_t active_llm_cfg = {0};
    const char* safe_model = (g_cfg && g_cfg->llm.model[0] != '\0') ? g_cfg->llm.model : "unknown";
    if (llm_get_config(&active_llm_cfg) == 0 && active_llm_cfg.model[0] != '\0') {
        safe_model = active_llm_cfg.model;
    }

    int entry_len = snprintf(NULL,
                             0,
                             "```text\n"
                             "Context Log - seen by model\n"
                             "timestamp=%s\n"
                             "phase=%s\n"
                             "sender=%s\n"
                             "model=%s\n"
                             "context_bytes=%zu\n"
                             "approx_tokens=%zu\n"
                             "```\n\n"
                             "%s\n\n---\n\n",
                             timestamp,
                             safe_phase,
                             safe_sender,
                             safe_model,
                             context_bytes,
                             approx_tokens,
                             safe_payload);
    if (entry_len < 0) {
        return;
    }

    char* entry = (char*)malloc((size_t)entry_len + 1U);
    if (!entry) {
        return;
    }

    snprintf(entry,
             (size_t)entry_len + 1U,
             "```text\n"
             "Context Log - seen by model\n"
             "timestamp=%s\n"
             "phase=%s\n"
             "sender=%s\n"
             "model=%s\n"
             "context_bytes=%zu\n"
             "approx_tokens=%zu\n"
             "```\n\n"
             "%s\n\n---\n\n",
             timestamp,
             safe_phase,
             safe_sender,
             safe_model,
             context_bytes,
             approx_tokens,
             safe_payload);

    char* old_data = NULL;
    size_t old_len = 0;

    FILE* in = fopen("context.log.md", "rb");
    if (in) {
        if (fseek(in, 0, SEEK_END) == 0) {
            long sz = ftell(in);
            if (sz > 0 && fseek(in, 0, SEEK_SET) == 0) {
                old_len = (size_t)sz;
                old_data = (char*)malloc(old_len);
                if (old_data) {
                    size_t n = fread(old_data, 1, old_len, in);
                    if (n != old_len) {
                        free(old_data);
                        old_data = NULL;
                        old_len = 0;
                    }
                } else {
                    old_len = 0;
                }
            }
        }
        fclose(in);
    }

    FILE* out = fopen("context.log.md", "wb");
    if (!out) {
        free(old_data);
        free(entry);
        return;
    }

    (void)fwrite(entry, 1, (size_t)entry_len, out);
    if (old_data && old_len > 0) {
        (void)fwrite(old_data, 1, old_len, out);
    }

    fclose(out);
    free(old_data);
    free(entry);
}

void agent_append_context_log(const char* sender_pubkey_hex, const char* phase, const char* context_payload) {
    append_context_log(sender_pubkey_hex, phase, context_payload);
}


static __attribute__((unused)) char* build_sender_verification_text(didactyl_sender_tier_t sender_tier) {
    if (sender_tier == DIDACTYL_SENDER_ADMIN) {
        return strdup("This message has been cryptographically verified as coming from your administrator.");
    }
    if (sender_tier == DIDACTYL_SENDER_WOT) {
        return strdup("This message is from a web-of-trust contact (not the administrator).");
    }
    return strdup("Sender tier is unknown.");
}


static __attribute__((unused)) int append_recent_admin_dm_history(cJSON* messages, const char* current_message) {
    if (!messages || !g_cfg) {
        return -1;
    }

    char* history_json = nostr_handler_get_dm_history_json(g_cfg->admin.pubkey, AGENT_HISTORY_TURNS);
    if (!history_json) {
        return 0;
    }

    cJSON* items = cJSON_Parse(history_json);
    free(history_json);
    if (!items || !cJSON_IsArray(items)) {
        cJSON_Delete(items);
        return 0;
    }

    const char* last_appended_role = NULL;
    const char* last_appended_content = NULL;

    int n = cJSON_GetArraySize(items);
    for (int i = 0; i < n; i++) {
        cJSON* item = cJSON_GetArrayItem(items, i);
        cJSON* role = item ? cJSON_GetObjectItemCaseSensitive(item, "role") : NULL;
        cJSON* content = item ? cJSON_GetObjectItemCaseSensitive(item, "content") : NULL;
        cJSON* created_at = item ? cJSON_GetObjectItemCaseSensitive(item, "created_at") : NULL;

        if (!role || !content || !cJSON_IsString(role) || !cJSON_IsString(content) ||
            !role->valuestring || !content->valuestring) {
            continue;
        }

        const char* role_s = role->valuestring;
        const char* content_s = content->valuestring;
        int role_is_user = (strcmp(role_s, "user") == 0);
        if (!role_is_user && strcmp(role_s, "assistant") != 0) {
            continue;
        }

        if (i == n - 1 && role_is_user && current_message && strcmp(content_s, current_message) == 0) {
            continue;
        }

        if (last_appended_role && last_appended_content &&
            strcmp(last_appended_role, role_s) == 0 &&
            strcmp(last_appended_content, content_s) == 0) {
            continue;
        }

        if (append_simple_message(messages, role_s, content_s) != 0) {
            cJSON_Delete(items);
            return -1;
        }

        cJSON* appended = cJSON_GetArrayItem(messages, cJSON_GetArraySize(messages) - 1);
        if (appended && cJSON_IsObject(appended) && created_at && cJSON_IsNumber(created_at)) {
            cJSON_AddNumberToObject(appended, "_ts", created_at->valuedouble);
        }

        last_appended_role = role_s;
        last_appended_content = content_s;
    }

    cJSON_Delete(items);
    return 0;
}

static cJSON* find_tag_value_string_local(cJSON* tags, const char* key) {
    if (!tags || !key || !cJSON_IsArray(tags)) {
        return NULL;
    }

    int n = cJSON_GetArraySize(tags);
    for (int i = 0; i < n; 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) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) {
            continue;
        }

        if (strcmp(k->valuestring, key) == 0) {
            return v;
        }
    }

    return NULL;
}

static int parse_skill_address_tag_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_d_tag[65]) {
    if (!addr || !out_kind || !out_pubkey_hex || !out_d_tag) {
        return -1;
    }

    const char* p1 = strchr(addr, ':');
    if (!p1) return -1;
    const char* p2 = strchr(p1 + 1, ':');
    if (!p2) return -1;

    char kind_buf[16] = {0};
    size_t kind_len = (size_t)(p1 - addr);
    if (kind_len == 0 || kind_len >= sizeof(kind_buf)) {
        return -1;
    }
    memcpy(kind_buf, addr, kind_len);

    int kind = atoi(kind_buf);
    if (kind != 31123 && kind != 31124) {
        return -1;
    }

    size_t pub_len = (size_t)(p2 - (p1 + 1));
    if (pub_len != 64U) {
        return -1;
    }
    memcpy(out_pubkey_hex, p1 + 1, 64U);
    out_pubkey_hex[64] = '\0';

    size_t d_tag_len = strlen(p2 + 1);
    if (d_tag_len == 0 || d_tag_len >= 65U) {
        return -1;
    }
    memcpy(out_d_tag, p2 + 1, d_tag_len + 1U);

    *out_kind = kind;
    return 0;
}

static int dm_filter_matches_tier_local(const char* filter_json, didactyl_sender_tier_t tier) {
    if (!filter_json || filter_json[0] == '\0') {
        return 0;
    }

    cJSON* root = cJSON_Parse(filter_json);
    if (!root || !cJSON_IsObject(root)) {
        cJSON_Delete(root);
        return 0;
    }

    cJSON* from = cJSON_GetObjectItemCaseSensitive(root, "from");
    const char* from_s = (from && cJSON_IsString(from) && from->valuestring) ? from->valuestring : "admin";

    int match = 0;
    if (strcmp(from_s, "any") == 0) {
        match = 1;
    } else if (strcmp(from_s, "admin") == 0) {
        match = (tier == DIDACTYL_SENDER_ADMIN);
    } else if (strcmp(from_s, "wot") == 0) {
        match = (tier == DIDACTYL_SENDER_WOT);
    }

    cJSON_Delete(root);
    return match;
}

static const char* adopted_skill_content_lookup_by_d_tag_locked(const char* d_tag) {
    if (!d_tag || d_tag[0] == '\0') {
        return NULL;
    }

    for (int i = 0; i < g_adopted_skills_count; i++) {
        if (strcmp(g_adopted_skills[i].d_tag, d_tag) == 0) {
            return g_adopted_skills[i].content;
        }
    }

    return NULL;
}

static const char* template_skill_lookup_callback(void* user_data, const char* d_tag) {
    (void)user_data;
    if (!d_tag || d_tag[0] == '\0') {
        return NULL;
    }

    (void)refresh_adopted_skills_cache_if_needed();

    const char* out = NULL;
    pthread_mutex_lock(&g_adopted_skills_mutex);
    out = adopted_skill_content_lookup_by_d_tag_locked(d_tag);
    pthread_mutex_unlock(&g_adopted_skills_mutex);
    return out;
}

static char* resolve_skill_references_local(const char* input) {
    return prompt_template_resolve_inline_variables(input ? input : "", &g_tools_ctx);
}

static char* build_context_from_triggers(trigger_type_t trigger_type,
                                         const char* trigger_filter,
                                         cJSON* trigger_event,
                                         const char* relay_url) {
    (void)relay_url;
    (void)trigger_filter;

    if (!g_cfg) {
        return strdup("You are an AI agent. Respond to the message.");
    }

    (void)refresh_adopted_skills_cache_if_needed();

    size_t cap = 4096;
    size_t used = 0;
    char* out = (char*)malloc(cap);
    if (!out) {
        return NULL;
    }
    out[0] = '\0';

    int matched = 0;

    didactyl_sender_tier_t tier = DIDACTYL_SENDER_STRANGER;
    if (trigger_event && cJSON_IsObject(trigger_event)) {
        cJSON* tier_j = cJSON_GetObjectItemCaseSensitive(trigger_event, "tier");
        const char* tier_s = (tier_j && cJSON_IsString(tier_j) && tier_j->valuestring) ? tier_j->valuestring : "stranger";
        if (strcmp(tier_s, "admin") == 0) tier = DIDACTYL_SENDER_ADMIN;
        else if (strcmp(tier_s, "wot") == 0) tier = DIDACTYL_SENDER_WOT;
    }

    char* matched_skill_contents[AGENT_ADOPTED_SKILLS_MAX];
    int matched_skill_count = 0;
    memset(matched_skill_contents, 0, sizeof(matched_skill_contents));

    pthread_mutex_lock(&g_adopted_skills_mutex);
    for (int i = 0; i < g_adopted_skills_count && matched_skill_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
        const agent_adopted_skill_t* s = &g_adopted_skills[i];
        if (!s->has_trigger || s->trigger_type != trigger_type) {
            continue;
        }
        if (trigger_type == TRIGGER_TYPE_DM && !dm_filter_matches_tier_local(s->filter_json, tier)) {
            continue;
        }

        matched_skill_contents[matched_skill_count] = strdup(s->content ? s->content : "");
        if (matched_skill_contents[matched_skill_count]) {
            matched_skill_count++;
        }
    }
    pthread_mutex_unlock(&g_adopted_skills_mutex);

    for (int i = 0; i < matched_skill_count; i++) {
        char* expanded = resolve_skill_references_local(matched_skill_contents[i] ? matched_skill_contents[i] : "");
        const char* skill_text = expanded ? expanded : (matched_skill_contents[i] ? matched_skill_contents[i] : "");

        size_t need = strlen("\n\n---\n\n") + strlen(skill_text) + 1U;
        if (used + need >= cap) {
            size_t next = cap;
            while (used + need >= next) next *= 2U;
            char* grown = (char*)realloc(out, next);
            if (!grown) {
                free(expanded);
                for (int j = i; j < matched_skill_count; j++) {
                    free(matched_skill_contents[j]);
                }
                free(out);
                return NULL;
            }
            out = grown;
            cap = next;
        }

        if (matched > 0) {
            memcpy(out + used, "\n\n---\n\n", 7);
            used += 7;
        }
        size_t sl = strlen(skill_text);
        memcpy(out + used, skill_text, sl);
        used += sl;
        out[used] = '\0';
        matched++;

        free(expanded);
        free(matched_skill_contents[i]);
    }

    if (matched == 0) {
        const char* fallback = "You are an AI agent. Respond to the message.";
        size_t need = strlen(fallback) + 1U;
        if (need > cap) {
            char* grown = (char*)realloc(out, need);
            if (!grown) {
                free(out);
                return NULL;
            }
            out = grown;
            cap = need;
        }
        memcpy(out, fallback, need);
        used = need - 1U;
    }

    return out;
}

static void clear_adopted_skills_cache_locked(void) {
    for (int i = 0; i < g_adopted_skills_count; i++) {
        free(g_adopted_skills[i].content);
        g_adopted_skills[i].content = NULL;
        g_adopted_skills[i].kind = 0;
        g_adopted_skills[i].author_pubkey_hex[0] = '\0';
        g_adopted_skills[i].d_tag[0] = '\0';
        g_adopted_skills[i].has_trigger = 0;
        g_adopted_skills[i].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
        g_adopted_skills[i].filter_json[0] = '\0';
    }
    g_adopted_skills_count = 0;
}

static int refresh_adopted_skills_cache_if_needed(void) {
    if (!g_cfg) {
        return -1;
    }

    time_t now = time(NULL);
    pthread_mutex_lock(&g_adopted_skills_mutex);
    int cache_fresh = (g_adopted_skills_last_refresh_at > 0) &&
                      ((now - g_adopted_skills_last_refresh_at) < AGENT_ADOPTED_SKILLS_CACHE_TTL_SECONDS);
    pthread_mutex_unlock(&g_adopted_skills_mutex);

    if (cache_fresh) {
        return 0;
    }

    agent_adopted_skill_t tmp[AGENT_ADOPTED_SKILLS_MAX];
    memset(tmp, 0, sizeof(tmp));
    int tmp_count = 0;

    char* adoption_json = nostr_handler_get_self_events_by_kind_json(10123);
    cJSON* adoption_events = adoption_json ? cJSON_Parse(adoption_json) : NULL;
    free(adoption_json);

    if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
        cJSON* list_event = cJSON_GetArrayItem(adoption_events, 0);
        cJSON* list_tags = list_event ? cJSON_GetObjectItemCaseSensitive(list_event, "tags") : NULL;

        if (list_tags && cJSON_IsArray(list_tags)) {
            int tn = cJSON_GetArraySize(list_tags);
            for (int i = 0; i < tn && tmp_count < AGENT_ADOPTED_SKILLS_MAX; i++) {
                cJSON* tag = cJSON_GetArrayItem(list_tags, i);
                if (!tag || !cJSON_IsArray(tag)) {
                    continue;
                }

                cJSON* key = cJSON_GetArrayItem(tag, 0);
                cJSON* val = cJSON_GetArrayItem(tag, 1);
                if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) ||
                    !key->valuestring || !val->valuestring || strcmp(key->valuestring, "a") != 0) {
                    continue;
                }

                int skill_kind = 0;
                char skill_author[65] = {0};
                char skill_d_tag[65] = {0};
                if (parse_skill_address_tag_local(val->valuestring, &skill_kind, skill_author, skill_d_tag) != 0) {
                    continue;
                }

                cJSON* skill_events = NULL;
                if (strcmp(skill_author, g_cfg->keys.public_key_hex) == 0) {
                    char* skill_json = nostr_handler_get_self_events_by_kind_json(skill_kind);
                    if (skill_json) {
                        cJSON* all_events = cJSON_Parse(skill_json);
                        free(skill_json);
                        if (all_events && cJSON_IsArray(all_events)) {
                            skill_events = cJSON_CreateArray();
                            if (skill_events) {
                                int all_n = cJSON_GetArraySize(all_events);
                                for (int ai = 0; ai < all_n; ai++) {
                                    cJSON* ev = cJSON_GetArrayItem(all_events, ai);
                                    cJSON* ev_pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
                                    cJSON* ev_tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
                                    cJSON* ev_d = find_tag_value_string_local(ev_tags, "d");
                                    if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
                                        strcmp(ev_pubkey->valuestring, skill_author) != 0 ||
                                        !ev_d || !cJSON_IsString(ev_d) || !ev_d->valuestring ||
                                        strcmp(ev_d->valuestring, skill_d_tag) != 0) {
                                        continue;
                                    }
                                    cJSON* dup = cJSON_Duplicate(ev, 1);
                                    if (dup) {
                                        cJSON_AddItemToArray(skill_events, dup);
                                    }
                                }
                            }
                        }
                        cJSON_Delete(all_events);
                    }
                } else {
                    cJSON* skill_filter = cJSON_CreateObject();
                    cJSON* sk_kinds = cJSON_CreateArray();
                    cJSON* sk_authors = cJSON_CreateArray();
                    cJSON* d_values = cJSON_CreateArray();
                    if (!skill_filter || !sk_kinds || !sk_authors || !d_values) {
                        cJSON_Delete(skill_filter);
                        cJSON_Delete(sk_kinds);
                        cJSON_Delete(sk_authors);
                        cJSON_Delete(d_values);
                        continue;
                    }

                    cJSON_AddItemToArray(sk_kinds, cJSON_CreateNumber(skill_kind));
                    cJSON_AddItemToObject(skill_filter, "kinds", sk_kinds);
                    cJSON_AddItemToArray(sk_authors, cJSON_CreateString(skill_author));
                    cJSON_AddItemToObject(skill_filter, "authors", sk_authors);
                    cJSON_AddItemToArray(d_values, cJSON_CreateString(skill_d_tag));
                    cJSON_AddItemToObject(skill_filter, "#d", d_values);
                    cJSON_AddNumberToObject(skill_filter, "limit", 1);

                    char* skill_json = nostr_handler_query_json(skill_filter, 2000);
                    cJSON_Delete(skill_filter);
                    if (skill_json) {
                        skill_events = cJSON_Parse(skill_json);
                        free(skill_json);
                    }
                }

                if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
                    cJSON_Delete(skill_events);
                    continue;
                }

                cJSON* skill_event = cJSON_GetArrayItem(skill_events, 0);
                cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
                cJSON* skill_tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL;
                if (!content || !cJSON_IsString(content) || !content->valuestring || !skill_tags || !cJSON_IsArray(skill_tags)) {
                    cJSON_Delete(skill_events);
                    continue;
                }

                cJSON* enabled_tag = find_tag_value_string_local(skill_tags, "enabled");
                const char* enabled_s = (enabled_tag && cJSON_IsString(enabled_tag) && enabled_tag->valuestring)
                                        ? enabled_tag->valuestring
                                        : "true";
                int is_enabled = !(strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0);
                if (!is_enabled) {
                    cJSON_Delete(skill_events);
                    continue;
                }

                tmp[tmp_count].kind = skill_kind;
                snprintf(tmp[tmp_count].author_pubkey_hex, sizeof(tmp[tmp_count].author_pubkey_hex), "%s", skill_author);
                snprintf(tmp[tmp_count].d_tag, sizeof(tmp[tmp_count].d_tag), "%s", skill_d_tag);
                tmp[tmp_count].content = strdup(content->valuestring);
                tmp[tmp_count].has_trigger = 0;
                tmp[tmp_count].trigger_type = TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
                tmp[tmp_count].filter_json[0] = '\0';
                cJSON* trigger_tag = find_tag_value_string_local(skill_tags, "trigger");
                cJSON* filter_tag = find_tag_value_string_local(skill_tags, "filter");
                if (trigger_tag && cJSON_IsString(trigger_tag) && trigger_tag->valuestring && trigger_tag->valuestring[0] != '\0' &&
                    filter_tag && cJSON_IsString(filter_tag) && filter_tag->valuestring && filter_tag->valuestring[0] != '\0') {
                    tmp[tmp_count].has_trigger = 1;
                    tmp[tmp_count].trigger_type = trigger_type_from_string(trigger_tag->valuestring);
                    snprintf(tmp[tmp_count].filter_json, sizeof(tmp[tmp_count].filter_json), "%s", filter_tag->valuestring);
                }
                if (tmp[tmp_count].content) {
                    tmp_count++;
                }

                cJSON_Delete(skill_events);
            }
        }
    }

    cJSON_Delete(adoption_events);

    pthread_mutex_lock(&g_adopted_skills_mutex);
    clear_adopted_skills_cache_locked();
    for (int i = 0; i < tmp_count; i++) {
        g_adopted_skills[g_adopted_skills_count] = tmp[i];
        tmp[i].content = NULL;
        g_adopted_skills_count++;
    }
    g_adopted_skills_last_refresh_at = now;
    pthread_mutex_unlock(&g_adopted_skills_mutex);

    for (int i = 0; i < tmp_count; i++) {
        free(tmp[i].content);
    }

    return 0;
}

static char* flatten_skill_content_to_text(const char* skill_content) {
    if (!skill_content || skill_content[0] == '\0') {
        return strdup("");
    }

    cJSON* root = cJSON_Parse(skill_content);
    if (!root || !cJSON_IsObject(root)) {
        cJSON_Delete(root);
        return strdup(skill_content);
    }

    cJSON* name = cJSON_GetObjectItemCaseSensitive(root, "name");
    cJSON* description = cJSON_GetObjectItemCaseSensitive(root, "description");
    cJSON* event_kind = cJSON_GetObjectItemCaseSensitive(root, "event_kind");
    cJSON* procedure = cJSON_GetObjectItemCaseSensitive(root, "procedure");

    size_t cap = strlen(skill_content) + 512U;
    char* out = (char*)malloc(cap);
    if (!out) {
        cJSON_Delete(root);
        return NULL;
    }
    out[0] = '\0';

    if (name && cJSON_IsString(name) && name->valuestring) {
        snprintf(out + strlen(out), cap - strlen(out), "Name: %s\n", name->valuestring);
    }
    if (description && cJSON_IsString(description) && description->valuestring) {
        snprintf(out + strlen(out), cap - strlen(out), "Description: %s\n", description->valuestring);
    }
    if (event_kind && cJSON_IsNumber(event_kind)) {
        snprintf(out + strlen(out), cap - strlen(out), "Event kind: %d\n", (int)event_kind->valuedouble);
    }

    if (procedure && cJSON_IsArray(procedure) && cJSON_GetArraySize(procedure) > 0) {
        strcat(out, "Procedure:\n");
        int pn = cJSON_GetArraySize(procedure);
        for (int i = 0; i < pn; i++) {
            cJSON* step = cJSON_GetArrayItem(procedure, i);
            if (!step || !cJSON_IsString(step) || !step->valuestring) continue;
            snprintf(out + strlen(out), cap - strlen(out), "  %d. %s\n", i + 1, step->valuestring);
        }
    }

    if (out[0] == '\0') {
        snprintf(out, cap, "%s", skill_content);
    }

    cJSON_Delete(root);
    return out;
}

static __attribute__((unused)) int append_adopted_skills_context(cJSON* messages) {
    if (!messages || !g_cfg) {
        return -1;
    }

    (void)refresh_adopted_skills_cache_if_needed();

    pthread_mutex_lock(&g_adopted_skills_mutex);
    int cached_count = g_adopted_skills_count;
    if (cached_count <= 0) {
        pthread_mutex_unlock(&g_adopted_skills_mutex);
        return 0;
    }

    size_t capacity = (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS + 2048U;
    char* payload = (char*)malloc(capacity);
    if (!payload) {
        pthread_mutex_unlock(&g_adopted_skills_mutex);
        return -1;
    }

    size_t used = 0;
    int n = snprintf(payload,
                     capacity,
                     "## Adopted Skills Memory (source: adoption list + startup fallback)\n\nAdopted skills memory (always-on behavioral instructions): follow these skill instructions when relevant and never violate explicit constraints.\n");
    if (n < 0) {
        free(payload);
        pthread_mutex_unlock(&g_adopted_skills_mutex);
        return -1;
    }
    used = (size_t)n < capacity ? (size_t)n : capacity - 1U;

    size_t total_skill_chars = 0;
    int included = 0;
    for (int i = 0; i < cached_count && used + 64U < capacity; i++) {
        char* flattened_skill = flatten_skill_content_to_text(g_adopted_skills[i].content ? g_adopted_skills[i].content : "");
        const char* skill_content = flattened_skill ? flattened_skill : (g_adopted_skills[i].content ? g_adopted_skills[i].content : "");
        size_t content_len = strlen(skill_content);
        size_t clipped_len = content_len;

        if (clipped_len > (size_t)AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS) {
            clipped_len = (size_t)AGENT_ADOPTED_SKILL_CONTENT_MAX_CHARS;
        }

        if (total_skill_chars + clipped_len > (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS) {
            size_t remaining = (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS - total_skill_chars;
            if (remaining == 0) {
                break;
            }
            clipped_len = remaining;
        }

        int head_n = snprintf(payload + used,
                              capacity - used,
                              "\n\n---\n\n### Skill\nSkill d_tag: %s\nSkill address: %d:%s:%s\n\nInstructions:\n",
                              g_adopted_skills[i].d_tag,
                              g_adopted_skills[i].kind,
                              g_adopted_skills[i].author_pubkey_hex,
                              g_adopted_skills[i].d_tag);
        if (head_n < 0 || (size_t)head_n >= (capacity - used)) {
            break;
        }
        used += (size_t)head_n;

        size_t write_len = clipped_len;
        if (write_len >= (capacity - used)) {
            write_len = capacity - used - 1U;
        }
        memcpy(payload + used, skill_content, write_len);
        used += write_len;
        payload[used] = '\0';

        int truncated = (content_len > write_len);
        if (truncated && used + 24U < capacity) {
            int tail_n = snprintf(payload + used, capacity - used, "\n...[truncated]\n");
            if (tail_n > 0 && (size_t)tail_n < (capacity - used)) {
                used += (size_t)tail_n;
            }
        } else if (used + 1U < capacity) {
            payload[used++] = '\n';
            payload[used] = '\0';
        }

        total_skill_chars += write_len;
        included++;
        if (total_skill_chars >= (size_t)AGENT_ADOPTED_SKILLS_TOTAL_MAX_CHARS) {
            break;
        }

        free(flattened_skill);
    }

    if (included < cached_count && used + 96U < capacity) {
        int extra_n = snprintf(payload + used,
                               capacity - used,
                               "\nAdditional adopted skills omitted due to context budget limits (%d/%d included).\n",
                               included,
                               cached_count);
        if (extra_n > 0 && (size_t)extra_n < (capacity - used)) {
            used += (size_t)extra_n;
        }
    }

    pthread_mutex_unlock(&g_adopted_skills_mutex);

    int rc = append_simple_message(messages, "system", payload);
    free(payload);
    return rc;
}

static __attribute__((unused)) cJSON* build_recent_admin_dm_history_messages(const char* current_message) {
    cJSON* messages = cJSON_CreateArray();
    if (!messages) {
        return NULL;
    }
    if (append_recent_admin_dm_history(messages, current_message) != 0) {
        cJSON_Delete(messages);
        return NULL;
    }
    return messages;
}

int agent_init(didactyl_config_t* config) {
    if (!config) {
        return -1;
    }

    g_cfg = config;

    if (tools_init(&g_tools_ctx, g_cfg) != 0) {
        g_cfg = NULL;
        return -1;
    }

    memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
    g_seen_msgs_count = 0;
    g_seen_msgs_next = 0;

    pthread_mutex_lock(&g_adopted_skills_mutex);
    clear_adopted_skills_cache_locked();
    g_adopted_skills_last_refresh_at = 0;
    pthread_mutex_unlock(&g_adopted_skills_mutex);

    g_tools_ctx.template_skill_lookup = template_skill_lookup_callback;
    g_tools_ctx.template_skill_lookup_user_data = NULL;

    clear_context_part_names();

    return 0;
}

void agent_set_trigger_manager(struct trigger_manager* trigger_manager) {
    g_trigger_manager = trigger_manager;
    g_tools_ctx.trigger_manager = trigger_manager;
}

void agent_on_trigger(const char* skill_d_tag,
                      const char* skill_content,
                      cJSON* triggering_event,
                      const char* relay_url) {
    if (!g_cfg || !skill_d_tag || !skill_content || !triggering_event) {
        return;
    }

    char* event_json = cJSON_PrintUnformatted(triggering_event);
    if (!event_json) {
        return;
    }

    const char* prev_trigger_event_json = g_tools_ctx.template_trigger_event_json;
    g_tools_ctx.template_trigger_event_json = event_json;

    const char* relay = (relay_url && relay_url[0] != '\0') ? relay_url : "unknown";

    const char* trigger_prefix =
        "Triggered-skill execution context:\n"
        "- You are executing a skill because a Nostr event matched its trigger filter.\n"
        "- Execute the skill instructions below against the triggering event.\n"
        "- Keep output concise and actionable for the administrator.\n"
        "- If no action is needed, explicitly say why.\n\n"
        "Skill d_tag: ";

    char* composed_context = build_context_from_triggers(TRIGGER_TYPE_NOSTR_SUBSCRIPTION,
                                                         NULL,
                                                         triggering_event,
                                                         relay_url);
    if (!composed_context) {
        composed_context = strdup("You are an AI agent. Respond to the trigger event.");
    }

    size_t system_len = strlen(composed_context ? composed_context : "") + 2 + strlen(trigger_prefix) + strlen(skill_d_tag) +
                        strlen("\nRelay: ") + strlen(relay) + strlen("\n\nSkill instructions:\n") +
                        strlen(skill_content) + 1U;
    char* system_prompt = (char*)malloc(system_len);
    if (!system_prompt) {
        free(composed_context);
        free(event_json);
        return;
    }

    snprintf(system_prompt,
             system_len,
             "%s\n\n%s%s\nRelay: %s\n\nSkill instructions:\n%s",
             composed_context ? composed_context : "",
             trigger_prefix,
             skill_d_tag,
             relay,
             skill_content);
    free(composed_context);

    size_t user_len = strlen("Triggering event JSON:\n") + strlen(event_json) + 1U;
    char* user_prompt = (char*)malloc(user_len);
    if (!user_prompt) {
        free(system_prompt);
        free(event_json);
        return;
    }

    snprintf(user_prompt, user_len, "Triggering event JSON:\n%s", event_json);

    char* tools_json = tools_build_openai_schema_json(&g_tools_ctx);
    if (!tools_json) {
        free(system_prompt);
        free(user_prompt);
        g_tools_ctx.template_trigger_event_json = prev_trigger_event_json;
        free(event_json);
        (void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: tools unavailable.");
        return;
    }

    cJSON* messages = cJSON_CreateArray();
    if (!messages || !cJSON_IsArray(messages) ||
        append_simple_message(messages, "system", system_prompt) != 0 ||
        append_simple_message(messages, "user", user_prompt) != 0) {
        cJSON_Delete(messages);
        free(tools_json);
        free(system_prompt);
        free(user_prompt);
        g_tools_ctx.template_trigger_event_json = prev_trigger_event_json;
        free(event_json);
        (void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: prompt initialization failed.");
        return;
    }

    append_context_log(g_cfg->admin.pubkey, "llm_trigger", user_prompt);
    free(system_prompt);
    free(user_prompt);

    int max_turns = g_cfg->tools.trigger_max_turns > 0
                        ? g_cfg->tools.trigger_max_turns
                        : (g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8);

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

        append_context_log(g_cfg->admin.pubkey, "llm_trigger_with_tools_messages", 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) {
            (void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: LLM request failed.");
            cJSON_Delete(messages);
            free(tools_json);
            g_tools_ctx.template_trigger_event_json = prev_trigger_event_json;
            free(event_json);
            return;
        }

        if (resp.tool_call_count <= 0) {
            trigger_completed = 1;
            llm_response_free(&resp);
            break;
        }

        if (append_assistant_tool_calls_message(messages, &resp) != 0) {
            llm_response_free(&resp);
            break;
        }

        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_tools_ctx, tc->name, tc->arguments_json);
            if (!tool_result) {
                tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
            }

            if (append_tool_result_message(messages,
                                           tc->id ? tc->id : "",
                                           tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
                free(tool_result);
                llm_response_free(&resp);
                cJSON_Delete(messages);
                free(tools_json);
                g_tools_ctx.template_trigger_event_json = prev_trigger_event_json;
                free(event_json);
                (void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: tool result append failed.");
                return;
            }

            free(tool_result);
        }

        llm_response_free(&resp);
    }

    if (!trigger_completed && turns_run >= max_turns) {
        notify_admin_limit_diagnostic("trigger_skill_loop",
                                      "max_turns_exhausted",
                                      skill_d_tag,
                                      max_turns,
                                      turns_run,
                                      g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3,
                                      0,
                                      g_cfg->llm.max_tokens,
                                      NULL,
                                      NULL,
                                      "");
    }

    if (g_trigger_manager) {
        (void)trigger_manager_fire_chains(g_trigger_manager,
                                          skill_d_tag,
                                          triggering_event,
                                          relay_url && relay_url[0] != '\0' ? relay_url : "trigger");
    }

    cJSON_Delete(messages);
    free(tools_json);
    g_tools_ctx.template_trigger_event_json = prev_trigger_event_json;
    free(event_json);
}

int agent_build_admin_messages_json(const char* current_user_message,
                                    didactyl_sender_tier_t sender_tier,
                                    char** out_messages_json) {
    if (!out_messages_json || !g_cfg) {
        return -1;
    }

    *out_messages_json = NULL;
    clear_context_part_names();

    cJSON* dm_event = cJSON_CreateObject();
    if (!dm_event) {
        return -1;
    }
    cJSON_AddStringToObject(dm_event, "type", "dm");
    cJSON_AddStringToObject(dm_event, "sender_pubkey", g_cfg->admin.pubkey);
    cJSON_AddStringToObject(dm_event, "message", current_user_message ? current_user_message : "");
    cJSON_AddStringToObject(dm_event,
                            "tier",
                            sender_tier == DIDACTYL_SENDER_ADMIN ? "admin" :
                            (sender_tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
    cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL));

    char* composed_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "api");
    cJSON_Delete(dm_event);
    if (!composed_context) {
        composed_context = strdup("You are an AI agent. Respond to the message.");
    }

    cJSON* messages = cJSON_CreateArray();
    if (!messages ||
        append_simple_message(messages, "system", composed_context ? composed_context : "You are an AI agent. Respond to the message.") != 0 ||
        append_simple_message(messages, "user", current_user_message ? current_user_message : "") != 0) {
        free(composed_context);
        cJSON_Delete(messages);
        return -1;
    }
    free(composed_context);

    int n = cJSON_GetArraySize(messages);
    for (int i = 0; i < n; i++) {
        cJSON* msg = cJSON_GetArrayItem(messages, i);
        cJSON* role = msg ? cJSON_GetObjectItemCaseSensitive(msg, "role") : NULL;
        cJSON* content = msg ? cJSON_GetObjectItemCaseSensitive(msg, "content") : NULL;
        const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "";
        const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
        set_context_part_name(i, detect_context_section(role_s, content_s, i));
    }

    char* messages_json = cJSON_PrintUnformatted(messages);
    cJSON_Delete(messages);
    if (!messages_json) {
        return -1;
    }

    *out_messages_json = messages_json;
    return 0;
}

tools_context_t* agent_tools_context(void) {
    return &g_tools_ctx;
}

void agent_on_message(const char* sender_pubkey_hex,
                      const char* message,
                      didactyl_sender_tier_t tier,
                      void* user_data) {
    (void)user_data;

    if (!g_cfg || !sender_pubkey_hex || !message) {
        return;
    }

    if (tier == DIDACTYL_SENDER_STRANGER) {
        return;
    }

    fprintf(stdout, "[didactyl] incoming message from %.16s... tier=%d\n", sender_pubkey_hex, (int)tier);

    if (agent_message_is_debounced(sender_pubkey_hex, message)) {
        fprintf(stdout, "[didactyl] debounced duplicate inbound message from %.16s...\n", sender_pubkey_hex);
        return;
    }

    int allow_tools = (tier == DIDACTYL_SENDER_ADMIN) && g_cfg->tools.enabled && g_cfg->security.admin.tools_enabled;
    int inbound_role = (message[0] == '/') ? DIDACTYL_DM_HISTORY_TOOL_REQUEST : DIDACTYL_DM_HISTORY_USER;
    (void)nostr_handler_dm_history_remember(sender_pubkey_hex, message, (didactyl_dm_history_role_t)inbound_role);

    if (message[0] == '/') {
        if (!allow_tools) {
            const char* denied = "{\"success\":false,\"error\":\"slash commands are disabled for this sender tier\"}";
            append_context_log(sender_pubkey_hex, "direct_tool_exec", denied);
            (void)nostr_handler_send_dm_auto_with_role(sender_pubkey_hex,
                                                       denied,
                                                       DIDACTYL_DM_HISTORY_TOOL_RESPONSE);
            return;
        }
        if (handle_slash_command(sender_pubkey_hex, message)) {
            return;
        }
    }

    cJSON* dm_event = cJSON_CreateObject();
    if (!dm_event) {
        return;
    }
    cJSON_AddStringToObject(dm_event, "type", "dm");
    cJSON_AddStringToObject(dm_event, "sender_pubkey", sender_pubkey_hex);
    cJSON_AddStringToObject(dm_event, "message", message);
    cJSON_AddStringToObject(dm_event, "tier",
                            tier == DIDACTYL_SENDER_ADMIN ? "admin" :
                            (tier == DIDACTYL_SENDER_WOT ? "wot" : "stranger"));
    cJSON_AddNumberToObject(dm_event, "created_at", (double)time(NULL));

    char* dm_context = build_context_from_triggers(TRIGGER_TYPE_DM, NULL, dm_event, "dm");
    cJSON_Delete(dm_event);
    if (!dm_context) {
        dm_context = strdup("You are an AI agent. Respond to the message.");
    }

    if (!allow_tools) {
        const char* tier_prefix = (tier == DIDACTYL_SENDER_WOT)
                                    ? "You are responding to a web-of-trust contact. Keep the response helpful and concise. Tool use is disabled for this tier."
                                    : "You are responding in chat-only mode. Tool use is disabled.";

        size_t ctx_len = strlen(dm_context ? dm_context : "") + strlen("\n\n") + strlen(tier_prefix) + 1U;
        char* system_for_chat = (char*)malloc(ctx_len);
        if (!system_for_chat) {
            free(dm_context);
            return;
        }

        snprintf(system_for_chat, ctx_len, "%s\n\n%s", dm_context ? dm_context : "", tier_prefix);

        size_t context_len = strlen("system:\n\nuser:\n") + strlen(system_for_chat) + strlen(message) + 1U;
        char* plain_context = (char*)malloc(context_len);
        if (plain_context) {
            snprintf(plain_context, context_len, "system:\n%s\n\nuser:\n%s", system_for_chat, message);
            append_context_log(sender_pubkey_hex, "llm_chat", plain_context);
            free(plain_context);
        }

        char* response = llm_chat(system_for_chat, message);
        free(system_for_chat);
        if (!response) {
            const char* fallback = "I could not get a response from the LLM right now.";
            fprintf(stdout, "[didactyl] llm response unavailable, sending fallback\n");
            (void)nostr_handler_send_dm_auto(sender_pubkey_hex, fallback);
            return;
        }

        fprintf(stdout, "[didactyl] llm response: %.240s%s\n",
                response,
                strlen(response) > 240 ? "..." : "");
        (void)nostr_handler_send_dm_auto(sender_pubkey_hex, response);
        free(response);
        free(dm_context);
        return;
    }

    char* tools_json = tools_build_openai_schema_json(&g_tools_ctx);
    if (!tools_json) {
        (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Tool schema generation failed.");
        return;
    }

    cJSON* messages = cJSON_CreateArray();
    if (!messages ||
        append_simple_message(messages, "system", dm_context ? dm_context : "You are an AI agent. Respond to the message.") != 0 ||
        append_simple_message(messages, "user", message) != 0) {
        cJSON_Delete(messages);
        free(tools_json);
        free(dm_context);
        (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to initialize conversation messages.");
        return;
    }

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

    int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 20;
    int stall_repeat_threshold = g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3;
    uint64_t last_tool_fp = 0;
    int repeated_tool_turns = 0;
    int exited_on_stall = 0;
    int stall_due_to_length = 0;
    char* final_answer_owned = NULL;
    int turns_run = 0;

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

        append_context_log(sender_pubkey_hex, "llm_chat_with_tools_messages", 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) {
            (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "LLM request failed.");
            cJSON_Delete(messages);
            free(tools_json);
            free(dm_context);
            return;
        }

        if (resp.tool_call_count <= 0) {
            const char* answer = resp.content ? resp.content : "No response content.";
            fprintf(stdout, "[didactyl] llm response (no tool call): %.240s%s\n",
                    answer,
                    strlen(answer) > 240 ? "..." : "");
            final_answer_owned = strdup(answer);
            llm_response_free(&resp);
            break;
        }

        uint64_t current_tool_fp = tool_calls_fingerprint(&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(messages, &resp) != 0) {
            llm_response_free(&resp);
            break;
        }

        for (int i = 0; i < resp.tool_call_count; i++) {
            llm_tool_call_t* tc = &resp.tool_calls[i];
            fprintf(stdout, "[didactyl] executing tool call: %s\n", tc->name ? tc->name : "<null>");
            DEBUG_TRACE("[didactyl] tool call args: %s", tc->arguments_json ? tc->arguments_json : "{}");

            char* tool_result = tools_execute(&g_tools_ctx, tc->name, tc->arguments_json);
            if (!tool_result) {
                tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
            }

            DEBUG_TRACE("[didactyl] tool call result: %s", tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}");

            if (append_tool_result_message(messages,
                                           tc->id ? tc->id : "",
                                           tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
                free(tool_result);
                llm_response_free(&resp);
                cJSON_Delete(messages);
                free(tools_json);
                free(dm_context);
                (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to append tool result.");
                return;
            }
            free(tool_result);
        }

        llm_response_free(&resp);
    }

    if (!final_answer_owned) {
        char* messages_json = cJSON_PrintUnformatted(messages);
        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_answer_owned = strdup(forced_answer);
                }
                llm_response_free(&final_resp);
            }
        }
    }

    int exhausted_on_max_turns = (!final_answer_owned && !exited_on_stall && turns_run >= max_turns);

    if (!final_answer_owned) {
        final_answer_owned = strdup(exited_on_stall
                                        ? "I stopped repeated tool calls after detecting a loop and could not produce a final summary."
                                        : "I hit my tool-use limit for this request.");
    }

    const char* final_answer = final_answer_owned ? final_answer_owned : "I hit my tool-use limit for this request.";

    if (exited_on_stall || exhausted_on_max_turns) {
        notify_admin_limit_diagnostic("dm_agent_loop",
                                      exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted",
                                      sender_pubkey_hex,
                                      max_turns,
                                      turns_run,
                                      stall_repeat_threshold,
                                      repeated_tool_turns,
                                      g_cfg->llm.max_tokens,
                                      (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_answer);
    }
    fprintf(stdout, "[didactyl] final response: %.240s%s\n",
            final_answer,
            strlen(final_answer) > 240 ? "..." : "");
    (void)nostr_handler_send_dm_auto(sender_pubkey_hex, final_answer);

    free(final_answer_owned);
    cJSON_Delete(messages);
    free(tools_json);
    free(dm_context);
}

void agent_cleanup(void) {
    tools_cleanup(&g_tools_ctx);
    g_cfg = NULL;
    g_trigger_manager = NULL;
    memset(g_seen_msgs, 0, sizeof(g_seen_msgs));
    g_seen_msgs_count = 0;
    g_seen_msgs_next = 0;

    pthread_mutex_lock(&g_adopted_skills_mutex);
    clear_adopted_skills_cache_locked();
    g_adopted_skills_last_refresh_at = 0;
    pthread_mutex_unlock(&g_adopted_skills_mutex);

    clear_context_part_names();
}
