#define _POSIX_C_SOURCE 200809L

#include "tools_internal.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "cjson/cJSON.h"
#include "../debug.h"
#include "../nostr_handler.h"
#include "../trigger_manager.h"

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

static const char* skip_ws_local(const char* p) {
    while (p && *p && isspace((unsigned char)*p)) p++;
    return p;
}

static char* sanitize_json_string_controls_local(const char* in) {
    if (!in) return NULL;

    size_t len = strlen(in);
    size_t cap = (len * 2U) + 1U;
    char* out = (char*)malloc(cap);
    if (!out) return NULL;

    int in_string = 0;
    int escaping = 0;
    size_t j = 0;

    for (size_t i = 0; i < len; i++) {
        char c = in[i];

        if (escaping) {
            if (j + 1U >= cap) {
                free(out);
                return NULL;
            }
            out[j++] = c;
            escaping = 0;
            continue;
        }

        if (c == '\\') {
            if (j + 1U >= cap) {
                free(out);
                return NULL;
            }
            out[j++] = c;
            if (in_string) escaping = 1;
            continue;
        }

        if (c == '"') {
            if (j + 1U >= cap) {
                free(out);
                return NULL;
            }
            out[j++] = c;
            in_string = !in_string;
            continue;
        }

        if (in_string && (c == '\n' || c == '\r' || c == '\t')) {
            if (j + 2U >= cap) {
                free(out);
                return NULL;
            }
            out[j++] = '\\';
            out[j++] = (c == '\n') ? 'n' : (c == '\r') ? 'r' : 't';
            continue;
        }

        if (j + 1U >= cap) {
            free(out);
            return NULL;
        }
        out[j++] = c;
    }

    out[j] = '\0';
    return out;
}

static cJSON* parse_args_local(const char* args_json) {
    const char* raw_args_json = args_json ? args_json : "{}";
    raw_args_json = skip_ws_local(raw_args_json);
    if (!raw_args_json || raw_args_json[0] == '\0') {
        raw_args_json = "{}";
    }

    cJSON* args = cJSON_Parse(raw_args_json);
    char* repaired_args_json = NULL;

    if (args && cJSON_IsString(args) && args->valuestring) {
        cJSON* nested = cJSON_Parse(args->valuestring);
        if (nested) {
            cJSON_Delete(args);
            args = nested;
        }
    }

    if (!args) {
        repaired_args_json = sanitize_json_string_controls_local(raw_args_json);
        if (repaired_args_json) {
            args = cJSON_Parse(repaired_args_json);
            if (args && cJSON_IsString(args) && args->valuestring) {
                cJSON* nested = cJSON_Parse(args->valuestring);
                if (nested) {
                    cJSON_Delete(args);
                    args = nested;
                }
            }
        }
    }

    free(repaired_args_json);
    return args;
}

static int is_hex_string_len_local(const char* s, size_t expected_len) {
    if (!s || strlen(s) != expected_len) return 0;
    for (size_t i = 0; i < expected_len; i++) {
        unsigned char c = (unsigned char)s[i];
        if (!isxdigit(c)) return 0;
    }
    return 1;
}

static int add_string_tag_local(cJSON* tags, const char* key, const char* value) {
    if (!tags || !cJSON_IsArray(tags) || !key || !value || value[0] == '\0') return -1;

    cJSON* tag = cJSON_CreateArray();
    if (!tag) return -1;

    cJSON_AddItemToArray(tag, cJSON_CreateString(key));
    cJSON_AddItemToArray(tag, cJSON_CreateString(value));
    cJSON_AddItemToArray(tags, tag);
    return 0;
}

static int remove_tag_key_all_local(cJSON* tags, const char* key) {
    if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') return 0;
    int removed = 0;
    for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) {
        cJSON* tag = cJSON_GetArrayItem(tags, i);
        if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 1) continue;
        cJSON* k = cJSON_GetArrayItem(tag, 0);
        if (k && cJSON_IsString(k) && k->valuestring && strcmp(k->valuestring, key) == 0) {
            cJSON_DeleteItemFromArray(tags, i);
            removed++;
        }
    }
    return removed;
}

static int upsert_string_tag_local(cJSON* tags, const char* key, const char* value) {
    if (!tags || !cJSON_IsArray(tags) || !key || !value || value[0] == '\0') return -1;
    remove_tag_key_all_local(tags, key);
    return add_string_tag_local(tags, key, value);
}

static int tag_tuple_equal_local(cJSON* a, cJSON* b) {
    if (!a || !b || !cJSON_IsArray(a) || !cJSON_IsArray(b)) return 0;
    int an = cJSON_GetArraySize(a);
    int bn = cJSON_GetArraySize(b);
    if (an != bn) return 0;

    for (int i = 0; i < an; i++) {
        cJSON* ai = cJSON_GetArrayItem(a, i);
        cJSON* bi = cJSON_GetArrayItem(b, i);
        if (!ai || !bi || !cJSON_IsString(ai) || !cJSON_IsString(bi) ||
            !ai->valuestring || !bi->valuestring || strcmp(ai->valuestring, bi->valuestring) != 0) {
            return 0;
        }
    }
    return 1;
}

static int tags_contains_tuple_local(cJSON* tags, cJSON* tuple) {
    if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
    int n = cJSON_GetArraySize(tags);
    for (int i = 0; i < n; i++) {
        cJSON* cur = cJSON_GetArrayItem(tags, i);
        if (tag_tuple_equal_local(cur, tuple)) {
            return 1;
        }
    }
    return 0;
}

static int remove_matching_tag_tuples_local(cJSON* tags, cJSON* tuple) {
    if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
    int removed = 0;
    for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) {
        cJSON* cur = cJSON_GetArrayItem(tags, i);
        if (tag_tuple_equal_local(cur, tuple)) {
            cJSON_DeleteItemFromArray(tags, i);
            removed++;
        }
    }
    return removed;
}

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 && strcmp(k->valuestring, key) == 0) {
            return v;
        }
    }

    return NULL;
}

static int validate_skill_d_tag_local(const char* d_tag) {
    if (!d_tag) return 0;
    size_t len = strlen(d_tag);
    if (len == 0 || len > 64U) return 0;
    if (d_tag[0] == '-' || d_tag[len - 1] == '-') return 0;

    int prev_dash = 0;
    for (size_t i = 0; i < len; i++) {
        unsigned char c = (unsigned char)d_tag[i];
        if (c == '-') {
            if (prev_dash) return 0;
            prev_dash = 1;
            continue;
        }
        if (!islower(c) && !isdigit(c) && c != '_') return 0;
        prev_dash = 0;
    }

    return 1;
}

static int ci_contains_local(const char* haystack, const char* needle) {
    if (!haystack || !needle) return 0;
    if (needle[0] == '\0') return 1;

    size_t nlen = strlen(needle);
    size_t hlen = strlen(haystack);
    if (nlen > hlen) return 0;

    for (size_t i = 0; i + nlen <= hlen; i++) {
        size_t j = 0;
        while (j < nlen) {
            unsigned char a = (unsigned char)haystack[i + j];
            unsigned char b = (unsigned char)needle[j];
            if (tolower(a) != tolower(b)) break;
            j++;
        }
        if (j == nlen) return 1;
    }

    return 0;
}

static int fetch_adoption_list_tags_local(tools_context_t* ctx, cJSON** out_tags, char** out_content) {
    if (!ctx || !ctx->cfg || !out_tags || !out_content) return -1;

    *out_tags = NULL;
    *out_content = NULL;

    char* events_json = nostr_handler_get_self_events_by_kind_json(10123);

    cJSON* tags = cJSON_CreateArray();
    char* content = strdup("");
    if (!tags || !content) {
        free(events_json);
        cJSON_Delete(tags);
        free(content);
        return -1;
    }

    if (events_json) {
        cJSON* events = cJSON_Parse(events_json);
        free(events_json);

        if (events && cJSON_IsArray(events) && cJSON_GetArraySize(events) > 0) {
            cJSON* ev0 = cJSON_GetArrayItem(events, 0);
            if (ev0 && cJSON_IsObject(ev0)) {
                cJSON* ev_content = cJSON_GetObjectItemCaseSensitive(ev0, "content");
                if (ev_content && cJSON_IsString(ev_content) && ev_content->valuestring) {
                    free(content);
                    content = strdup(ev_content->valuestring);
                    if (!content) {
                        cJSON_Delete(events);
                        cJSON_Delete(tags);
                        return -1;
                    }
                }

                cJSON* ev_tags = cJSON_GetObjectItemCaseSensitive(ev0, "tags");
                if (ev_tags && cJSON_IsArray(ev_tags)) {
                    cJSON* dup = cJSON_Duplicate(ev_tags, 1);
                    if (!dup) {
                        cJSON_Delete(events);
                        cJSON_Delete(tags);
                        free(content);
                        return -1;
                    }
                    cJSON_Delete(tags);
                    tags = dup;
                }
            }
        }

        cJSON_Delete(events);
    }

    *out_tags = tags;
    *out_content = content;
    return 0;
}

static int publish_adoption_list_local(const char* content, cJSON* tags, nostr_publish_result_t* out_result) {
    if (!tags || !cJSON_IsArray(tags) || !out_result) return -1;
    memset(out_result, 0, sizeof(*out_result));
    return nostr_handler_publish_kind_event(10123, content ? content : "", tags, out_result);
}

static int adoption_tags_contains_address_local(cJSON* adoption_tags,
                                                int kind,
                                                const char* pubkey,
                                                const char* d_tag) {
    if (!adoption_tags || !cJSON_IsArray(adoption_tags) || !pubkey || !d_tag || d_tag[0] == '\0') {
        return 0;
    }

    char addr[256];
    snprintf(addr, sizeof(addr), "%d:%s:%s", kind, pubkey, d_tag);

    int tag_count = cJSON_GetArraySize(adoption_tags);
    for (int i = 0; i < tag_count; i++) {
        cJSON* tag = cJSON_GetArrayItem(adoption_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 && strcmp(v->valuestring, addr) == 0) {
            return 1;
        }
    }

    return 0;
}

static const char* skill_owner_label_local(const didactyl_config_t* cfg, const char* pubkey_hex) {
    if (!cfg || !pubkey_hex || pubkey_hex[0] == '\0') return "unknown";
    if (strcmp(pubkey_hex, cfg->keys.public_key_hex) == 0) return "agent";
    if (cfg->admin.pubkey[0] != '\0' && strcmp(pubkey_hex, cfg->admin.pubkey) == 0) return "admin";
    return "external";
}

static cJSON* extract_skill_summary_local(cJSON* event) {
    if (!event || !cJSON_IsObject(event)) return NULL;

    cJSON* summary = cJSON_CreateObject();
    if (!summary) return NULL;

    cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
    cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
    cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
    cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
    cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content");
    cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");

    if (kind && cJSON_IsNumber(kind)) {
        cJSON_AddNumberToObject(summary, "kind", (int)kind->valuedouble);
    }
    if (created_at && cJSON_IsNumber(created_at)) {
        char ts_buf[32];
        snprintf(ts_buf, sizeof(ts_buf), "%lld", (long long)created_at->valuedouble);
        cJSON* ts_raw = cJSON_CreateRaw(ts_buf);
        if (ts_raw) {
            cJSON_AddItemToObject(summary, "created_at", ts_raw);
        }
    }
    if (id && cJSON_IsString(id) && id->valuestring) {
        cJSON_AddStringToObject(summary, "id", id->valuestring);
    }
    if (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring) {
        cJSON_AddStringToObject(summary, "pubkey", pubkey->valuestring);
    }

    if (tags && cJSON_IsArray(tags)) {
        cJSON* d = find_tag_value_string_local(tags, "d");
        cJSON* scope = find_tag_value_string_local(tags, "scope");
        cJSON* description = find_tag_value_string_local(tags, "description");

        if (d && cJSON_IsString(d) && d->valuestring) {
            cJSON_AddStringToObject(summary, "d_tag", d->valuestring);
        }
        if (scope && cJSON_IsString(scope) && scope->valuestring) {
            cJSON_AddStringToObject(summary, "scope", scope->valuestring);
        }
        if (description && cJSON_IsString(description) && description->valuestring) {
            cJSON_AddStringToObject(summary, "description", description->valuestring);
        }
    }

    if (content && cJSON_IsString(content) && content->valuestring) {
        size_t len = strlen(content->valuestring);
        size_t preview_len = len > 200U ? 200U : len;
        char* preview = (char*)malloc(preview_len + 1U);
        if (!preview) {
            cJSON_Delete(summary);
            return NULL;
        }
        memcpy(preview, content->valuestring, preview_len);
        preview[preview_len] = '\0';

        cJSON_AddStringToObject(summary, "content_preview", preview);
        cJSON_AddNumberToObject(summary, "content_length", (double)len);
        cJSON_AddBoolToObject(summary, "content_truncated", len > preview_len ? 1 : 0);
        free(preview);
    }

    return summary;
}

char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d");
    cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
    cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope");
    cJSON* description = cJSON_GetObjectItemCaseSensitive(args, "description");
    cJSON* auto_adopt = cJSON_GetObjectItemCaseSensitive(args, "auto_adopt");
    cJSON* trigger = cJSON_GetObjectItemCaseSensitive(args, "trigger");
    cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter");
    cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
    cJSON* enabled = cJSON_GetObjectItemCaseSensitive(args, "enabled");
    cJSON* llm = cJSON_GetObjectItemCaseSensitive(args, "llm");
    cJSON* tools = cJSON_GetObjectItemCaseSensitive(args, "tools");
    cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(args, "max_tokens");
    cJSON* temperature = cJSON_GetObjectItemCaseSensitive(args, "temperature");
    cJSON* seed = cJSON_GetObjectItemCaseSensitive(args, "seed");

    if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring ||
        !content || !cJSON_IsString(content) || !content->valuestring) {
        cJSON_Delete(args);
        return json_error_local("skill_create requires d and content strings");
    }
    if (!validate_skill_d_tag_local(d_tag->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_create d must be lowercase letters/digits/hyphens (1-64 chars)");
    }
    if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_create scope must be string when provided");
    }
    if (description && (!cJSON_IsString(description) || !description->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_create description must be string when provided");
    }

    if (trigger && (!cJSON_IsString(trigger) || !trigger->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_create trigger must be string when provided");
    }
    if (filter && (!cJSON_IsString(filter) || !filter->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_create filter must be string when provided");
    }
    if (action && (!cJSON_IsString(action) || !action->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_create action must be string when provided");
    }
    if (enabled && !cJSON_IsBool(enabled)) {
        cJSON_Delete(args);
        return json_error_local("skill_create enabled must be boolean when provided");
    }
    if (llm && (!cJSON_IsString(llm) || !llm->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_create llm must be string when provided");
    }
    if (tools && (!cJSON_IsString(tools) || !tools->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_create tools must be string when provided");
    }
    if (max_tokens && !cJSON_IsNumber(max_tokens)) {
        cJSON_Delete(args);
        return json_error_local("skill_create max_tokens must be number when provided");
    }
    if (temperature && !cJSON_IsNumber(temperature)) {
        cJSON_Delete(args);
        return json_error_local("skill_create temperature must be number when provided");
    }
    if (seed && !cJSON_IsNumber(seed)) {
        cJSON_Delete(args);
        return json_error_local("skill_create seed must be number when provided");
    }

    const char* scope_str = (scope && scope->valuestring && scope->valuestring[0] != '\0') ? scope->valuestring : "public";
    int kind = 0;
    if (strcmp(scope_str, "public") == 0) {
        kind = 31123;
    } else if (strcmp(scope_str, "private") == 0) {
        kind = 31124;
    } else {
        cJSON_Delete(args);
        return json_error_local("skill_create scope must be public or private");
    }

    int do_auto_adopt = (!auto_adopt || !cJSON_IsBool(auto_adopt) || cJSON_IsTrue(auto_adopt)) ? 1 : 0;

    cJSON* tags = cJSON_CreateArray();
    if (!tags) {
        cJSON_Delete(args);
        return json_error_local("failed to create skill tags");
    }

    if (add_string_tag_local(tags, "d", d_tag->valuestring) != 0 ||
        add_string_tag_local(tags, "app", "didactyl") != 0 ||
        add_string_tag_local(tags, "scope", scope_str) != 0) {
        cJSON_Delete(tags);
        cJSON_Delete(args);
        return json_error_local("failed to add required skill tags");
    }

    if (description && description->valuestring && description->valuestring[0] != '\0') {
        if (add_string_tag_local(tags, "description", description->valuestring) != 0) {
            cJSON_Delete(tags);
            cJSON_Delete(args);
            return json_error_local("failed to add description tag");
        }
    }

    const char* trigger_str = (trigger && trigger->valuestring && trigger->valuestring[0] != '\0')
                                  ? trigger->valuestring
                                  : NULL;
    const char* filter_str = (filter && filter->valuestring && filter->valuestring[0] != '\0')
                                 ? filter->valuestring
                                 : NULL;
    const char* action_str = (action && action->valuestring && action->valuestring[0] != '\0')
                                 ? action->valuestring
                                 : "llm";
    int enabled_int = (!enabled || cJSON_IsTrue(enabled)) ? 1 : 0;

    if (trigger_str &&
        strcmp(trigger_str, "nostr-subscription") != 0 &&
        strcmp(trigger_str, "webhook") != 0 &&
        strcmp(trigger_str, "cron") != 0 &&
        strcmp(trigger_str, "chain") != 0 &&
        strcmp(trigger_str, "dm") != 0) {
        cJSON_Delete(tags);
        cJSON_Delete(args);
        return json_error_local("skill_create trigger must be one of: nostr-subscription, webhook, cron, chain, dm");
    }
    if ((trigger_str && !filter_str) || (!trigger_str && filter_str)) {
        cJSON_Delete(tags);
        cJSON_Delete(args);
        return json_error_local("skill_create trigger and filter must both be provided together");
    }
    if (trigger_str) {
        if (add_string_tag_local(tags, "trigger", trigger_str) != 0 ||
            add_string_tag_local(tags, "filter", filter_str) != 0 ||
            add_string_tag_local(tags, "action", action_str) != 0 ||
            add_string_tag_local(tags, "enabled", enabled_int ? "true" : "false") != 0) {
            cJSON_Delete(tags);
            cJSON_Delete(args);
            return json_error_local("failed to add trigger tags");
        }

        if (llm && llm->valuestring && llm->valuestring[0] != '\0' &&
            add_string_tag_local(tags, "llm", llm->valuestring) != 0) {
            cJSON_Delete(tags);
            cJSON_Delete(args);
            return json_error_local("failed to add llm trigger tag");
        }

        if (tools && tools->valuestring && tools->valuestring[0] != '\0' &&
            add_string_tag_local(tags, "tools", tools->valuestring) != 0) {
            cJSON_Delete(tags);
            cJSON_Delete(args);
            return json_error_local("failed to add tools trigger tag");
        }

        if (max_tokens) {
            char buf[32];
            snprintf(buf, sizeof(buf), "%d", (int)max_tokens->valuedouble);
            if (add_string_tag_local(tags, "max_tokens", buf) != 0) {
                cJSON_Delete(tags);
                cJSON_Delete(args);
                return json_error_local("failed to add max_tokens trigger tag");
            }
        }

        if (temperature) {
            char buf[32];
            snprintf(buf, sizeof(buf), "%g", temperature->valuedouble);
            if (add_string_tag_local(tags, "temperature", buf) != 0) {
                cJSON_Delete(tags);
                cJSON_Delete(args);
                return json_error_local("failed to add temperature trigger tag");
            }
        }

        if (seed) {
            char buf[32];
            snprintf(buf, sizeof(buf), "%d", (int)seed->valuedouble);
            if (add_string_tag_local(tags, "seed", buf) != 0) {
                cJSON_Delete(tags);
                cJSON_Delete(args);
                return json_error_local("failed to add seed trigger tag");
            }
        }
    }

    nostr_publish_result_t skill_publish;
    memset(&skill_publish, 0, sizeof(skill_publish));

    int rc = nostr_handler_publish_kind_event(kind, content->valuestring, tags, &skill_publish);
    cJSON_Delete(tags);
    if (rc != 0) {
        cJSON_Delete(args);
        nostr_handler_publish_result_free(&skill_publish);
        return json_error_local("skill_create failed to publish skill event");
    }

    int adopted = 0;
    int already_adopted = 0;
    nostr_publish_result_t adoption_publish;
    memset(&adoption_publish, 0, sizeof(adoption_publish));

    if (do_auto_adopt && kind == 31123) {
        cJSON* adoption_tags = NULL;
        char* adoption_content = NULL;
        if (fetch_adoption_list_tags_local(ctx, &adoption_tags, &adoption_content) == 0) {
            char addr[256];
            snprintf(addr, sizeof(addr), "31123:%s:%s", ctx->cfg->keys.public_key_hex, d_tag->valuestring);

            cJSON* tuple = cJSON_CreateArray();
            if (tuple) {
                cJSON_AddItemToArray(tuple, cJSON_CreateString("a"));
                cJSON_AddItemToArray(tuple, cJSON_CreateString(addr));

                if (tags_contains_tuple_local(adoption_tags, tuple)) {
                    already_adopted = 1;
                    adopted = 1;
                } else {
                    cJSON* dup = cJSON_Duplicate(tuple, 1);
                    if (dup) {
                        cJSON_AddItemToArray(adoption_tags, dup);
                        if (publish_adoption_list_local(adoption_content, adoption_tags, &adoption_publish) == 0) {
                            adopted = 1;
                        }
                    }
                }

                cJSON_Delete(tuple);
            }

            cJSON_Delete(adoption_tags);
            free(adoption_content);
        }
    }

    int trigger_registered = 0;
    if (trigger_str && ctx->trigger_manager && enabled_int) {
        if (strcmp(action_str, "template") == 0) {
            DEBUG_WARN("[didactyl] skill_create trigger action template is deprecated; forcing llm for d_tag=%s", d_tag->valuestring);
        }
        if (trigger_manager_add(ctx->trigger_manager,
                                d_tag->valuestring,
                                content->valuestring,
                                filter_str,
                                TRIGGER_ACTION_LLM,
                                trigger_str,
                                enabled_int,
                                (llm && llm->valuestring) ? llm->valuestring : NULL,
                                (tools && tools->valuestring) ? tools->valuestring : NULL,
                                max_tokens ? 1 : 0,
                                max_tokens ? (int)max_tokens->valuedouble : 0,
                                temperature ? 1 : 0,
                                temperature ? temperature->valuedouble : 0.0,
                                seed ? 1 : 0,
                                seed ? (int)seed->valuedouble : 0) == 0) {
            trigger_registered = 1;
        }
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        cJSON_Delete(args);
        nostr_handler_publish_result_free(&skill_publish);
        nostr_handler_publish_result_free(&adoption_publish);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", skill_publish.success ? 1 : 0);
    cJSON_AddStringToObject(out, "d_tag", d_tag->valuestring);
    cJSON_AddNumberToObject(out, "kind", kind);
    cJSON_AddStringToObject(out, "scope", scope_str);
    cJSON_AddStringToObject(out, "skill_event_id", skill_publish.event_id);
    cJSON_AddBoolToObject(out, "auto_adopt", do_auto_adopt ? 1 : 0);
    cJSON_AddBoolToObject(out, "adopted", adopted ? 1 : 0);
    cJSON_AddBoolToObject(out, "already_adopted", already_adopted ? 1 : 0);
    cJSON_AddBoolToObject(out, "trigger_registered", trigger_registered ? 1 : 0);

    if (adoption_publish.event_id[0] != '\0') {
        cJSON_AddStringToObject(out, "adoption_event_id", adoption_publish.event_id);
    }
    if (skill_publish.naddr_uri[0] != '\0') {
        cJSON_AddStringToObject(out, "naddr_uri", skill_publish.naddr_uri);
    }

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    cJSON_Delete(args);
    nostr_handler_publish_result_free(&skill_publish);
    nostr_handler_publish_result_free(&adoption_publish);
    return json;
}

char* execute_skill_list(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope");
    cJSON* adopted = cJSON_GetObjectItemCaseSensitive(args, "adopted");
    if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_list scope must be string when provided");
    }
    if (adopted && !cJSON_IsBool(adopted)) {
        cJSON_Delete(args);
        return json_error_local("skill_list adopted must be boolean when provided");
    }

    const char* scope_str = (scope && scope->valuestring && scope->valuestring[0] != '\0') ? scope->valuestring : NULL;
    int include_public = 1;
    int include_private = 1;
    if (scope_str) {
        if (strcmp(scope_str, "public") == 0) {
            include_private = 0;
        } else if (strcmp(scope_str, "private") == 0) {
            include_public = 0;
        } else {
            cJSON_Delete(args);
            return json_error_local("skill_list scope must be public or private");
        }
    }

    int adopted_filter = -1;
    if (adopted) {
        adopted_filter = cJSON_IsTrue(adopted) ? 1 : 0;
    }
    cJSON_Delete(args);

    cJSON* adoption_tags = NULL;
    char* adoption_content = NULL;
    if (fetch_adoption_list_tags_local(ctx, &adoption_tags, &adoption_content) != 0) {
        return json_error_local("skill_list failed to load adoption list");
    }

    char* events_json = nostr_handler_get_self_skill_events_json();
    if (!events_json) {
        cJSON_Delete(adoption_tags);
        free(adoption_content);
        return json_error_local("skill_list cache unavailable");
    }

    cJSON* events = cJSON_Parse(events_json);
    free(events_json);
    if (!events || !cJSON_IsArray(events)) {
        cJSON_Delete(events);
        cJSON_Delete(adoption_tags);
        free(adoption_content);
        return json_error_local("skill_list cache returned invalid JSON");
    }

    cJSON* out = cJSON_CreateObject();
    cJSON* summary_rows = cJSON_CreateArray();
    cJSON* skills = cJSON_CreateArray();
    if (!out || !summary_rows || !skills) {
        cJSON_Delete(out);
        cJSON_Delete(summary_rows);
        cJSON_Delete(skills);
        cJSON_Delete(events);
        cJSON_Delete(adoption_tags);
        free(adoption_content);
        return NULL;
    }

    int adopted_count = 0;
    int event_count = cJSON_GetArraySize(events);
    for (int i = 0; i < event_count; i++) {
        cJSON* ev = cJSON_GetArrayItem(events, i);
        if (!ev || !cJSON_IsObject(ev)) continue;

        cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
        cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
        cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
        if (!kind || !cJSON_IsNumber(kind) ||
            !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
            !tags || !cJSON_IsArray(tags)) {
            continue;
        }

        int kind_val = (int)kind->valuedouble;
        if (kind_val != 31123 && kind_val != 31124) {
            continue;
        }
        if ((kind_val == 31123 && !include_public) || (kind_val == 31124 && !include_private)) {
            continue;
        }

        cJSON* d = find_tag_value_string_local(tags, "d");
        if (!d || !cJSON_IsString(d) || !d->valuestring || d->valuestring[0] == '\0') {
            continue;
        }

        int is_adopted = adoption_tags_contains_address_local(adoption_tags, kind_val, pubkey->valuestring, d->valuestring) ? 1 : 0;
        if (adopted_filter >= 0 && is_adopted != adopted_filter) {
            continue;
        }
        if (is_adopted) {
            adopted_count++;
        }

        const char* owner = skill_owner_label_local(ctx->cfg, pubkey->valuestring);

        cJSON* detail = extract_skill_summary_local(ev);
        if (detail) {
            cJSON_AddStringToObject(detail, "owner", owner);
            cJSON_AddBoolToObject(detail, "adopted", is_adopted ? 1 : 0);
            cJSON_AddItemToArray(skills, detail);
        }

        cJSON* row = cJSON_CreateObject();
        if (row) {
            cJSON_AddStringToObject(row, "d_tag", d->valuestring);
            cJSON_AddStringToObject(row, "owner", owner);
            cJSON_AddBoolToObject(row, "adopted", is_adopted ? 1 : 0);
            cJSON_AddItemToArray(summary_rows, row);
        }
    }

    cJSON* skills_pretty = cJSON_CreateArray();
    if (!skills_pretty) {
        cJSON_Delete(out);
        cJSON_Delete(events);
        cJSON_Delete(adoption_tags);
        free(adoption_content);
        return NULL;
    }

    int skill_count = cJSON_GetArraySize(skills);
    for (int i = 0; i < skill_count; i++) {
        cJSON* skill_item = cJSON_GetArrayItem(skills, i);
        if (!skill_item) continue;
        char* skill_json = cJSON_PrintUnformatted(skill_item);
        if (!skill_json) continue;

        cJSON* block = cJSON_CreateString(skill_json);
        free(skill_json);
        if (block) {
            cJSON_AddItemToArray(skills_pretty, block);
        }
    }

    cJSON_AddBoolToObject(out, "success", 1);
    cJSON_AddBoolToObject(out, "fallback_used", 0);
    cJSON_AddStringToObject(out, "skills_separator", "\n\n\n");
    cJSON_AddItemToObject(out, "summary", summary_rows);
    cJSON_AddItemToObject(out, "skills", skills);
    cJSON_AddItemToObject(out, "skills_pretty", skills_pretty);
    cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(skills));
    cJSON_AddNumberToObject(out, "adopted_count", adopted_count);

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    cJSON_Delete(events);
    cJSON_Delete(adoption_tags);
    free(adoption_content);
    return json;
}

char* execute_skill_adopt(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
    cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d");
    cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");

    if (!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len_local(pubkey->valuestring, 64U) ||
        !d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || !validate_skill_d_tag_local(d_tag->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_adopt requires pubkey (hex) and valid d tag");
    }

    int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : 31123;
    if (kind_val != 31123 && kind_val != 31124) {
        cJSON_Delete(args);
        return json_error_local("skill_adopt kind must be 31123 or 31124");
    }

    char addr[256];
    snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey->valuestring, d_tag->valuestring);

    cJSON* tags = NULL;
    char* content = NULL;
    if (fetch_adoption_list_tags_local(ctx, &tags, &content) != 0) {
        cJSON_Delete(args);
        return json_error_local("skill_adopt failed to load adoption list");
    }

    cJSON* tuple = cJSON_CreateArray();
    if (!tuple) {
        cJSON_Delete(tags);
        free(content);
        cJSON_Delete(args);
        return json_error_local("allocation failure");
    }
    cJSON_AddItemToArray(tuple, cJSON_CreateString("a"));
    cJSON_AddItemToArray(tuple, cJSON_CreateString(addr));

    int already_adopted = tags_contains_tuple_local(tags, tuple);

    nostr_publish_result_t publish_result;
    memset(&publish_result, 0, sizeof(publish_result));

    if (!already_adopted) {
        cJSON* dup = cJSON_Duplicate(tuple, 1);
        if (!dup) {
            cJSON_Delete(tuple);
            cJSON_Delete(tags);
            free(content);
            cJSON_Delete(args);
            return json_error_local("failed to duplicate adoption tuple");
        }
        cJSON_AddItemToArray(tags, dup);
        if (publish_adoption_list_local(content, tags, &publish_result) != 0) {
            cJSON_Delete(tuple);
            cJSON_Delete(tags);
            free(content);
            cJSON_Delete(args);
            return json_error_local("skill_adopt failed to publish adoption list");
        }
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        cJSON_Delete(tuple);
        cJSON_Delete(tags);
        free(content);
        cJSON_Delete(args);
        nostr_handler_publish_result_free(&publish_result);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", 1);
    cJSON_AddStringToObject(out, "adopted_address", addr);
    cJSON_AddBoolToObject(out, "already_adopted", already_adopted ? 1 : 0);
    if (publish_result.event_id[0] != '\0') {
        cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
    }

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    cJSON_Delete(tuple);
    cJSON_Delete(tags);
    free(content);
    cJSON_Delete(args);
    nostr_handler_publish_result_free(&publish_result);
    return json;
}

char* execute_skill_remove(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
    cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d");
    cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");

    const char* pubkey_str = ctx->cfg->keys.public_key_hex;
    if (pubkey) {
        if (!cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len_local(pubkey->valuestring, 64U)) {
            cJSON_Delete(args);
            return json_error_local("skill_remove pubkey must be 64-char hex when provided");
        }
        pubkey_str = pubkey->valuestring;
    }

    if (!d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || !validate_skill_d_tag_local(d_tag->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_remove requires valid d tag");
    }

    int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : 31123;
    if (kind_val != 31123 && kind_val != 31124) {
        cJSON_Delete(args);
        return json_error_local("skill_remove kind must be 31123 or 31124");
    }

    char addr[256];
    snprintf(addr, sizeof(addr), "%d:%s:%s", kind_val, pubkey_str, d_tag->valuestring);

    cJSON* tags = NULL;
    char* content = NULL;
    if (fetch_adoption_list_tags_local(ctx, &tags, &content) != 0) {
        cJSON_Delete(args);
        return json_error_local("skill_remove failed to load adoption list");
    }

    cJSON* tuple = cJSON_CreateArray();
    if (!tuple) {
        cJSON_Delete(tags);
        free(content);
        cJSON_Delete(args);
        return json_error_local("allocation failure");
    }
    cJSON_AddItemToArray(tuple, cJSON_CreateString("a"));
    cJSON_AddItemToArray(tuple, cJSON_CreateString(addr));

    int removed = remove_matching_tag_tuples_local(tags, tuple);

    nostr_publish_result_t publish_result;
    memset(&publish_result, 0, sizeof(publish_result));

    if (removed > 0) {
        if (publish_adoption_list_local(content, tags, &publish_result) != 0) {
            cJSON_Delete(tuple);
            cJSON_Delete(tags);
            free(content);
            cJSON_Delete(args);
            return json_error_local("skill_remove failed to publish adoption list");
        }
    }

    if (ctx->trigger_manager) {
        (void)trigger_manager_remove(ctx->trigger_manager, d_tag->valuestring);
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        cJSON_Delete(tuple);
        cJSON_Delete(tags);
        free(content);
        cJSON_Delete(args);
        nostr_handler_publish_result_free(&publish_result);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", 1);
    cJSON_AddStringToObject(out, "removed_address", addr);
    cJSON_AddNumberToObject(out, "removed_count", removed);
    cJSON_AddBoolToObject(out, "already_absent", removed == 0 ? 1 : 0);
    if (publish_result.event_id[0] != '\0') {
        cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
    }

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    cJSON_Delete(tuple);
    cJSON_Delete(tags);
    free(content);
    cJSON_Delete(args);
    nostr_handler_publish_result_free(&publish_result);
    return json;
}

char* execute_skill_edit(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* d = cJSON_GetObjectItemCaseSensitive(args, "d");
    cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
    cJSON* scope = cJSON_GetObjectItemCaseSensitive(args, "scope");
    cJSON* description = cJSON_GetObjectItemCaseSensitive(args, "description");
    cJSON* trigger = cJSON_GetObjectItemCaseSensitive(args, "trigger");
    cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter");
    cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
    cJSON* enabled = cJSON_GetObjectItemCaseSensitive(args, "enabled");
    cJSON* llm = cJSON_GetObjectItemCaseSensitive(args, "llm");
    cJSON* tools = cJSON_GetObjectItemCaseSensitive(args, "tools");
    cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(args, "max_tokens");
    cJSON* temperature = cJSON_GetObjectItemCaseSensitive(args, "temperature");
    cJSON* seed = cJSON_GetObjectItemCaseSensitive(args, "seed");

    if (!d || !cJSON_IsString(d) || !d->valuestring || !validate_skill_d_tag_local(d->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit requires valid d tag");
    }
    if (content && (!cJSON_IsString(content) || !content->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit content must be string when provided");
    }
    if (scope && (!cJSON_IsString(scope) || !scope->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit scope must be string when provided");
    }
    if (description && (!cJSON_IsString(description) || !description->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit description must be string when provided");
    }
    if (trigger && (!cJSON_IsString(trigger) || !trigger->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit trigger must be string when provided");
    }
    if (filter && (!cJSON_IsString(filter) || !filter->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit filter must be string when provided");
    }
    if (action && (!cJSON_IsString(action) || !action->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit action must be string when provided");
    }
    if (enabled && !cJSON_IsBool(enabled)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit enabled must be boolean when provided");
    }
    if (llm && (!cJSON_IsString(llm) || !llm->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit llm must be string when provided");
    }
    if (tools && (!cJSON_IsString(tools) || !tools->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit tools must be string when provided");
    }
    if (max_tokens && !cJSON_IsNumber(max_tokens)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit max_tokens must be number when provided");
    }
    if (temperature && !cJSON_IsNumber(temperature)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit temperature must be number when provided");
    }
    if (seed && !cJSON_IsNumber(seed)) {
        cJSON_Delete(args);
        return json_error_local("skill_edit seed must be number when provided");
    }

    char* events_json = nostr_handler_get_self_skill_events_json();
    if (!events_json) {
        cJSON_Delete(args);
        return json_error_local("skill_edit cache unavailable");
    }

    cJSON* events = cJSON_Parse(events_json);
    free(events_json);
    if (!events || !cJSON_IsArray(events)) {
        cJSON_Delete(args);
        cJSON_Delete(events);
        return json_error_local("skill_edit cache returned invalid JSON");
    }

    cJSON* target = NULL;
    long long best_created_at = -1;
    int event_count = cJSON_GetArraySize(events);
    for (int i = 0; i < event_count; i++) {
        cJSON* ev = cJSON_GetArrayItem(events, i);
        if (!ev || !cJSON_IsObject(ev)) continue;

        cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
        cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
        if (!kind || !cJSON_IsNumber(kind) || !tags || !cJSON_IsArray(tags)) continue;

        int kind_val = (int)kind->valuedouble;
        if (kind_val != 31123 && kind_val != 31124) continue;

        cJSON* d_val = find_tag_value_string_local(tags, "d");
        if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) continue;
        if (strcmp(d_val->valuestring, d->valuestring) != 0) continue;

        cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
        long long created = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0;
        if (!target || created > best_created_at) {
            target = ev;
            best_created_at = created;
        }
    }

    if (!target) {
        cJSON_Delete(args);
        cJSON_Delete(events);
        return json_error_local("skill_edit could not find self skill with that d");
    }

    cJSON* target_kind = cJSON_GetObjectItemCaseSensitive(target, "kind");
    cJSON* target_content = cJSON_GetObjectItemCaseSensitive(target, "content");
    cJSON* target_tags = cJSON_GetObjectItemCaseSensitive(target, "tags");
    if (!target_kind || !cJSON_IsNumber(target_kind) || !target_content || !cJSON_IsString(target_content) ||
        !target_content->valuestring || !target_tags || !cJSON_IsArray(target_tags)) {
        cJSON_Delete(args);
        cJSON_Delete(events);
        return json_error_local("skill_edit found malformed skill event");
    }

    int out_kind = (int)target_kind->valuedouble;
    if (scope && scope->valuestring) {
        if (strcmp(scope->valuestring, "public") == 0) {
            out_kind = 31123;
        } else if (strcmp(scope->valuestring, "private") == 0) {
            out_kind = 31124;
        } else {
            cJSON_Delete(args);
            cJSON_Delete(events);
            return json_error_local("skill_edit scope must be public or private");
        }
    }

    const char* out_content = (content && content->valuestring) ? content->valuestring : target_content->valuestring;

    cJSON* tags_out = cJSON_Duplicate(target_tags, 1);
    if (!tags_out || !cJSON_IsArray(tags_out)) {
        cJSON_Delete(args);
        cJSON_Delete(events);
        cJSON_Delete(tags_out);
        return json_error_local("skill_edit failed to duplicate existing tags");
    }

    if (upsert_string_tag_local(tags_out, "d", d->valuestring) != 0) {
        cJSON_Delete(args);
        cJSON_Delete(events);
        cJSON_Delete(tags_out);
        return json_error_local("skill_edit failed to set d tag");
    }
    if (upsert_string_tag_local(tags_out, "app", "didactyl") != 0) {
        cJSON_Delete(args);
        cJSON_Delete(events);
        cJSON_Delete(tags_out);
        return json_error_local("skill_edit failed to set app tag");
    }
    if (upsert_string_tag_local(tags_out, "scope", out_kind == 31123 ? "public" : "private") != 0) {
        cJSON_Delete(args);
        cJSON_Delete(events);
        cJSON_Delete(tags_out);
        return json_error_local("skill_edit failed to set scope tag");
    }

    if (description) {
        if (description->valuestring[0] == '\0') {
            remove_tag_key_all_local(tags_out, "description");
        } else if (upsert_string_tag_local(tags_out, "description", description->valuestring) != 0) {
            cJSON_Delete(args);
            cJSON_Delete(events);
            cJSON_Delete(tags_out);
            return json_error_local("skill_edit failed to set description tag");
        }
    }

    cJSON* existing_trigger = find_tag_value_string_local(tags_out, "trigger");
    cJSON* existing_filter = find_tag_value_string_local(tags_out, "filter");
    cJSON* existing_action = find_tag_value_string_local(tags_out, "action");
    cJSON* existing_enabled = find_tag_value_string_local(tags_out, "enabled");

    const char* merged_trigger = (trigger && trigger->valuestring) ? trigger->valuestring
                                : (existing_trigger && cJSON_IsString(existing_trigger) ? existing_trigger->valuestring : NULL);
    const char* merged_filter = (filter && filter->valuestring) ? filter->valuestring
                               : (existing_filter && cJSON_IsString(existing_filter) ? existing_filter->valuestring : NULL);
    const char* merged_action = (action && action->valuestring) ? action->valuestring
                               : (existing_action && cJSON_IsString(existing_action) && existing_action->valuestring[0] != '\0'
                                    ? existing_action->valuestring
                                    : "llm");

    int merged_enabled = (enabled && cJSON_IsBool(enabled)) ? (cJSON_IsTrue(enabled) ? 1 : 0)
                        : (existing_enabled && cJSON_IsString(existing_enabled) && existing_enabled->valuestring &&
                           strcmp(existing_enabled->valuestring, "false") == 0)
                            ? 0
                            : 1;

    int trigger_patch_requested = (trigger || filter || action || enabled || llm || tools || max_tokens || temperature || seed) ? 1 : 0;
    if (trigger_patch_requested || (merged_trigger || merged_filter)) {
        if ((merged_trigger && !merged_filter) || (!merged_trigger && merged_filter)) {
            cJSON_Delete(args);
            cJSON_Delete(events);
            cJSON_Delete(tags_out);
            return json_error_local("skill_edit trigger and filter must both be set together");
        }
        remove_tag_key_all_local(tags_out, "trigger");
        remove_tag_key_all_local(tags_out, "filter");
        remove_tag_key_all_local(tags_out, "action");
        remove_tag_key_all_local(tags_out, "enabled");

        if (merged_trigger && merged_filter) {
            if (strcmp(merged_trigger, "nostr-subscription") != 0 &&
                strcmp(merged_trigger, "webhook") != 0 &&
                strcmp(merged_trigger, "cron") != 0 &&
                strcmp(merged_trigger, "chain") != 0 &&
                strcmp(merged_trigger, "dm") != 0) {
                cJSON_Delete(args);
                cJSON_Delete(events);
                cJSON_Delete(tags_out);
                return json_error_local("skill_edit trigger must be one of: nostr-subscription, webhook, cron, chain, dm");
            }
            if (add_string_tag_local(tags_out, "trigger", merged_trigger) != 0 ||
                add_string_tag_local(tags_out, "filter", merged_filter) != 0 ||
                add_string_tag_local(tags_out, "action", merged_action) != 0 ||
                add_string_tag_local(tags_out, "enabled", merged_enabled ? "true" : "false") != 0) {
                cJSON_Delete(args);
                cJSON_Delete(events);
                cJSON_Delete(tags_out);
                return json_error_local("skill_edit failed to set trigger tags");
            }

            if (llm && llm->valuestring && llm->valuestring[0] != '\0' &&
                add_string_tag_local(tags_out, "llm", llm->valuestring) != 0) {
                cJSON_Delete(args);
                cJSON_Delete(events);
                cJSON_Delete(tags_out);
                return json_error_local("skill_edit failed to set llm tag");
            }
            if (tools && tools->valuestring && tools->valuestring[0] != '\0' &&
                add_string_tag_local(tags_out, "tools", tools->valuestring) != 0) {
                cJSON_Delete(args);
                cJSON_Delete(events);
                cJSON_Delete(tags_out);
                return json_error_local("skill_edit failed to set tools tag");
            }
            if (max_tokens) {
                char buf[32];
                snprintf(buf, sizeof(buf), "%d", (int)max_tokens->valuedouble);
                if (add_string_tag_local(tags_out, "max_tokens", buf) != 0) {
                    cJSON_Delete(args);
                    cJSON_Delete(events);
                    cJSON_Delete(tags_out);
                    return json_error_local("skill_edit failed to set max_tokens tag");
                }
            }
            if (temperature) {
                char buf[32];
                snprintf(buf, sizeof(buf), "%g", temperature->valuedouble);
                if (add_string_tag_local(tags_out, "temperature", buf) != 0) {
                    cJSON_Delete(args);
                    cJSON_Delete(events);
                    cJSON_Delete(tags_out);
                    return json_error_local("skill_edit failed to set temperature tag");
                }
            }
            if (seed) {
                char buf[32];
                snprintf(buf, sizeof(buf), "%d", (int)seed->valuedouble);
                if (add_string_tag_local(tags_out, "seed", buf) != 0) {
                    cJSON_Delete(args);
                    cJSON_Delete(events);
                    cJSON_Delete(tags_out);
                    return json_error_local("skill_edit failed to set seed tag");
                }
            }
        }
    }

    nostr_publish_result_t publish_result;
    memset(&publish_result, 0, sizeof(publish_result));
    int rc = nostr_handler_publish_kind_event(out_kind, out_content, tags_out, &publish_result);
    cJSON_Delete(tags_out);
    cJSON_Delete(events);

    if (rc != 0) {
        cJSON_Delete(args);
        nostr_handler_publish_result_free(&publish_result);
        return json_error_local("skill_edit failed to publish updated skill");
    }

    int trigger_registered = 0;
    if (ctx->trigger_manager) {
        if (merged_trigger && merged_filter && merged_enabled) {
            if (strcmp(merged_action, "template") == 0) {
                DEBUG_WARN("[didactyl] skill_edit trigger action template is deprecated; forcing llm for d_tag=%s", d->valuestring);
            }
            if (trigger_manager_update(ctx->trigger_manager,
                                       d->valuestring,
                                       out_content,
                                       merged_filter,
                                       TRIGGER_ACTION_LLM,
                                       merged_trigger,
                                       merged_enabled,
                                       (llm && llm->valuestring) ? llm->valuestring : NULL,
                                       (tools && tools->valuestring) ? tools->valuestring : NULL,
                                       max_tokens ? 1 : 0,
                                       max_tokens ? (int)max_tokens->valuedouble : 0,
                                       temperature ? 1 : 0,
                                       temperature ? temperature->valuedouble : 0.0,
                                       seed ? 1 : 0,
                                       seed ? (int)seed->valuedouble : 0) == 0 ||
                trigger_manager_add(ctx->trigger_manager,
                                    d->valuestring,
                                    out_content,
                                    merged_filter,
                                    TRIGGER_ACTION_LLM,
                                    merged_trigger,
                                    merged_enabled,
                                    (llm && llm->valuestring) ? llm->valuestring : NULL,
                                    (tools && tools->valuestring) ? tools->valuestring : NULL,
                                    max_tokens ? 1 : 0,
                                    max_tokens ? (int)max_tokens->valuedouble : 0,
                                    temperature ? 1 : 0,
                                    temperature ? temperature->valuedouble : 0.0,
                                    seed ? 1 : 0,
                                    seed ? (int)seed->valuedouble : 0) == 0) {
                trigger_registered = 1;
            }
        } else {
            (void)trigger_manager_remove(ctx->trigger_manager, d->valuestring);
        }
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        cJSON_Delete(args);
        nostr_handler_publish_result_free(&publish_result);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
    cJSON_AddStringToObject(out, "d_tag", d->valuestring);
    cJSON_AddNumberToObject(out, "kind", out_kind);
    cJSON_AddStringToObject(out, "scope", out_kind == 31123 ? "public" : "private");
    cJSON_AddStringToObject(out, "skill_event_id", publish_result.event_id);
    cJSON_AddBoolToObject(out, "trigger_registered", trigger_registered ? 1 : 0);
    if (publish_result.naddr_uri[0] != '\0') {
        cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
    }

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    cJSON_Delete(args);
    nostr_handler_publish_result_free(&publish_result);
    return json;
}

char* execute_skill_search(const char* args_json) {
    cJSON* args = parse_args_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* query = cJSON_GetObjectItemCaseSensitive(args, "query");
    cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
    cJSON* popular = cJSON_GetObjectItemCaseSensitive(args, "popular");

    if (query && (!cJSON_IsString(query) || !query->valuestring)) {
        cJSON_Delete(args);
        return json_error_local("skill_search query must be string when provided");
    }
    if (pubkey && (!cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len_local(pubkey->valuestring, 64U))) {
        cJSON_Delete(args);
        return json_error_local("skill_search pubkey must be 64-char hex string when provided");
    }

    int do_popular = (popular && cJSON_IsBool(popular) && cJSON_IsTrue(popular)) ? 1 : 0;
    const char* query_str = (query && query->valuestring) ? query->valuestring : NULL;

    if (do_popular) {
        cJSON* filter = cJSON_CreateObject();
        cJSON* kinds = cJSON_CreateArray();
        if (!filter || !kinds) {
            cJSON_Delete(filter);
            cJSON_Delete(kinds);
            cJSON_Delete(args);
            return json_error_local("failed to create skill_search popularity filter");
        }
        cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
        cJSON_AddItemToObject(filter, "kinds", kinds);
        cJSON_AddNumberToObject(filter, "limit", 300);

        char* events_json = nostr_handler_query_json(filter, 8000);
        cJSON_Delete(filter);
        cJSON_Delete(args);
        if (!events_json) return json_error_local("skill_search popularity query failed");

        cJSON* events = cJSON_Parse(events_json);
        free(events_json);
        if (!events || !cJSON_IsArray(events)) {
            cJSON_Delete(events);
            return json_error_local("skill_search popularity returned invalid JSON");
        }

        typedef struct {
            char* addr;
            int count;
        } skill_count_t;

        skill_count_t* counts = NULL;
        int count_len = 0;

        int event_n = cJSON_GetArraySize(events);
        for (int i = 0; i < event_n; i++) {
            cJSON* ev = cJSON_GetArrayItem(events, i);
            cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;
            if (!tags || !cJSON_IsArray(tags)) continue;

            int tn = cJSON_GetArraySize(tags);
            for (int t = 0; t < tn; t++) {
                cJSON* tuple = cJSON_GetArrayItem(tags, t);
                if (!tuple || !cJSON_IsArray(tuple) || cJSON_GetArraySize(tuple) < 2) continue;
                cJSON* k = cJSON_GetArrayItem(tuple, 0);
                cJSON* v = cJSON_GetArrayItem(tuple, 1);
                if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue;
                if (strcmp(k->valuestring, "a") != 0) continue;
                if (strncmp(v->valuestring, "31123:", 6) != 0) continue;
                if (pubkey && !strstr(v->valuestring, pubkey->valuestring)) continue;
                if (query_str && !ci_contains_local(v->valuestring, query_str)) continue;

                int found = -1;
                for (int j = 0; j < count_len; j++) {
                    if (strcmp(counts[j].addr, v->valuestring) == 0) {
                        found = j;
                        break;
                    }
                }

                if (found >= 0) {
                    counts[found].count++;
                } else {
                    skill_count_t* bigger = (skill_count_t*)realloc(counts, (size_t)(count_len + 1) * sizeof(skill_count_t));
                    if (!bigger) {
                        for (int j = 0; j < count_len; j++) free(counts[j].addr);
                        free(counts);
                        cJSON_Delete(events);
                        return json_error_local("allocation failure");
                    }
                    counts = bigger;
                    counts[count_len].addr = strdup(v->valuestring);
                    if (!counts[count_len].addr) {
                        for (int j = 0; j < count_len; j++) free(counts[j].addr);
                        free(counts);
                        cJSON_Delete(events);
                        return json_error_local("allocation failure");
                    }
                    counts[count_len].count = 1;
                    count_len++;
                }
            }
        }

        for (int i = 0; i < count_len; i++) {
            for (int j = i + 1; j < count_len; j++) {
                if (counts[j].count > counts[i].count) {
                    skill_count_t tmp = counts[i];
                    counts[i] = counts[j];
                    counts[j] = tmp;
                }
            }
        }

        cJSON* out = cJSON_CreateObject();
        cJSON* items = cJSON_CreateArray();
        if (!out || !items) {
            cJSON_Delete(out);
            cJSON_Delete(items);
            for (int j = 0; j < count_len; j++) free(counts[j].addr);
            free(counts);
            cJSON_Delete(events);
            return NULL;
        }

        for (int i = 0; i < count_len; i++) {
            cJSON* it = cJSON_CreateObject();
            if (!it) continue;
            cJSON_AddStringToObject(it, "address", counts[i].addr);
            cJSON_AddNumberToObject(it, "adoption_count", counts[i].count);
            cJSON_AddItemToArray(items, it);
        }

        cJSON_AddBoolToObject(out, "success", 1);
        cJSON_AddBoolToObject(out, "popular", 1);
        cJSON_AddItemToObject(out, "results", items);
        cJSON_AddNumberToObject(out, "count", count_len);

        char* json = cJSON_PrintUnformatted(out);
        cJSON_Delete(out);
        for (int j = 0; j < count_len; j++) free(counts[j].addr);
        free(counts);
        cJSON_Delete(events);
        return json;
    }

    cJSON* filter = cJSON_CreateObject();
    cJSON* kinds = cJSON_CreateArray();
    if (!filter || !kinds) {
        cJSON_Delete(filter);
        cJSON_Delete(kinds);
        cJSON_Delete(args);
        return json_error_local("failed to create skill_search filter");
    }

    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123));
    cJSON_AddItemToObject(filter, "kinds", kinds);

    if (pubkey) {
        cJSON* authors = cJSON_CreateArray();
        if (!authors) {
            cJSON_Delete(filter);
            cJSON_Delete(args);
            return json_error_local("failed to create skill_search authors");
        }
        cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey->valuestring));
        cJSON_AddItemToObject(filter, "authors", authors);
    }
    cJSON_AddNumberToObject(filter, "limit", 200);

    char* events_json = nostr_handler_query_json(filter, 8000);
    cJSON_Delete(filter);
    cJSON_Delete(args);
    if (!events_json) return json_error_local("skill_search query failed");

    cJSON* events = cJSON_Parse(events_json);
    free(events_json);
    if (!events || !cJSON_IsArray(events)) {
        cJSON_Delete(events);
        return json_error_local("skill_search returned invalid JSON");
    }

    cJSON* out = cJSON_CreateObject();
    cJSON* results = cJSON_CreateArray();
    if (!out || !results) {
        cJSON_Delete(out);
        cJSON_Delete(results);
        cJSON_Delete(events);
        return NULL;
    }

    int n = cJSON_GetArraySize(events);
    for (int i = 0; i < n; i++) {
        cJSON* ev = cJSON_GetArrayItem(events, i);
        cJSON* summary = extract_skill_summary_local(ev);
        if (!summary) continue;

        if (query_str && query_str[0] != '\0') {
            cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(summary, "d_tag");
            cJSON* desc = cJSON_GetObjectItemCaseSensitive(summary, "description");
            cJSON* preview = cJSON_GetObjectItemCaseSensitive(summary, "content_preview");
            int match = 0;
            if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring && ci_contains_local(d_tag->valuestring, query_str)) match = 1;
            if (!match && desc && cJSON_IsString(desc) && desc->valuestring && ci_contains_local(desc->valuestring, query_str)) match = 1;
            if (!match && preview && cJSON_IsString(preview) && preview->valuestring && ci_contains_local(preview->valuestring, query_str)) match = 1;
            if (!match) {
                cJSON_Delete(summary);
                continue;
            }
        }

        cJSON_AddItemToArray(results, summary);
    }

    cJSON_AddBoolToObject(out, "success", 1);
    cJSON_AddBoolToObject(out, "popular", 0);
    cJSON_AddItemToObject(out, "results", results);
    cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(results));

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    cJSON_Delete(events);
    return json;
}
