#define _POSIX_C_SOURCE 200809L

#include "nostr_handler.h"

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

#include "../../nostr_core_lib/cjson/cJSON.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#include "debug.h"
#include "trigger_manager.h"
#include "nostr_block_list.h"
#include "cashu_wallet.h"

#define NIP17_MAX_RELAYS 32
#define NIP17_MAX_GIFT_WRAPS 8

static didactyl_config_t* g_cfg = NULL;
static nostr_relay_pool_t* g_pool = NULL;
static dm_callback_t g_dm_callback = NULL;
static void* g_dm_user_data = NULL;
static int g_poll_counter = 0;
static time_t g_start_time = 0;
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
static char* g_system_context = NULL;
static unsigned char* g_startup_published = NULL;
static int g_startup_publish_tracking_enabled = 0;
static int g_startup_kind1_already_exists = 0;
static char g_startup_display_name[128] = "Didactyl";

static char* g_admin_kind0_json = NULL;
static char* g_admin_kind10002_json = NULL;
static char** g_admin_wot_contacts = NULL;
static int g_admin_wot_contact_count = 0;

typedef struct {
    time_t created_at;
    char* content;
} admin_kind1_note_t;

static admin_kind1_note_t* g_admin_kind1_notes = NULL;
static int g_admin_kind1_note_count = 0;

static char* g_agent_kind0_json = NULL;
static char* g_agent_kind10002_json = NULL;
static char** g_agent_contacts = NULL;
static int g_agent_contact_count = 0;
static admin_kind1_note_t* g_agent_kind1_notes = NULL;
static int g_agent_kind1_note_count = 0;

static pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER;

typedef enum {
    MANAGED_SUB_ADMIN_PROFILE = 0,
    MANAGED_SUB_ADMIN_NOTES,
    MANAGED_SUB_AGENT_PROFILE,
    MANAGED_SUB_AGENT_NOTES,
    MANAGED_SUB_SELF_SKILLS,
    MANAGED_SUB_ADMIN_SKILLS,
    MANAGED_SUB_DM_KIND4,
    MANAGED_SUB_DM_KIND1059,
    MANAGED_SUB_WALLET,
    MANAGED_SUB_COUNT
} managed_subscription_id_t;

typedef struct {
    const char* name;
    nostr_pool_subscription_t* handle;
    cJSON* filter;
    void (*on_event)(cJSON* event, const char* relay_url, void* user_data);
    void (*on_eose)(cJSON** events, int event_count, void* user_data);
    void* user_data;
    int close_on_eose;
    int enable_deduplication;
    nostr_pool_eose_result_mode_t result_mode;
    int relay_timeout_seconds;
    int eose_timeout_seconds;
    int enabled;
} managed_subscription_t;

typedef struct external_subscription_wrapper {
    nostr_pool_subscription_t* handle;
    void (*on_event)(cJSON* event, const char* relay_url, void* user_data);
    void (*on_eose)(cJSON** events, int event_count, void* user_data);
    void* user_data;
    struct external_subscription_wrapper* next;
} external_subscription_wrapper_t;

static managed_subscription_t g_managed_subs[MANAGED_SUB_COUNT];
static int g_managed_subs_initialized = 0;
static pthread_mutex_t g_subscription_mutex = PTHREAD_MUTEX_INITIALIZER;

static external_subscription_wrapper_t* g_external_sub_wrappers = NULL;

static cJSON* g_self_skill_events = NULL;
static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;

#define DM_DEDUP_CACHE_SIZE 256

static nostr_self_skill_eose_cb_t g_self_skill_eose_cb = NULL;
static void* g_self_skill_eose_user_data = NULL;

static struct trigger_manager* g_trigger_manager = NULL;

static char g_seen_dm_ids[DM_DEDUP_CACHE_SIZE][65];
static int g_seen_dm_count = 0;
static int g_seen_dm_next = 0;

static pthread_mutex_t g_dm_dedup_mutex = PTHREAD_MUTEX_INITIALIZER;

#define SENDER_PROTOCOL_CACHE_SIZE 128
#define DM_HISTORY_RING_SIZE 256

static int didactyl_debug_to_nostr_log_level(void) {
    switch (g_debug_level) {
        case DEBUG_LEVEL_TRACE: return NOSTR_LOG_LEVEL_TRACE;
        case DEBUG_LEVEL_DEBUG: return NOSTR_LOG_LEVEL_DEBUG;
        case DEBUG_LEVEL_INFO: return NOSTR_LOG_LEVEL_INFO;
        case DEBUG_LEVEL_WARN: return NOSTR_LOG_LEVEL_WARN;
        case DEBUG_LEVEL_ERROR: return NOSTR_LOG_LEVEL_ERROR;
        case DEBUG_LEVEL_NONE:
        default:
            return NOSTR_LOG_LEVEL_ERROR;
    }
}

static debug_level_t nostr_log_to_didactyl_level(int level) {
    switch (level) {
        case NOSTR_LOG_LEVEL_TRACE: return DEBUG_LEVEL_TRACE;
        case NOSTR_LOG_LEVEL_DEBUG: return DEBUG_LEVEL_DEBUG;
        case NOSTR_LOG_LEVEL_INFO: return DEBUG_LEVEL_INFO;
        case NOSTR_LOG_LEVEL_WARN: return DEBUG_LEVEL_WARN;
        case NOSTR_LOG_LEVEL_ERROR:
        default:
            return DEBUG_LEVEL_ERROR;
    }
}

static void nostr_core_log_bridge(int level, const char* component, const char* message, void* user_data) {
    (void)user_data;

    debug_level_t mapped = nostr_log_to_didactyl_level(level);
    const char* comp = (component && component[0] != '\0') ? component : "core";
    const char* msg = message ? message : "";

    switch (mapped) {
        case DEBUG_LEVEL_TRACE:
            DEBUG_TRACE("[didactyl] [nostr:%s] %s", comp, msg);
            break;
        case DEBUG_LEVEL_DEBUG:
            DEBUG_LOG("[didactyl] [nostr:%s] %s", comp, msg);
            break;
        case DEBUG_LEVEL_INFO:
            DEBUG_INFO("[didactyl] [nostr:%s] %s", comp, msg);
            break;
        case DEBUG_LEVEL_WARN:
            DEBUG_WARN("[didactyl] [nostr:%s] %s", comp, msg);
            break;
        case DEBUG_LEVEL_ERROR:
        default:
            DEBUG_ERROR("[didactyl] [nostr:%s] %s", comp, msg);
            break;
    }
}

#if defined(__GNUC__) || defined(__clang__)
extern void nostr_set_log_callback(nostr_log_callback_t callback, void* user_data) __attribute__((weak));
extern void nostr_set_log_level(nostr_log_level_t level) __attribute__((weak));
#endif

static void nostr_core_set_log_bridge_if_supported(void) {
#if defined(__GNUC__) || defined(__clang__)
    if (nostr_set_log_callback) {
        nostr_set_log_callback(nostr_core_log_bridge, NULL);
    }
    if (nostr_set_log_level) {
        nostr_set_log_level((nostr_log_level_t)didactyl_debug_to_nostr_log_level());
    }
#endif
}

static void nostr_core_clear_log_bridge_if_supported(void) {
#if defined(__GNUC__) || defined(__clang__)
    if (nostr_set_log_callback) {
        nostr_set_log_callback(NULL, NULL);
    }
#endif
}

typedef struct {
    char pubkey_hex[65];
    dm_protocol_t protocol;
    time_t seen_at;
} sender_protocol_entry_t;

typedef struct {
    time_t created_at;
    int incoming;
    char peer_pubkey_hex[65];
    char* content;
} dm_history_entry_t;

static sender_protocol_entry_t g_sender_protocol_cache[SENDER_PROTOCOL_CACHE_SIZE];
static pthread_mutex_t g_sender_protocol_mutex = PTHREAD_MUTEX_INITIALIZER;

static dm_history_entry_t g_dm_history_ring[DM_HISTORY_RING_SIZE];
static int g_dm_history_count = 0;
static int g_dm_history_next = 0;
static pthread_mutex_t g_dm_history_mutex = PTHREAD_MUTEX_INITIALIZER;

static int dm_id_seen_or_remember(const char* event_id_hex) {
    if (!event_id_hex || strlen(event_id_hex) != 64U) {
        return 0;
    }

    int seen = 0;
    pthread_mutex_lock(&g_dm_dedup_mutex);

    for (int i = 0; i < g_seen_dm_count; i++) {
        if (strncmp(g_seen_dm_ids[i], event_id_hex, 64U) == 0) {
            seen = 1;
            break;
        }
    }

    if (!seen) {
        int slot = 0;
        if (g_seen_dm_count < DM_DEDUP_CACHE_SIZE) {
            slot = g_seen_dm_count;
            g_seen_dm_count++;
        } else {
            slot = g_seen_dm_next;
            g_seen_dm_next = (g_seen_dm_next + 1) % DM_DEDUP_CACHE_SIZE;
        }

        memcpy(g_seen_dm_ids[slot], event_id_hex, 64U);
        g_seen_dm_ids[slot][64] = '\0';
    }

    pthread_mutex_unlock(&g_dm_dedup_mutex);
    return seen;
}

static didactyl_sender_tier_t sender_tier_from_pubkey(const char* sender_pubkey_hex) {
    if (!g_cfg || !sender_pubkey_hex) {
        return DIDACTYL_SENDER_STRANGER;
    }

    if (strcmp(sender_pubkey_hex, g_cfg->admin.pubkey) == 0) {
        return DIDACTYL_SENDER_ADMIN;
    }

    if (g_cfg->security.wot.enabled && nostr_handler_is_wot_contact(sender_pubkey_hex)) {
        return DIDACTYL_SENDER_WOT;
    }

    return DIDACTYL_SENDER_STRANGER;
}

static void sender_protocol_remember(const char* sender_pubkey_hex, dm_protocol_t protocol) {
    if (!sender_pubkey_hex || strlen(sender_pubkey_hex) != 64U) {
        return;
    }

    if (protocol != DM_PROTOCOL_NIP04 && protocol != DM_PROTOCOL_NIP17) {
        return;
    }

    pthread_mutex_lock(&g_sender_protocol_mutex);

    int slot = -1;
    time_t oldest_time = 0;
    int oldest_idx = 0;

    for (int i = 0; i < SENDER_PROTOCOL_CACHE_SIZE; i++) {
        if (g_sender_protocol_cache[i].pubkey_hex[0] == '\0') {
            slot = i;
            break;
        }

        if (strncmp(g_sender_protocol_cache[i].pubkey_hex, sender_pubkey_hex, 64U) == 0) {
            slot = i;
            break;
        }

        if (i == 0 || g_sender_protocol_cache[i].seen_at < oldest_time) {
            oldest_time = g_sender_protocol_cache[i].seen_at;
            oldest_idx = i;
        }
    }

    if (slot < 0) {
        slot = oldest_idx;
    }

    memcpy(g_sender_protocol_cache[slot].pubkey_hex, sender_pubkey_hex, 64U);
    g_sender_protocol_cache[slot].pubkey_hex[64] = '\0';
    g_sender_protocol_cache[slot].protocol = protocol;
    g_sender_protocol_cache[slot].seen_at = time(NULL);

    pthread_mutex_unlock(&g_sender_protocol_mutex);
}

static dm_protocol_t sender_protocol_lookup(const char* sender_pubkey_hex) {
    if (!sender_pubkey_hex || strlen(sender_pubkey_hex) != 64U) {
        return DM_PROTOCOL_NIP04;
    }

    dm_protocol_t out = DM_PROTOCOL_NIP04;

    pthread_mutex_lock(&g_sender_protocol_mutex);
    for (int i = 0; i < SENDER_PROTOCOL_CACHE_SIZE; i++) {
        if (g_sender_protocol_cache[i].pubkey_hex[0] == '\0') {
            continue;
        }
        if (strncmp(g_sender_protocol_cache[i].pubkey_hex, sender_pubkey_hex, 64U) == 0) {
            out = g_sender_protocol_cache[i].protocol;
            break;
        }
    }
    pthread_mutex_unlock(&g_sender_protocol_mutex);

    return out;
}

static void dm_history_clear_locked(void) {
    for (int i = 0; i < DM_HISTORY_RING_SIZE; i++) {
        free(g_dm_history_ring[i].content);
        g_dm_history_ring[i].content = NULL;
        g_dm_history_ring[i].created_at = 0;
        g_dm_history_ring[i].incoming = 0;
        g_dm_history_ring[i].peer_pubkey_hex[0] = '\0';
    }
    g_dm_history_count = 0;
    g_dm_history_next = 0;
}

static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at) {
    if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U || !content) {
        return;
    }

    char* dup = strdup(content);
    if (!dup) {
        return;
    }

    pthread_mutex_lock(&g_dm_history_mutex);

    int slot = 0;
    if (g_dm_history_count < DM_HISTORY_RING_SIZE) {
        slot = g_dm_history_count;
        g_dm_history_count++;
    } else {
        slot = g_dm_history_next;
        g_dm_history_next = (g_dm_history_next + 1) % DM_HISTORY_RING_SIZE;
    }

    free(g_dm_history_ring[slot].content);
    g_dm_history_ring[slot].content = dup;
    g_dm_history_ring[slot].created_at = (created_at > 0) ? created_at : time(NULL);
    g_dm_history_ring[slot].incoming = incoming ? 1 : 0;
    memcpy(g_dm_history_ring[slot].peer_pubkey_hex, peer_pubkey_hex, 64U);
    g_dm_history_ring[slot].peer_pubkey_hex[64] = '\0';

    pthread_mutex_unlock(&g_dm_history_mutex);
}

static const char* relay_status_str(nostr_pool_relay_status_t status) {
    switch (status) {
        case NOSTR_POOL_RELAY_DISCONNECTED:
            return "disconnected";
        case NOSTR_POOL_RELAY_CONNECTING:
            return "connecting";
        case NOSTR_POOL_RELAY_CONNECTED:
            return "connected";
        case NOSTR_POOL_RELAY_ERROR:
            return "error";
        default:
            return "unknown";
    }
}

static void publish_pending_startup_events_for_relay_index(int relay_index, const char* reason);
static int publish_kind_event_to_relays(int kind,
                                        const char* content,
                                        cJSON* tags,
                                        const char** relay_urls,
                                        int relay_count,
                                        const char* reason_label,
                                        nostr_publish_result_t* out_result);
static void on_admin_context_event(cJSON* event, const char* relay_url, void* user_data);
static int parse_kind3_wot_contacts(cJSON* tags);
static int parse_kind10002_relays(cJSON* tags);
static void upsert_kind1_note(time_t created_at, const char* content);
static void free_agent_context_locked(void);
static int parse_kind3_agent_contacts(cJSON* tags);
static int parse_kind10002_agent_relays(cJSON* tags);
static void upsert_agent_kind1_note(time_t created_at, const char* content);
static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data);
static int startup_self_kind1_exists(void);
static void load_startup_display_name(void);
static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback);
static void register_trigger_from_self_skill_event(cJSON* event);
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data);
static void dm_history_clear_locked(void);
static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at);
static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url);
static void free_relay_list_local(char** relays, int relay_count);
static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count);
static int merge_relays_into_runtime_config(char** discovered_relays, int discovered_count);
static void managed_subscriptions_init_defaults(void);
static int managed_subscription_close_locked(managed_subscription_id_t id);
static int managed_subscription_apply_locked(managed_subscription_id_t id);
static int managed_subscription_register_locked(managed_subscription_id_t id,
                                               cJSON* filter,
                                               void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
                                               void (*on_eose)(cJSON** events, int event_count, void* user_data),
                                               void* user_data,
                                               int close_on_eose,
                                               int enable_deduplication,
                                               nostr_pool_eose_result_mode_t result_mode,
                                               int relay_timeout_seconds,
                                               int eose_timeout_seconds,
                                               int enabled);
static int managed_subscriptions_resubscribe_all_locked(void);
static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id);
static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label);
static void seed_default_skill_into_cache(void);

static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url) {
    if (!relays || relay_count <= 0 || !relay_url || relay_url[0] == '\0') {
        return 0;
    }

    for (int i = 0; i < relay_count; i++) {
        if (relays[i] && strcmp(relays[i], relay_url) == 0) {
            return 1;
        }
    }

    return 0;
}

static void free_relay_list_local(char** relays, int relay_count) {
    if (!relays) {
        return;
    }

    for (int i = 0; i < relay_count; i++) {
        free(relays[i]);
    }
    free(relays);
}

static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count) {
    if (!out_relays || !out_relay_count) {
        return -1;
    }

    *out_relays = NULL;
    *out_relay_count = 0;

    if (!tags || !cJSON_IsArray(tags)) {
        return 0;
    }

    char** relays = NULL;
    int relay_count = 0;

    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* key = cJSON_GetArrayItem(tag, 0);
        cJSON* val = cJSON_GetArrayItem(tag, 1);
        if (!key || !val || !cJSON_IsString(key) || !key->valuestring ||
            !cJSON_IsString(val) || !val->valuestring || val->valuestring[0] == '\0') {
            continue;
        }

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

        if (relay_list_contains_local(relays, relay_count, val->valuestring)) {
            continue;
        }

        char** grown = (char**)realloc(relays, (size_t)(relay_count + 1) * sizeof(char*));
        if (!grown) {
            free_relay_list_local(relays, relay_count);
            return -1;
        }
        relays = grown;

        relays[relay_count] = strdup(val->valuestring);
        if (!relays[relay_count]) {
            free_relay_list_local(relays, relay_count);
            return -1;
        }

        relay_count++;
    }

    *out_relays = relays;
    *out_relay_count = relay_count;
    return relay_count;
}

static int merge_relays_into_runtime_config(char** discovered_relays, int discovered_count) {
    if (!g_cfg || discovered_count < 0) {
        return -1;
    }

    int old_count = g_cfg->relay_count;
    int max_count = old_count + discovered_count;
    if (max_count <= 0) {
        return -1;
    }

    char** merged = (char**)calloc((size_t)max_count, sizeof(char*));
    if (!merged) {
        return -1;
    }

    int merged_count = 0;
    for (int i = 0; i < old_count; i++) {
        const char* relay = (g_cfg->relays && g_cfg->relays[i]) ? g_cfg->relays[i] : NULL;
        if (!relay || relay[0] == '\0' || relay_list_contains_local(merged, merged_count, relay)) {
            continue;
        }

        merged[merged_count] = strdup(relay);
        if (!merged[merged_count]) {
            free_relay_list_local(merged, merged_count);
            return -1;
        }
        merged_count++;
    }

    int existing_unique_count = merged_count;

    for (int i = 0; i < discovered_count; i++) {
        const char* relay = (discovered_relays && discovered_relays[i]) ? discovered_relays[i] : NULL;
        if (!relay || relay[0] == '\0' || relay_list_contains_local(merged, merged_count, relay)) {
            continue;
        }

        merged[merged_count] = strdup(relay);
        if (!merged[merged_count]) {
            free_relay_list_local(merged, merged_count);
            return -1;
        }
        merged_count++;
    }

    if (merged_count <= 0) {
        free(merged);
        return -1;
    }

    if (g_cfg->relays) {
        for (int i = 0; i < g_cfg->relay_count; i++) {
            free(g_cfg->relays[i]);
        }
        free(g_cfg->relays);
    }

    g_cfg->relays = merged;
    g_cfg->relay_count = merged_count;
    return merged_count - existing_unique_count;
}

static void managed_subscriptions_init_defaults(void) {
    if (g_managed_subs_initialized) {
        return;
    }

    memset(g_managed_subs, 0, sizeof(g_managed_subs));

    g_managed_subs[MANAGED_SUB_ADMIN_PROFILE].name = "admin_context_profile";
    g_managed_subs[MANAGED_SUB_ADMIN_NOTES].name = "admin_context_notes";
    g_managed_subs[MANAGED_SUB_AGENT_PROFILE].name = "agent_context_profile";
    g_managed_subs[MANAGED_SUB_AGENT_NOTES].name = "agent_context_notes";
    g_managed_subs[MANAGED_SUB_SELF_SKILLS].name = "self_skills";
    g_managed_subs[MANAGED_SUB_ADMIN_SKILLS].name = "admin_skills";
    g_managed_subs[MANAGED_SUB_DM_KIND4].name = "dms_kind4";
    g_managed_subs[MANAGED_SUB_DM_KIND1059].name = "dms_kind1059";
    g_managed_subs[MANAGED_SUB_WALLET].name = "wallet_events";

    g_managed_subs_initialized = 1;
}

static int managed_subscription_close_locked(managed_subscription_id_t id) {
    if (id < 0 || id >= MANAGED_SUB_COUNT) {
        return -1;
    }

    managed_subscription_t* sub = &g_managed_subs[id];
    if (sub->handle) {
        (void)nostr_pool_subscription_close(sub->handle);
        sub->handle = NULL;
    }

    return 0;
}

static void managed_subscription_on_event_filtered(cJSON* event, const char* relay_url, void* user_data) {
    managed_subscription_t* sub = (managed_subscription_t*)user_data;
    if (!sub || !sub->on_event) return;
    if (nostr_block_list_is_event_filtered(event)) return;
    sub->on_event(event, relay_url, sub->user_data);
}

static void managed_subscription_on_eose_passthrough(cJSON** events, int event_count, void* user_data) {
    managed_subscription_t* sub = (managed_subscription_t*)user_data;
    if (!sub || !sub->on_eose) return;
    sub->on_eose(events, event_count, sub->user_data);
}

static int managed_subscription_apply_locked(managed_subscription_id_t id) {
    if (id < 0 || id >= MANAGED_SUB_COUNT || !g_cfg || !g_pool) {
        return -1;
    }

    managed_subscription_t* sub = &g_managed_subs[id];
    (void)managed_subscription_close_locked(id);

    if (!sub->enabled) {
        return 0;
    }

    if (!sub->filter || !sub->on_event) {
        return -1;
    }

    cJSON* filter_copy = cJSON_Duplicate(sub->filter, 1);
    if (!filter_copy) {
        return -1;
    }

    sub->handle = nostr_relay_pool_subscribe(
        g_pool,
        (const char**)g_cfg->relays,
        g_cfg->relay_count,
        filter_copy,
        managed_subscription_on_event_filtered,
        sub->on_eose ? managed_subscription_on_eose_passthrough : NULL,
        sub,
        sub->close_on_eose,
        sub->enable_deduplication,
        sub->result_mode,
        sub->relay_timeout_seconds,
        sub->eose_timeout_seconds);

    cJSON_Delete(filter_copy);

    if (!sub->handle) {
        return -1;
    }

    return 0;
}

static int managed_subscription_register_locked(managed_subscription_id_t id,
                                               cJSON* filter,
                                               void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
                                               void (*on_eose)(cJSON** events, int event_count, void* user_data),
                                               void* user_data,
                                               int close_on_eose,
                                               int enable_deduplication,
                                               nostr_pool_eose_result_mode_t result_mode,
                                               int relay_timeout_seconds,
                                               int eose_timeout_seconds,
                                               int enabled) {
    if (id < 0 || id >= MANAGED_SUB_COUNT) {
        return -1;
    }

    managed_subscriptions_init_defaults();

    managed_subscription_t* sub = &g_managed_subs[id];

    cJSON* filter_dup = NULL;
    if (filter) {
        filter_dup = cJSON_Duplicate(filter, 1);
        if (!filter_dup) {
            return -1;
        }
    }

    cJSON_Delete(sub->filter);
    sub->filter = filter_dup;
    sub->on_event = on_event;
    sub->on_eose = on_eose;
    sub->user_data = user_data;
    sub->close_on_eose = close_on_eose;
    sub->enable_deduplication = enable_deduplication;
    sub->result_mode = result_mode;
    sub->relay_timeout_seconds = relay_timeout_seconds;
    sub->eose_timeout_seconds = eose_timeout_seconds;
    sub->enabled = enabled ? 1 : 0;

    return managed_subscription_apply_locked(id);
}

static int managed_subscriptions_resubscribe_all_locked(void) {
    int rc = 0;
    managed_subscriptions_init_defaults();
    for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
        if (managed_subscription_apply_locked((managed_subscription_id_t)i) != 0) {
            rc = -1;
        }
    }
    return rc;
}

static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id) {
    if (!name || !out_id) {
        return -1;
    }

    managed_subscriptions_init_defaults();

    for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
        if (g_managed_subs[i].name && strcmp(g_managed_subs[i].name, name) == 0) {
            *out_id = (managed_subscription_id_t)i;
            return 0;
        }
    }

    return -1;
}

static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label) {
    if (!g_cfg || !g_pool || !tags || !cJSON_IsArray(tags)) {
        return 0;
    }

    char** discovered_relays = NULL;
    int discovered_count = 0;
    if (extract_relays_from_kind10002_tags(tags, &discovered_relays, &discovered_count) < 0) {
        return -1;
    }

    if (discovered_count <= 0) {
        free(discovered_relays);
        return 0;
    }

    int added_relays = merge_relays_into_runtime_config(discovered_relays, discovered_count);
    free_relay_list_local(discovered_relays, discovered_count);

    if (added_relays < 0) {
        return -1;
    }

    if (added_relays == 0) {
        return 0;
    }

    if (nostr_handler_sync_relays_from_config() != 0) {
        DEBUG_WARN("[didactyl] runtime relay sync failed after kind10002 update (%s)",
                   source_label ? source_label : "unknown");
        return -1;
    }

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    int sub_rc = managed_subscriptions_resubscribe_all_locked();
    pthread_mutex_unlock(&g_subscription_mutex);

    if (sub_rc != 0) {
        DEBUG_WARN("[didactyl] some subscriptions failed to resubscribe after relay update (%s)",
                   source_label ? source_label : "unknown");
    }

    DEBUG_INFO("[didactyl] runtime relay sync from kind10002 (%s): discovered=%d added=%d total=%d",
               source_label ? source_label : "unknown",
               discovered_count,
               added_relays,
               g_cfg->relay_count);

    return 0;
}

static void log_relay_state_changes(void) {
    if (!g_pool || !g_cfg || !g_last_relay_statuses) {
        return;
    }

    for (int i = 0; i < g_cfg->relay_count; i++) {
        const char* relay = g_cfg->relays[i];
        nostr_pool_relay_status_t now = nostr_relay_pool_get_relay_status(g_pool, relay);
        nostr_pool_relay_status_t prev = g_last_relay_statuses[i];
        if (now != prev) {
            const char* conn_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, relay);
            if (conn_err && (now == NOSTR_POOL_RELAY_DISCONNECTED || now == NOSTR_POOL_RELAY_ERROR)) {
                DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s (cause: %s)",
                           relay,
                           relay_status_str(prev),
                           relay_status_str(now),
                           conn_err);
            } else {
                DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s",
                           relay,
                           relay_status_str(prev),
                           relay_status_str(now));
            }
            g_last_relay_statuses[i] = now;

            if (now == NOSTR_POOL_RELAY_CONNECTED) {
                publish_pending_startup_events_for_relay_index(i, "relay_connected");
            }
        }
    }
}

void nostr_handler_refresh_relay_statuses(void) {
    if (!g_pool || !g_cfg || !g_last_relay_statuses) {
        return;
    }

    for (int i = 0; i < g_cfg->relay_count; i++) {
        g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]);
    }
}

int nostr_handler_sync_relays_from_config(void) {
    if (!g_cfg || !g_pool || g_cfg->relay_count <= 0 || !g_cfg->relays) {
        return -1;
    }

    for (int i = 0; i < g_cfg->relay_count; i++) {
        const char* relay = g_cfg->relays[i];
        if (!relay || relay[0] == '\0') {
            continue;
        }
        if (nostr_relay_pool_add_relay(g_pool, relay) != NOSTR_SUCCESS) {
            DEBUG_WARN("[didactyl] failed to add discovered relay to pool: %s", relay);
        }
    }

    free(g_last_relay_statuses);
    g_last_relay_statuses = (nostr_pool_relay_status_t*)calloc((size_t)g_cfg->relay_count,
                                                                sizeof(nostr_pool_relay_status_t));
    if (!g_last_relay_statuses) {
        return -1;
    }

    for (int i = 0; i < g_cfg->relay_count; i++) {
        g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]);
    }

    return 0;
}

static void log_publish_targets(const char* action) {
    if (!g_cfg) {
        return;
    }

    DEBUG_INFO("[didactyl] %s target relays (%d):", action ? action : "publish", g_cfg->relay_count);
    for (int i = 0; i < g_cfg->relay_count; i++) {
        DEBUG_INFO("[didactyl]   -> %s", g_cfg->relays[i]);
    }
}

static int startup_self_kind1_exists(void) {
    if (!g_cfg || !g_pool) {
        return 0;
    }

    cJSON* filter = cJSON_CreateObject();
    cJSON* kinds = cJSON_CreateArray();
    cJSON* authors = cJSON_CreateArray();
    if (!filter || !kinds || !authors) {
        cJSON_Delete(filter);
        cJSON_Delete(kinds);
        cJSON_Delete(authors);
        return 0;
    }

    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
    cJSON_AddItemToObject(filter, "authors", authors);
    cJSON_AddNumberToObject(filter, "limit", 1);

    int event_count = 0;
    cJSON** events = nostr_relay_pool_query_sync(
        g_pool,
        (const char**)g_cfg->relays,
        g_cfg->relay_count,
        filter,
        &event_count,
        3000);

    cJSON_Delete(filter);

    if (events) {
        for (int i = 0; i < event_count; i++) {
            cJSON_Delete(events[i]);
        }
        free(events);
    }

    return event_count > 0 ? 1 : 0;
}

static void load_startup_display_name(void) {
    snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", "Didactyl");

    if (!g_cfg) {
        return;
    }

    for (int i = 0; i < g_cfg->startup_event_count; i++) {
        startup_event_t* se = &g_cfg->startup_events[i];
        if (se->kind != 0 || !se->content) {
            continue;
        }

        cJSON* content_json = cJSON_Parse(se->content);
        if (!content_json || !cJSON_IsObject(content_json)) {
            cJSON_Delete(content_json);
            continue;
        }

        cJSON* display_name = cJSON_GetObjectItemCaseSensitive(content_json, "display_name");
        cJSON* name = cJSON_GetObjectItemCaseSensitive(content_json, "name");

        if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
            snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", display_name->valuestring);
            cJSON_Delete(content_json);
            return;
        }

        if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
            snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", name->valuestring);
            cJSON_Delete(content_json);
            return;
        }

        cJSON_Delete(content_json);
    }
}

static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback) {
    if (!out || out_size == 0) {
        return;
    }

    if (fallback && fallback[0] != '\0') {
        snprintf(out, out_size, "%s (%s startup)", fallback, g_startup_display_name);
    } else {
        snprintf(out, out_size, "%s startup complete and online", g_startup_display_name);
    }
}

static const char* find_tag_value_local(cJSON* tags, const char* key) {
    if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') {
        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) || !k->valuestring ||
            !cJSON_IsString(v) || !v->valuestring) {
            continue;
        }

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

    return NULL;
}

static int parse_enabled_flag_local(const char* enabled_s) {
    if (!enabled_s || enabled_s[0] == '\0') {
        return 1;
    }
    return (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
}

static int parse_skill_address_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;

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

    size_t pub_len = (size_t)(p2 - (p1 + 1));
    if (pub_len != 64U) {
        return -1;
    }

    size_t d_len = strlen(p2 + 1);
    if (d_len == 0U || d_len >= 65U) {
        return -1;
    }

    memcpy(out_pubkey_hex, p1 + 1, pub_len);
    out_pubkey_hex[pub_len] = '\0';
    memcpy(out_d_tag, p2 + 1, d_len + 1U);

    *out_kind = kind;
    return 0;
}

static int self_skill_is_adopted_local(int kind, const char* pubkey_hex, const char* d_tag) {
    if ((kind != 31123 && kind != 31124) || !pubkey_hex || !d_tag || d_tag[0] == '\0') {
        return 0;
    }

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

    int adopted = 0;
    pthread_mutex_lock(&g_self_skill_mutex);

    if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
        int n = cJSON_GetArraySize(g_self_skill_events);
        for (int i = 0; i < n && !adopted; i++) {
            cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
            cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
            cJSON* ev_pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL;
            cJSON* ev_tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL;

            if (!ev_kind || !cJSON_IsNumber(ev_kind) || (int)ev_kind->valuedouble != 10123 ||
                !ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
                strcmp(ev_pubkey->valuestring, pubkey_hex) != 0 ||
                !ev_tags || !cJSON_IsArray(ev_tags)) {
                continue;
            }

            int tag_count = cJSON_GetArraySize(ev_tags);
            for (int ti = 0; ti < tag_count; ti++) {
                cJSON* tag = cJSON_GetArrayItem(ev_tags, ti);
                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) {
                    adopted = 1;
                    break;
                }
            }
        }
    }

    pthread_mutex_unlock(&g_self_skill_mutex);
    return adopted;
}

static void register_trigger_from_self_skill_event(cJSON* event) {
    if (!event || !g_cfg || !g_trigger_manager) {
        return;
    }

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

    if (!kind || !cJSON_IsNumber(kind) ||
        !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
        !content || !cJSON_IsString(content) || !content->valuestring ||
        !tags || !cJSON_IsArray(tags)) {
        return;
    }

    int kind_val = (int)kind->valuedouble;
    if (kind_val != 31123 && kind_val != 31124) {
        return;
    }

    if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
        return;
    }

    const char* d_tag = find_tag_value_local(tags, "d");
    const char* trigger = find_tag_value_local(tags, "trigger");
    const char* filter = find_tag_value_local(tags, "filter");
    const char* action = find_tag_value_local(tags, "action");
    const char* enabled = find_tag_value_local(tags, "enabled");

    int trigger_supported = trigger &&
                            (strcmp(trigger, "nostr-subscription") == 0 ||
                             strcmp(trigger, "webhook") == 0 ||
                             strcmp(trigger, "cron") == 0 ||
                             strcmp(trigger, "chain") == 0 ||
                             strcmp(trigger, "dm") == 0);
    if (!d_tag || d_tag[0] == '\0' ||
        !trigger_supported ||
        !filter || filter[0] == '\0') {
        return;
    }

    if (!self_skill_is_adopted_local(kind_val, pubkey->valuestring, d_tag)) {
        DEBUG_INFO("[didactyl] live self-skill trigger ignored (not adopted) d_tag=%s", d_tag);
        return;
    }

    if (action && strcmp(action, "template") == 0) {
        DEBUG_WARN("[didactyl] live self-skill trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
    }
    trigger_action_type_t action_type = TRIGGER_ACTION_LLM;
    int is_enabled = parse_enabled_flag_local(enabled);

    const char* llm = find_tag_value_local(tags, "llm");
    const char* tools = find_tag_value_local(tags, "tools");
    const char* max_tokens_s = find_tag_value_local(tags, "max_tokens");
    const char* temperature_s = find_tag_value_local(tags, "temperature");
    const char* seed_s = find_tag_value_local(tags, "seed");

    int has_max_tokens = (max_tokens_s && max_tokens_s[0] != '\0') ? 1 : 0;
    int max_tokens = has_max_tokens ? atoi(max_tokens_s) : 0;
    int has_temperature = (temperature_s && temperature_s[0] != '\0') ? 1 : 0;
    double temperature = has_temperature ? atof(temperature_s) : 0.0;
    int has_seed = (seed_s && seed_s[0] != '\0') ? 1 : 0;
    int seed = has_seed ? atoi(seed_s) : 0;

    int rc = trigger_manager_add((trigger_manager_t*)g_trigger_manager,
                                 d_tag,
                                 content->valuestring,
                                 filter,
                                 action_type,
                                 trigger,
                                 is_enabled,
                                 llm,
                                 tools,
                                 has_max_tokens,
                                 max_tokens,
                                 has_temperature,
                                 temperature,
                                 has_seed,
                                 seed);
    if (rc == 0) {
        DEBUG_INFO("[didactyl] live self-skill trigger registered d_tag=%s action=%s enabled=%d",
                   d_tag,
                   "llm",
                   is_enabled);
    } else {
        DEBUG_WARN("[didactyl] live self-skill trigger register failed d_tag=%s", d_tag);
    }
}

static void self_skill_cache_upsert_event_locked(cJSON* event) {
    if (!event || !cJSON_IsObject(event)) {
        return;
    }

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

    if (!kind || !cJSON_IsNumber(kind) ||
        !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
        !tags || !cJSON_IsArray(tags)) {
        return;
    }

    int kind_val = (int)kind->valuedouble;
    if (kind_val != 31123 && kind_val != 31124 && kind_val != 10123) {
        return;
    }

    if (!g_cfg) {
        return;
    }

    int is_agent_author = (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0);
    int is_admin_author = (g_cfg->admin.pubkey[0] != '\0' && strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0);
    if (!is_agent_author && !is_admin_author) {
        return;
    }

    if (is_admin_author && kind_val != 31123) {
        return;
    }

    const char* d_tag = find_tag_value_local(tags, "d");
    if ((kind_val == 31123 || kind_val == 31124) && (!d_tag || d_tag[0] == '\0')) {
        return;
    }

    if (!g_self_skill_events || !cJSON_IsArray(g_self_skill_events)) {
        cJSON_Delete(g_self_skill_events);
        g_self_skill_events = cJSON_CreateArray();
        if (!g_self_skill_events) {
            return;
        }
    }

    double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;

    int n = cJSON_GetArraySize(g_self_skill_events);
    for (int i = 0; i < n; i++) {
        cJSON* existing = cJSON_GetArrayItem(g_self_skill_events, i);
        if (!existing || !cJSON_IsObject(existing)) {
            continue;
        }

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

        int ex_kind_val = (int)ex_kind->valuedouble;
        if (ex_kind_val != kind_val || strcmp(ex_pubkey->valuestring, pubkey->valuestring) != 0) {
            continue;
        }

        if (kind_val == 31123 || kind_val == 31124) {
            const char* ex_d = find_tag_value_local(ex_tags, "d");
            if (!ex_d || !d_tag || strcmp(ex_d, d_tag) != 0) {
                continue;
            }
        }

        cJSON* ex_created_at = cJSON_GetObjectItemCaseSensitive(existing, "created_at");
        double ex_created = (ex_created_at && cJSON_IsNumber(ex_created_at)) ? ex_created_at->valuedouble : 0.0;
        if (created < ex_created) {
            return;
        }

        cJSON* dup = cJSON_Duplicate(event, 1);
        if (!dup) {
            return;
        }

        cJSON_ReplaceItemInArray(g_self_skill_events, i, dup);
        return;
    }

    cJSON* dup = cJSON_Duplicate(event, 1);
    if (!dup) {
        return;
    }
    cJSON_AddItemToArray(g_self_skill_events, dup);
}

static void seed_default_skill_into_cache(void) {
    if (!g_cfg) {
        return;
    }

    if (g_cfg->default_skill.kind != 31123 && g_cfg->default_skill.kind != 31124) {
        return;
    }

    if (g_cfg->keys.public_key_hex[0] == '\0' ||
        !g_cfg->default_skill.content || g_cfg->default_skill.content[0] == '\0' ||
        !g_cfg->default_skill.tags_json || g_cfg->default_skill.tags_json[0] == '\0') {
        return;
    }

    cJSON* tags = cJSON_Parse(g_cfg->default_skill.tags_json);
    if (!tags || !cJSON_IsArray(tags)) {
        cJSON_Delete(tags);
        return;
    }

    cJSON* event = cJSON_CreateObject();
    if (!event) {
        cJSON_Delete(tags);
        return;
    }

    const double now = (double)time(NULL);
    cJSON_AddNumberToObject(event, "kind", g_cfg->default_skill.kind);
    cJSON_AddStringToObject(event, "pubkey", g_cfg->keys.public_key_hex);
    cJSON_AddStringToObject(event, "content", g_cfg->default_skill.content);
    cJSON_AddItemToObject(event, "tags", tags);
    cJSON_AddNumberToObject(event, "created_at", now);
    cJSON_AddStringToObject(event,
                            "id",
                            "0000000000000000000000000000000000000000000000000000000000000000");

    pthread_mutex_lock(&g_self_skill_mutex);
    self_skill_cache_upsert_event_locked(event);
    pthread_mutex_unlock(&g_self_skill_mutex);

    cJSON_Delete(event);
}

static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
    if (!hex || !out_pubkey || strlen(hex) != 64U) {
        return -1;
    }
    return nostr_hex_to_bytes(hex, out_pubkey, 32) == 0 ? 0 : -1;
}

static int duplicate_relay_list(const char** relay_urls, int relay_count, char*** out_relays) {
    if (!out_relays) {
        return -1;
    }
    *out_relays = NULL;
    if (!relay_urls || relay_count <= 0) {
        return 0;
    }

    char** relays = (char**)calloc((size_t)relay_count, sizeof(char*));
    if (!relays) {
        return -1;
    }

    for (int i = 0; i < relay_count; i++) {
        relays[i] = strdup(relay_urls[i] ? relay_urls[i] : "");
        if (!relays[i]) {
            for (int j = 0; j < i; j++) {
                free(relays[j]);
            }
            free(relays);
            return -1;
        }
    }

    *out_relays = relays;
    return 0;
}

void nostr_handler_publish_result_free(nostr_publish_result_t* result) {
    if (!result) {
        return;
    }

    if (result->relays) {
        for (int i = 0; i < result->relay_count; i++) {
            free(result->relays[i]);
        }
        free(result->relays);
    }

    memset(result, 0, sizeof(*result));
}

static void extract_d_tag(cJSON* tags, char* out_d_tag, size_t out_size) {
    if (!out_d_tag || out_size == 0) {
        return;
    }
    out_d_tag[0] = '\0';

    if (!tags || !cJSON_IsArray(tags)) {
        return;
    }

    int tag_count = cJSON_GetArraySize(tags);
    for (int i = 0; i < tag_count; i++) {
        cJSON* tag = cJSON_GetArrayItem(tags, i);
        if (!tag || !cJSON_IsArray(tag)) {
            continue;
        }

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

        if (strcmp(key->valuestring, "d") == 0) {
            snprintf(out_d_tag, out_size, "%s", value->valuestring);
            return;
        }
    }
}

static void fill_publish_result(nostr_publish_result_t* out_result,
                                int kind,
                                cJSON* tags,
                                const char* event_id_hex,
                                const char** relay_urls,
                                int relay_count,
                                int accepted_by_pool_count) {
    if (!out_result) {
        return;
    }

    nostr_handler_publish_result_free(out_result);
    out_result->kind = kind;
    out_result->relay_count = relay_count;
    out_result->accepted_by_pool_count = accepted_by_pool_count;
    out_result->success = accepted_by_pool_count > 0 ? 1 : 0;

    if (event_id_hex) {
        snprintf(out_result->event_id, sizeof(out_result->event_id), "%s", event_id_hex);

        unsigned char event_id_bytes[32];
        char note_bech32[128];
        if (strlen(event_id_hex) == 64U &&
            nostr_hex_to_bytes(event_id_hex, event_id_bytes, sizeof(event_id_bytes)) == 0 &&
            nostr_key_to_bech32(event_id_bytes, "note", note_bech32) == 0) {
            snprintf(out_result->note_uri, sizeof(out_result->note_uri), "nostr:%s", note_bech32);
        }
    }

    extract_d_tag(tags, out_result->d_tag, sizeof(out_result->d_tag));

    (void)kind;
    (void)relay_urls;

    (void)duplicate_relay_list(relay_urls, relay_count, &out_result->relays);
}

static cJSON* create_dm_tags_for_recipient(const char* recipient_pubkey_hex) {
    cJSON* tags = cJSON_CreateArray();
    if (!tags) {
        return NULL;
    }

    cJSON* p_tag = cJSON_CreateArray();
    if (!p_tag) {
        cJSON_Delete(tags);
        return NULL;
    }

    cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
    cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkey_hex));
    cJSON_AddItemToArray(tags, p_tag);
    return tags;
}

static int extract_first_p_tag(cJSON* tags, char out_pubkey_hex[65]) {
    if (!tags || !cJSON_IsArray(tags)) {
        return -1;
    }

    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* key = cJSON_GetArrayItem(tag, 0);
        cJSON* val = cJSON_GetArrayItem(tag, 1);
        if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val)) {
            continue;
        }

        if (strcmp(key->valuestring, "p") == 0 && strlen(val->valuestring) == 64U) {
            memcpy(out_pubkey_hex, val->valuestring, 65U);
            return 0;
        }
    }

    return -1;
}

static void trace_event_json(const char* prefix, cJSON* event) {
    if (g_debug_level < DEBUG_LEVEL_TRACE || !event) {
        return;
    }

    char* event_json = cJSON_PrintUnformatted(event);
    if (!event_json) {
        DEBUG_TRACE("[didactyl] %s <failed to serialize event>", prefix ? prefix : "event");
        return;
    }

    DEBUG_TRACE("[didactyl] %s %s", prefix ? prefix : "event", event_json);
    free(event_json);
}

static void trace_plaintext_dm(const char* prefix, const char* plaintext) {
    if (g_debug_level < DEBUG_LEVEL_TRACE) {
        return;
    }
    DEBUG_TRACE("[didactyl] %s %s", prefix ? prefix : "dm plaintext", plaintext ? plaintext : "");
}

static void on_event(cJSON* event, const char* relay_url, void* user_data) {
    (void)user_data;

    DEBUG_TRACE("[didactyl] DEBUG on_event ENTRY from %s (event=%p g_cfg=%p g_dm_callback=%s)",
                relay_url ? relay_url : "NULL", (void*)event, (void*)g_cfg,
                g_dm_callback ? "set" : "NULL");

    if (!event || !g_cfg || !g_dm_callback) {
        DEBUG_TRACE("[didactyl] DEBUG on_event NULL guard: event=%p g_cfg=%p g_dm_callback=%s",
                    (void*)event, (void*)g_cfg, g_dm_callback ? "set" : "NULL");
        return;
    }

    if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) {
        DEBUG_WARN("[didactyl] dropped event with invalid signature via %s",
                   relay_url ? relay_url : "unknown relay");
        return;
    }

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

    if (!kind || !pubkey || !content || !tags ||
        !cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !cJSON_IsString(content)) {
        DEBUG_TRACE("[didactyl] DEBUG on_event: missing required fields (kind=%p pubkey=%p content=%p tags=%p)",
                    (void*)kind, (void*)pubkey, (void*)content, (void*)tags);
        return;
    }

    const char* event_id_hex = (id && cJSON_IsString(id) && id->valuestring && strlen(id->valuestring) == 64U)
                                   ? id->valuestring
                                   : NULL;
    int kind_val = (int)kind->valuedouble;

    DEBUG_TRACE("[didactyl] DEBUG on_event: kind=%d id=%.16s... from=%.16s... via %s",
                kind_val,
                event_id_hex ? event_id_hex : "<no-id>",
                pubkey->valuestring ? pubkey->valuestring : "<no-pk>",
                relay_url ? relay_url : "unknown");

    char sender_pubkey_hex[65] = {0};
    char* decrypted = NULL;
    const char* dedup_id_hex = event_id_hex;
    dm_protocol_t received_protocol = DM_PROTOCOL_NIP04;

    if (kind_val == 4) {
        if (g_cfg->dm_protocol == DM_PROTOCOL_NIP17) {
            DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring kind4 in dm_protocol=nip17 mode");
            return;
        }

        char recipient_pubkey_hex[65] = {0};
        if (extract_first_p_tag(tags, recipient_pubkey_hex) != 0) {
            DEBUG_TRACE("[didactyl] DEBUG on_event: no p-tag found in kind4 event %.16s...",
                        event_id_hex ? event_id_hex : "<no-id>");
            return;
        }

        if (strcmp(recipient_pubkey_hex, g_cfg->keys.public_key_hex) != 0) {
            DEBUG_TRACE("[didactyl] DEBUG on_event: p-tag mismatch (got=%.16s... want=%.16s...)",
                        recipient_pubkey_hex, g_cfg->keys.public_key_hex);
            return;
        }

        memcpy(sender_pubkey_hex, pubkey->valuestring, 65U);

        unsigned char sender_pubkey[32];
        if (hex_to_pubkey(sender_pubkey_hex, sender_pubkey) != 0) {
            return;
        }

        decrypted = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
        if (!decrypted) {
            fprintf(stderr, "[didactyl] failed to allocate DM decrypt buffer\n");
            return;
        }
        decrypted[0] = '\0';

        trace_event_json("received encrypted DM event:", event);

        if (nostr_nip04_decrypt(g_cfg->keys.private_key, sender_pubkey, content->valuestring, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
            fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s...\n", sender_pubkey_hex);
            free(decrypted);
            return;
        }

        trace_plaintext_dm("received decrypted DM content:", decrypted);
        received_protocol = DM_PROTOCOL_NIP04;
    } else if (kind_val == 1059) {
        if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) {
            DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring kind1059 in dm_protocol=nip04 mode");
            return;
        }

        cJSON* rumor = nostr_nip17_receive_dm(event, g_cfg->keys.private_key);
        if (!rumor) {
            DEBUG_TRACE("[didactyl] DEBUG on_event: failed to unwrap/decrypt NIP-17 gift wrap %.16s...",
                        event_id_hex ? event_id_hex : "<no-id>");
            return;
        }

        cJSON* rumor_id = cJSON_GetObjectItemCaseSensitive(rumor, "id");
        cJSON* rumor_kind = cJSON_GetObjectItemCaseSensitive(rumor, "kind");
        cJSON* rumor_pubkey = cJSON_GetObjectItemCaseSensitive(rumor, "pubkey");
        cJSON* rumor_content = cJSON_GetObjectItemCaseSensitive(rumor, "content");
        cJSON* rumor_created_at = cJSON_GetObjectItemCaseSensitive(rumor, "created_at");

        if (!rumor_kind || !rumor_pubkey || !rumor_content || !rumor_created_at ||
            !cJSON_IsNumber(rumor_kind) || !cJSON_IsString(rumor_pubkey) || !cJSON_IsString(rumor_content) ||
            !cJSON_IsNumber(rumor_created_at) ||
            !rumor_pubkey->valuestring || strlen(rumor_pubkey->valuestring) != 64U) {
            cJSON_Delete(rumor);
            return;
        }

        time_t rumor_created_at_ts = (time_t)rumor_created_at->valuedouble;
        if (rumor_created_at_ts < g_start_time) {
            DEBUG_TRACE("[didactyl] DEBUG on_event: skipping old NIP-17 rumor created_at=%ld start=%ld",
                        (long)rumor_created_at_ts,
                        (long)g_start_time);
            cJSON_Delete(rumor);
            return;
        }

        int rumor_kind_val = (int)rumor_kind->valuedouble;
        if (rumor_kind_val != 14 && rumor_kind_val != 15 && rumor_kind_val != 7) {
            DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring NIP-17 rumor kind=%d", rumor_kind_val);
            cJSON_Delete(rumor);
            return;
        }

        memcpy(sender_pubkey_hex, rumor_pubkey->valuestring, 65U);

        const char* rumor_id_hex = (rumor_id && cJSON_IsString(rumor_id) && rumor_id->valuestring && strlen(rumor_id->valuestring) == 64U)
                                      ? rumor_id->valuestring
                                      : NULL;
        if (rumor_id_hex) {
            dedup_id_hex = rumor_id_hex;
        }

        decrypted = strdup(rumor_content->valuestring ? rumor_content->valuestring : "");
        cJSON_Delete(rumor);
        if (!decrypted) {
            return;
        }

        trace_plaintext_dm("received NIP-17 DM content:", decrypted);
        received_protocol = DM_PROTOCOL_NIP17;
    } else {
        DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring unsupported kind=%d", kind_val);
        return;
    }

    if (!sender_pubkey_hex[0] || strlen(sender_pubkey_hex) != 64U) {
        free(decrypted);
        return;
    }

    if (strcmp(sender_pubkey_hex, g_cfg->keys.public_key_hex) == 0) {
        DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring self-sent DM %.16s... via %s",
                    sender_pubkey_hex,
                    relay_url ? relay_url : "unknown relay");
        free(decrypted);
        return;
    }

    didactyl_sender_tier_t tier = sender_tier_from_pubkey(sender_pubkey_hex);

    DEBUG_TRACE("[didactyl] DEBUG on_event: sender=%.16s... tier=%d (admin=%.16s...)",
                sender_pubkey_hex, (int)tier, g_cfg->admin.pubkey);

    sender_protocol_remember(sender_pubkey_hex, received_protocol);

    if (tier == DIDACTYL_SENDER_STRANGER) {
        if (!g_cfg->security.stranger.enabled) {
            DEBUG_LOG("[didactyl] ignored DM from stranger %.16s... via %s",
                      sender_pubkey_hex,
                      relay_url ? relay_url : "unknown relay");
            free(decrypted);
            return;
        }

        if (g_cfg->security.stranger_response[0] != '\0') {
            (void)nostr_handler_send_dm_auto(sender_pubkey_hex, g_cfg->security.stranger_response);
        }
        free(decrypted);
        return;
    }

    if (dedup_id_hex && dm_id_seen_or_remember(dedup_id_hex)) {
        DEBUG_LOG("[didactyl] skipped duplicate DM event %.16s... from %.16s... via %s",
                  dedup_id_hex,
                  sender_pubkey_hex,
                  relay_url ? relay_url : "unknown relay");
        free(decrypted);
        return;
    }

    DEBUG_INFO("[didactyl] received kind %d event %.16s... from %.16s... via %s tier=%d protocol=%s",
               kind_val,
               dedup_id_hex ? dedup_id_hex : "<no-id>",
               sender_pubkey_hex,
               relay_url ? relay_url : "unknown relay",
               (int)tier,
               received_protocol == DM_PROTOCOL_NIP17 ? "nip17" : "nip04");

    dm_history_remember(sender_pubkey_hex,
                        decrypted,
                        1,
                        time(NULL));

    g_dm_callback(sender_pubkey_hex, decrypted, tier, g_dm_user_data);
    free(decrypted);
}

static void on_eose(cJSON** events, int event_count, void* user_data) {
    (void)events;
    (void)user_data;
    DEBUG_TRACE("[didactyl] DEBUG on_eose called: event_count=%d", event_count);
}

static void on_self_skill_eose(cJSON** events, int event_count, void* user_data) {
    (void)events;
    (void)user_data;
    DEBUG_INFO("[didactyl] self-skill EOSE received: cached skill events=%d", event_count);

    if (g_self_skill_eose_cb) {
        g_self_skill_eose_cb(event_count, g_self_skill_eose_user_data);
    }
}

static void free_admin_context_locked(void) {
    free(g_admin_kind0_json);
    g_admin_kind0_json = NULL;
    free(g_admin_kind10002_json);
    g_admin_kind10002_json = NULL;

    if (g_admin_wot_contacts) {
        for (int i = 0; i < g_admin_wot_contact_count; i++) {
            free(g_admin_wot_contacts[i]);
        }
        free(g_admin_wot_contacts);
    }
    g_admin_wot_contacts = NULL;
    g_admin_wot_contact_count = 0;

    if (g_admin_kind1_notes) {
        for (int i = 0; i < g_admin_kind1_note_count; i++) {
            free(g_admin_kind1_notes[i].content);
        }
        free(g_admin_kind1_notes);
    }
    g_admin_kind1_notes = NULL;
    g_admin_kind1_note_count = 0;
}

static void free_agent_context_locked(void) {
    free(g_agent_kind0_json);
    g_agent_kind0_json = NULL;
    free(g_agent_kind10002_json);
    g_agent_kind10002_json = NULL;

    if (g_agent_contacts) {
        for (int i = 0; i < g_agent_contact_count; i++) {
            free(g_agent_contacts[i]);
        }
        free(g_agent_contacts);
    }
    g_agent_contacts = NULL;
    g_agent_contact_count = 0;

    if (g_agent_kind1_notes) {
        for (int i = 0; i < g_agent_kind1_note_count; i++) {
            free(g_agent_kind1_notes[i].content);
        }
        free(g_agent_kind1_notes);
    }
    g_agent_kind1_notes = NULL;
    g_agent_kind1_note_count = 0;
}

static int parse_kind3_wot_contacts(cJSON* tags) {
    if (!tags || !cJSON_IsArray(tags)) {
        return 0;
    }

    if (g_admin_wot_contacts) {
        for (int i = 0; i < g_admin_wot_contact_count; i++) {
            free(g_admin_wot_contacts[i]);
        }
        free(g_admin_wot_contacts);
        g_admin_wot_contacts = NULL;
        g_admin_wot_contact_count = 0;
    }

    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* key = cJSON_GetArrayItem(tag, 0);
        cJSON* val = cJSON_GetArrayItem(tag, 1);
        if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
            continue;
        }

        if (strcmp(key->valuestring, "p") != 0 || strlen(val->valuestring) != 64U) {
            continue;
        }

        char* dup = strdup(val->valuestring);
        if (!dup) {
            return -1;
        }

        char** grown = (char**)realloc(g_admin_wot_contacts, (size_t)(g_admin_wot_contact_count + 1) * sizeof(char*));
        if (!grown) {
            free(dup);
            return -1;
        }

        g_admin_wot_contacts = grown;
        g_admin_wot_contacts[g_admin_wot_contact_count++] = dup;
    }

    return 0;
}

static int parse_kind10002_relays(cJSON* tags) {
    if (!tags || !cJSON_IsArray(tags)) {
        free(g_admin_kind10002_json);
        g_admin_kind10002_json = strdup("[]");
        return g_admin_kind10002_json ? 0 : -1;
    }

    cJSON* relays = cJSON_CreateArray();
    if (!relays) {
        return -1;
    }

    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* key = cJSON_GetArrayItem(tag, 0);
        cJSON* val = cJSON_GetArrayItem(tag, 1);
        if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
            continue;
        }

        if (strcmp(key->valuestring, "r") != 0 || val->valuestring[0] == '\0') {
            continue;
        }

        cJSON_AddItemToArray(relays, cJSON_CreateString(val->valuestring));
    }

    char* relays_json = cJSON_PrintUnformatted(relays);
    cJSON_Delete(relays);
    if (!relays_json) {
        return -1;
    }

    free(g_admin_kind10002_json);
    g_admin_kind10002_json = relays_json;
    return 0;
}

static void upsert_kind1_note(time_t created_at, const char* content) {
    if (!content) {
        return;
    }

    int limit = g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10;
    if (limit > 256) {
        limit = 256;
    }

    char* dup = strdup(content);
    if (!dup) {
        return;
    }

    admin_kind1_note_t* grown = (admin_kind1_note_t*)realloc(g_admin_kind1_notes,
                                                              (size_t)(g_admin_kind1_note_count + 1) * sizeof(admin_kind1_note_t));
    if (!grown) {
        free(dup);
        return;
    }

    g_admin_kind1_notes = grown;
    g_admin_kind1_notes[g_admin_kind1_note_count].created_at = created_at;
    g_admin_kind1_notes[g_admin_kind1_note_count].content = dup;
    g_admin_kind1_note_count++;

    while (g_admin_kind1_note_count > limit) {
        free(g_admin_kind1_notes[0].content);
        memmove(&g_admin_kind1_notes[0],
                &g_admin_kind1_notes[1],
                (size_t)(g_admin_kind1_note_count - 1) * sizeof(admin_kind1_note_t));
        g_admin_kind1_note_count--;
    }
}

static int parse_kind3_agent_contacts(cJSON* tags) {
    if (!tags || !cJSON_IsArray(tags)) {
        return 0;
    }

    if (g_agent_contacts) {
        for (int i = 0; i < g_agent_contact_count; i++) {
            free(g_agent_contacts[i]);
        }
        free(g_agent_contacts);
        g_agent_contacts = NULL;
        g_agent_contact_count = 0;
    }

    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* key = cJSON_GetArrayItem(tag, 0);
        cJSON* val = cJSON_GetArrayItem(tag, 1);
        if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
            continue;
        }

        if (strcmp(key->valuestring, "p") != 0 || strlen(val->valuestring) != 64U) {
            continue;
        }

        char* dup = strdup(val->valuestring);
        if (!dup) {
            return -1;
        }

        char** grown = (char**)realloc(g_agent_contacts, (size_t)(g_agent_contact_count + 1) * sizeof(char*));
        if (!grown) {
            free(dup);
            return -1;
        }

        g_agent_contacts = grown;
        g_agent_contacts[g_agent_contact_count++] = dup;
    }

    return 0;
}

static int parse_kind10002_agent_relays(cJSON* tags) {
    if (!tags || !cJSON_IsArray(tags)) {
        free(g_agent_kind10002_json);
        g_agent_kind10002_json = strdup("[]");
        return g_agent_kind10002_json ? 0 : -1;
    }

    cJSON* relays = cJSON_CreateArray();
    if (!relays) {
        return -1;
    }

    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* key = cJSON_GetArrayItem(tag, 0);
        cJSON* val = cJSON_GetArrayItem(tag, 1);
        if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
            continue;
        }

        if (strcmp(key->valuestring, "r") != 0 || val->valuestring[0] == '\0') {
            continue;
        }

        cJSON_AddItemToArray(relays, cJSON_CreateString(val->valuestring));
    }

    char* relays_json = cJSON_PrintUnformatted(relays);
    cJSON_Delete(relays);
    if (!relays_json) {
        return -1;
    }

    free(g_agent_kind10002_json);
    g_agent_kind10002_json = relays_json;
    return 0;
}

static void upsert_agent_kind1_note(time_t created_at, const char* content) {
    if (!content) {
        return;
    }

    int limit = g_cfg && g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10;
    if (limit > 256) {
        limit = 256;
    }

    char* dup = strdup(content);
    if (!dup) {
        return;
    }

    admin_kind1_note_t* grown = (admin_kind1_note_t*)realloc(g_agent_kind1_notes,
                                                              (size_t)(g_agent_kind1_note_count + 1) * sizeof(admin_kind1_note_t));
    if (!grown) {
        free(dup);
        return;
    }

    g_agent_kind1_notes = grown;
    g_agent_kind1_notes[g_agent_kind1_note_count].created_at = created_at;
    g_agent_kind1_notes[g_agent_kind1_note_count].content = dup;
    g_agent_kind1_note_count++;

    while (g_agent_kind1_note_count > limit) {
        free(g_agent_kind1_notes[0].content);
        memmove(&g_agent_kind1_notes[0],
                &g_agent_kind1_notes[1],
                (size_t)(g_agent_kind1_note_count - 1) * sizeof(admin_kind1_note_t));
        g_agent_kind1_note_count--;
    }
}

static void on_admin_context_event(cJSON* event, const char* relay_url, void* user_data) {
    (void)relay_url;
    (void)user_data;

    if (!event || !g_cfg || !g_cfg->admin_context.enabled) {
        return;
    }

    if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) {
        return;
    }

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

    if (!kind || !pubkey || !cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !pubkey->valuestring) {
        return;
    }

    if (strcmp(pubkey->valuestring, g_cfg->admin.pubkey) != 0) {
        return;
    }

    int k = (int)kind->valuedouble;

    pthread_mutex_lock(&g_admin_ctx_mutex);

    if (k == 0 && g_cfg->admin_context.track_kind_0 && content && cJSON_IsString(content) && content->valuestring) {
        free(g_admin_kind0_json);
        g_admin_kind0_json = strdup(content->valuestring);
    } else if (k == 3 && g_cfg->admin_context.track_kind_3 && tags && cJSON_IsArray(tags)) {
        (void)parse_kind3_wot_contacts(tags);
    } else if (k == 10002 && g_cfg->admin_context.track_kind_10002 && tags && cJSON_IsArray(tags)) {
        (void)parse_kind10002_relays(tags);
    } else if (k == 1 && g_cfg->admin_context.track_kind_1 && content && cJSON_IsString(content) && content->valuestring) {
        time_t ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : time(NULL);
        upsert_kind1_note(ts, content->valuestring);
    }

    pthread_mutex_unlock(&g_admin_ctx_mutex);
}

static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data) {
    (void)user_data;

    if (!event || !g_cfg) {
        return;
    }

    if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) {
        return;
    }

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

    if (!kind || !pubkey || !cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !pubkey->valuestring) {
        return;
    }

    if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
        return;
    }

    int k = (int)kind->valuedouble;
    int runtime_sync_needed = 0;

    pthread_mutex_lock(&g_admin_ctx_mutex);

    if (k == 0 && content && cJSON_IsString(content) && content->valuestring) {
        free(g_agent_kind0_json);
        g_agent_kind0_json = strdup(content->valuestring);
    } else if (k == 3 && tags && cJSON_IsArray(tags)) {
        (void)parse_kind3_agent_contacts(tags);
    } else if (k == 10002 && tags && cJSON_IsArray(tags)) {
        (void)parse_kind10002_agent_relays(tags);
        runtime_sync_needed = 1;
    } else if (k == 1 && content && cJSON_IsString(content) && content->valuestring) {
        time_t ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : time(NULL);
        upsert_agent_kind1_note(ts, content->valuestring);
    }

    pthread_mutex_unlock(&g_admin_ctx_mutex);

    if (runtime_sync_needed) {
        (void)apply_runtime_kind10002_relays(tags, relay_url ? relay_url : "agent_context");
    }
}

static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) {
    (void)relay_url;
    (void)user_data;

    if (!event || !g_cfg) {
        return;
    }

    if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) {
        return;
    }

    register_trigger_from_self_skill_event(event);

    pthread_mutex_lock(&g_self_skill_mutex);
    self_skill_cache_upsert_event_locked(event);
    pthread_mutex_unlock(&g_self_skill_mutex);
}

static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data) {
    (void)event;
    (void)relay_url;
    (void)user_data;
    (void)cashu_wallet_load_from_relays();
}

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

    g_cfg = config;
    g_poll_counter = 0;
    g_start_time = time(NULL);
    memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
    g_seen_dm_count = 0;
    g_seen_dm_next = 0;

    pthread_mutex_lock(&g_dm_history_mutex);
    dm_history_clear_locked();
    pthread_mutex_unlock(&g_dm_history_mutex);

    managed_subscriptions_init_defaults();
    pthread_mutex_lock(&g_subscription_mutex);
    for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
        managed_subscription_t* sub = &g_managed_subs[i];
        sub->enabled = 0;
        sub->on_event = NULL;
        sub->on_eose = NULL;
        sub->user_data = NULL;
        sub->close_on_eose = 0;
        sub->enable_deduplication = 0;
        sub->result_mode = NOSTR_POOL_EOSE_FULL_SET;
        sub->relay_timeout_seconds = 30;
        sub->eose_timeout_seconds = 120;
        (void)managed_subscription_close_locked((managed_subscription_id_t)i);
        cJSON_Delete(sub->filter);
        sub->filter = NULL;
    }
    pthread_mutex_unlock(&g_subscription_mutex);

    DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);

    nostr_core_set_log_bridge_if_supported();

    nostr_pool_reconnect_config_t reconnect = *nostr_pool_reconnect_config_default();
    reconnect.enable_auto_reconnect = 1;
    reconnect.ping_interval_seconds = 20;
    reconnect.pong_timeout_seconds = 10;

    g_pool = nostr_relay_pool_create(&reconnect);
    if (!g_pool) {
        return -1;
    }

    if (nostr_relay_pool_set_auth(g_pool, g_cfg->keys.private_key, 1) != NOSTR_SUCCESS) {
        fprintf(stderr, "[didactyl] failed to enable relay pool auth\n");
        return -1;
    }

    for (int i = 0; i < g_cfg->relay_count; i++) {
        if (nostr_relay_pool_add_relay(g_pool, g_cfg->relays[i]) != NOSTR_SUCCESS) {
            fprintf(stderr, "[didactyl] failed to add relay: %s\n", g_cfg->relays[i]);
            return -1;
        }
        DEBUG_INFO("[didactyl] added relay: %s", g_cfg->relays[i]);
    }

    free(g_last_relay_statuses);
    g_last_relay_statuses = (nostr_pool_relay_status_t*)calloc((size_t)g_cfg->relay_count, sizeof(nostr_pool_relay_status_t));
    if (!g_last_relay_statuses) {
        return -1;
    }
    for (int i = 0; i < g_cfg->relay_count; i++) {
        g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]);
    }

    free(g_startup_published);
    g_startup_published = NULL;
    g_startup_publish_tracking_enabled = 0;

    pthread_mutex_lock(&g_self_skill_mutex);
    cJSON_Delete(g_self_skill_events);
    g_self_skill_events = cJSON_CreateArray();
    pthread_mutex_unlock(&g_self_skill_mutex);
    if (!g_self_skill_events) {
        return -1;
    }

    return 0;
}

int nostr_handler_subscribe_admin_context(void) {
    if (!g_cfg || !g_pool || !g_cfg->admin_context.enabled) {
        return 0;
    }

    int rc = 0;

    cJSON* profile_filter = cJSON_CreateObject();
    cJSON* profile_kinds = cJSON_CreateArray();
    cJSON* profile_authors = cJSON_CreateArray();
    if (!profile_filter || !profile_kinds || !profile_authors) {
        cJSON_Delete(profile_filter);
        cJSON_Delete(profile_kinds);
        cJSON_Delete(profile_authors);
        return -1;
    }

    if (g_cfg->admin_context.track_kind_0) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(0));
    if (g_cfg->admin_context.track_kind_3) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(3));
    if (g_cfg->admin_context.track_kind_10002) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(10002));

    int profile_enabled = cJSON_GetArraySize(profile_kinds) > 0 ? 1 : 0;
    if (profile_enabled) {
        cJSON_AddItemToObject(profile_filter, "kinds", profile_kinds);
        cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->admin.pubkey));
        cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
        cJSON_AddNumberToObject(profile_filter, "limit", 32);
    } else {
        cJSON_Delete(profile_kinds);
        cJSON_Delete(profile_authors);
    }

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_PROFILE,
                                             profile_enabled ? profile_filter : NULL,
                                             on_admin_context_event,
                                             on_eose,
                                             NULL,
                                             0,
                                             1,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             profile_enabled) != 0) {
        rc = -1;
    }
    pthread_mutex_unlock(&g_subscription_mutex);
    cJSON_Delete(profile_filter);

    int notes_enabled = g_cfg->admin_context.track_kind_1 ? 1 : 0;
    cJSON* notes_filter = NULL;
    if (notes_enabled) {
        notes_filter = cJSON_CreateObject();
        cJSON* notes_kinds = cJSON_CreateArray();
        cJSON* notes_authors = cJSON_CreateArray();
        if (!notes_filter || !notes_kinds || !notes_authors) {
            cJSON_Delete(notes_filter);
            cJSON_Delete(notes_kinds);
            cJSON_Delete(notes_authors);
            return -1;
        }

        int kind1_limit = g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10;
        if (kind1_limit > 256) {
            kind1_limit = 256;
        }

        cJSON_AddItemToArray(notes_kinds, cJSON_CreateNumber(1));
        cJSON_AddItemToObject(notes_filter, "kinds", notes_kinds);
        cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->admin.pubkey));
        cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
        cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
    }

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_NOTES,
                                             notes_filter,
                                             on_admin_context_event,
                                             on_eose,
                                             NULL,
                                             0,
                                             1,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             notes_enabled) != 0) {
        rc = -1;
    }
    pthread_mutex_unlock(&g_subscription_mutex);
    cJSON_Delete(notes_filter);

    if (rc == 0) {
        DEBUG_INFO("[didactyl] admin context subscriptions active for admin %.16s...", g_cfg->admin.pubkey);
    }
    return rc;
}

int nostr_handler_subscribe_agent_context(void) {
    if (!g_cfg || !g_pool) {
        return -1;
    }

    int rc = 0;

    cJSON* profile_filter = cJSON_CreateObject();
    cJSON* profile_kinds = cJSON_CreateArray();
    cJSON* profile_authors = cJSON_CreateArray();
    if (!profile_filter || !profile_kinds || !profile_authors) {
        cJSON_Delete(profile_filter);
        cJSON_Delete(profile_kinds);
        cJSON_Delete(profile_authors);
        return -1;
    }

    cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(0));
    cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(3));
    cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(10002));
    cJSON_AddItemToObject(profile_filter, "kinds", profile_kinds);
    cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
    cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
    cJSON_AddNumberToObject(profile_filter, "limit", 64);

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    if (managed_subscription_register_locked(MANAGED_SUB_AGENT_PROFILE,
                                             profile_filter,
                                             on_agent_context_event,
                                             on_eose,
                                             NULL,
                                             0,
                                             1,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             1) != 0) {
        rc = -1;
    }
    pthread_mutex_unlock(&g_subscription_mutex);
    cJSON_Delete(profile_filter);

    cJSON* notes_filter = cJSON_CreateObject();
    cJSON* notes_kinds = cJSON_CreateArray();
    cJSON* notes_authors = cJSON_CreateArray();
    if (!notes_filter || !notes_kinds || !notes_authors) {
        cJSON_Delete(notes_filter);
        cJSON_Delete(notes_kinds);
        cJSON_Delete(notes_authors);
        return -1;
    }

    int kind1_limit = g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10;
    if (kind1_limit > 256) {
        kind1_limit = 256;
    }

    cJSON_AddItemToArray(notes_kinds, cJSON_CreateNumber(1));
    cJSON_AddItemToObject(notes_filter, "kinds", notes_kinds);
    cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
    cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
    cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    if (managed_subscription_register_locked(MANAGED_SUB_AGENT_NOTES,
                                             notes_filter,
                                             on_agent_context_event,
                                             on_eose,
                                             NULL,
                                             0,
                                             1,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             1) != 0) {
        rc = -1;
    }
    pthread_mutex_unlock(&g_subscription_mutex);
    cJSON_Delete(notes_filter);

    if (rc == 0) {
        DEBUG_INFO("[didactyl] agent self-context subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex);
    }
    return rc;
}

void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager) {
    g_trigger_manager = trigger_manager;
}

int nostr_handler_subscribe_self_skills(void) {
    if (!g_cfg || !g_pool) {
        return -1;
    }

    int rc = 0;

    cJSON* filter = cJSON_CreateObject();
    cJSON* kinds = cJSON_CreateArray();
    cJSON* authors = cJSON_CreateArray();
    if (!filter || !kinds || !authors) {
        cJSON_Delete(filter);
        cJSON_Delete(kinds);
        cJSON_Delete(authors);
        return -1;
    }

    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123));
    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124));
    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
    cJSON_AddItemToObject(filter, "authors", authors);
    cJSON_AddNumberToObject(filter, "limit", 300);

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    if (managed_subscription_register_locked(MANAGED_SUB_SELF_SKILLS,
                                             filter,
                                             on_self_skill_event,
                                             on_self_skill_eose,
                                             NULL,
                                             0,
                                             0,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             1) != 0) {
        rc = -1;
    }
    pthread_mutex_unlock(&g_subscription_mutex);
    cJSON_Delete(filter);

    int admin_enabled = (g_cfg->admin.pubkey[0] != '\0' && strcmp(g_cfg->admin.pubkey, g_cfg->keys.public_key_hex) != 0) ? 1 : 0;
    cJSON* admin_filter = NULL;
    if (admin_enabled) {
        admin_filter = cJSON_CreateObject();
        cJSON* admin_kinds = cJSON_CreateArray();
        cJSON* admin_authors = cJSON_CreateArray();
        if (!admin_filter || !admin_kinds || !admin_authors) {
            cJSON_Delete(admin_filter);
            cJSON_Delete(admin_kinds);
            cJSON_Delete(admin_authors);
            return -1;
        }

        cJSON_AddItemToArray(admin_kinds, cJSON_CreateNumber(31123));
        cJSON_AddItemToObject(admin_filter, "kinds", admin_kinds);
        cJSON_AddItemToArray(admin_authors, cJSON_CreateString(g_cfg->admin.pubkey));
        cJSON_AddItemToObject(admin_filter, "authors", admin_authors);
        cJSON_AddNumberToObject(admin_filter, "limit", 150);
    }

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_SKILLS,
                                             admin_filter,
                                             on_self_skill_event,
                                             on_eose,
                                             NULL,
                                             0,
                                             0,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             admin_enabled) != 0) {
        rc = -1;
    }
    pthread_mutex_unlock(&g_subscription_mutex);
    cJSON_Delete(admin_filter);

    if (rc == 0) {
        DEBUG_INFO("[didactyl] self/admin skill subscriptions active (self=%.16s..., admin=%.16s...)",
                   g_cfg->keys.public_key_hex,
                   g_cfg->admin.pubkey);
    }
    return rc;
}

int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
    if (!g_cfg || !g_pool || !callback) {
        return -1;
    }

    g_dm_callback = callback;
    g_dm_user_data = user_data;

    const int need_kind4 = (g_cfg->dm_protocol == DM_PROTOCOL_NIP04 || g_cfg->dm_protocol == DM_PROTOCOL_BOTH) ? 1 : 0;
    const int need_kind1059 = (g_cfg->dm_protocol == DM_PROTOCOL_NIP17 || g_cfg->dm_protocol == DM_PROTOCOL_BOTH) ? 1 : 0;

    if (!need_kind4 && !need_kind1059) {
        DEBUG_WARN("[didactyl] DM subscription skipped: no protocol selected");
        return -1;
    }

    int rc = 0;

    cJSON* filter4 = NULL;
    if (need_kind4) {
        cJSON* kinds4 = NULL;
        cJSON* p_values4 = NULL;

        filter4 = cJSON_CreateObject();
        kinds4 = cJSON_CreateArray();
        p_values4 = cJSON_CreateArray();
        if (!filter4 || !kinds4 || !p_values4) {
            cJSON_Delete(filter4);
            cJSON_Delete(kinds4);
            cJSON_Delete(p_values4);
            return -1;
        }

        cJSON_AddItemToArray(kinds4, cJSON_CreateNumber(4));
        cJSON_AddItemToObject(filter4, "kinds", kinds4);
        cJSON_AddItemToArray(p_values4, cJSON_CreateString(g_cfg->keys.public_key_hex));
        cJSON_AddItemToObject(filter4, "#p", p_values4);
        cJSON_AddNumberToObject(filter4, "since", (double)g_start_time);
        cJSON_AddNumberToObject(filter4, "limit", 100);
    }

    cJSON* filter1059 = NULL;
    if (need_kind1059) {
        cJSON* kinds1059 = NULL;
        cJSON* p_values1059 = NULL;

        filter1059 = cJSON_CreateObject();
        kinds1059 = cJSON_CreateArray();
        p_values1059 = cJSON_CreateArray();
        if (!filter1059 || !kinds1059 || !p_values1059) {
            cJSON_Delete(filter4);
            cJSON_Delete(filter1059);
            cJSON_Delete(kinds1059);
            cJSON_Delete(p_values1059);
            return -1;
        }

        cJSON_AddItemToArray(kinds1059, cJSON_CreateNumber(1059));
        cJSON_AddItemToObject(filter1059, "kinds", kinds1059);
        cJSON_AddItemToArray(p_values1059, cJSON_CreateString(g_cfg->keys.public_key_hex));
        cJSON_AddItemToObject(filter1059, "#p", p_values1059);
        cJSON_AddNumberToObject(filter1059, "since", (double)g_start_time);
        cJSON_AddNumberToObject(filter1059, "limit", 400);
    }

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();

    if (managed_subscription_register_locked(MANAGED_SUB_DM_KIND4,
                                             need_kind4 ? filter4 : NULL,
                                             on_event,
                                             on_eose,
                                             NULL,
                                             0,
                                             0,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             need_kind4) != 0) {
        rc = -1;
    }

    if (managed_subscription_register_locked(MANAGED_SUB_DM_KIND1059,
                                             need_kind1059 ? filter1059 : NULL,
                                             on_event,
                                             on_eose,
                                             NULL,
                                             0,
                                             0,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             need_kind1059) != 0) {
        rc = -1;
    }

    pthread_mutex_unlock(&g_subscription_mutex);
    cJSON_Delete(filter4);
    cJSON_Delete(filter1059);

    if (rc != 0) {
        return -1;
    }

    DEBUG_INFO("[didactyl] DM subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex);
    DEBUG_TRACE("[didactyl] DEBUG DM subscription g_start_time=%ld now=%ld delta=%ld relay_count=%d",
                (long)g_start_time, (long)time(NULL), (long)(time(NULL) - g_start_time), g_cfg->relay_count);
    return 0;
}

int nostr_handler_subscribe_wallet_events(void) {
    if (!g_cfg || !g_pool) {
        return -1;
    }

    cJSON* filter = cJSON_CreateObject();
    cJSON* kinds = cJSON_CreateArray();
    cJSON* authors = cJSON_CreateArray();
    if (!filter || !kinds || !authors) {
        cJSON_Delete(filter);
        cJSON_Delete(kinds);
        cJSON_Delete(authors);
        return -1;
    }

    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_WALLET_KIND));
    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_TOKEN_KIND));
    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(5));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
    cJSON_AddItemToObject(filter, "authors", authors);
    cJSON_AddNumberToObject(filter, "since", (double)g_start_time);
    cJSON_AddNumberToObject(filter, "limit", 500);

    int rc = 0;
    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    if (managed_subscription_register_locked(MANAGED_SUB_WALLET,
                                             filter,
                                             on_wallet_event,
                                             NULL,
                                             NULL,
                                             0,
                                             0,
                                             NOSTR_POOL_EOSE_FULL_SET,
                                             30,
                                             120,
                                             1) != 0) {
        rc = -1;
    }
    pthread_mutex_unlock(&g_subscription_mutex);
    cJSON_Delete(filter);

    if (rc != 0) {
        return -1;
    }

    DEBUG_INFO("[didactyl] wallet event subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex);
    return 0;
}

void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data) {
    g_self_skill_eose_cb = callback;
    g_self_skill_eose_user_data = user_data;
}

int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) {
    if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) {
        return -1;
    }

    unsigned char recipient_pubkey[32];
    if (hex_to_pubkey(recipient_pubkey_hex, recipient_pubkey) != 0) {
        return -1;
    }

    trace_plaintext_dm("sending plaintext DM content:", message);

    char* encrypted = (char*)malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE);
    if (!encrypted) {
        fprintf(stderr, "[didactyl] failed to allocate DM encrypt buffer\n");
        return -1;
    }
    encrypted[0] = '\0';

    if (nostr_nip04_encrypt(g_cfg->keys.private_key, recipient_pubkey, message, encrypted, NOSTR_NIP04_MAX_ENCRYPTED_SIZE) != NOSTR_SUCCESS) {
        free(encrypted);
        return -1;
    }

    cJSON* tags = create_dm_tags_for_recipient(recipient_pubkey_hex);
    if (!tags) {
        free(encrypted);
        return -1;
    }

    cJSON* event = nostr_create_and_sign_event(4, encrypted, tags, g_cfg->keys.private_key, time(NULL));
    cJSON_Delete(tags);
    free(encrypted);
    if (!event) {
        return -1;
    }

    trace_event_json("sending encrypted DM event:", event);

    log_publish_targets("publish DM");

    const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
    if (!connected_relays) {
        cJSON_Delete(event);
        return -1;
    }

    int connected_count = 0;
    for (int i = 0; i < g_cfg->relay_count; i++) {
        if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
            connected_relays[connected_count++] = g_cfg->relays[i];
        }
    }

    int sent = 0;
    if (connected_count > 0) {
        sent = nostr_relay_pool_publish_async(
            g_pool,
            connected_relays,
            connected_count,
            event,
            NULL,
            NULL);

        for (int i = 0; i < connected_count; i++) {
            DEBUG_INFO("[didactyl] kind 4 event published to %s (async)", connected_relays[i]);
        }
    } else {
        DEBUG_WARN("[didactyl] kind 4 event not queued: no connected relays");
    }

    cJSON* event_id = cJSON_GetObjectItemCaseSensitive(event, "id");
    const char* out_event_id_hex = (event_id && cJSON_IsString(event_id) && event_id->valuestring) ? event_id->valuestring : "<no-id>";

    DEBUG_INFO("[didactyl] sent DM %.16s... to %.16s... via %d connected relay(s)",
               out_event_id_hex,
               recipient_pubkey_hex,
               sent);

    if (sent > 0) {
        dm_history_remember(recipient_pubkey_hex,
                            message,
                            0,
                            time(NULL));
    }

    free(connected_relays);
    cJSON_Delete(event);
    return sent > 0 ? 0 : -1;
}

int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) {
    if (!recipient_pubkey_hex || !message) {
        return -1;
    }

    if (!g_cfg) {
        return nostr_handler_send_dm(recipient_pubkey_hex, message);
    }

    if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) {
        return nostr_handler_send_dm(recipient_pubkey_hex, message);
    }

    if (g_cfg->dm_protocol == DM_PROTOCOL_NIP17) {
        return nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL);
    }

    dm_protocol_t target = sender_protocol_lookup(recipient_pubkey_hex);
    if (target == DM_PROTOCOL_NIP17) {
        int rc17 = nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL);
        if (rc17 == 0) {
            return 0;
        }
        DEBUG_WARN("[didactyl] auto DM fallback to NIP-04 for %.16s... after NIP-17 send failure",
                   recipient_pubkey_hex);
    }

    return nostr_handler_send_dm(recipient_pubkey_hex, message);
}

static int publish_kind_event_to_relays(int kind,
                                        const char* content,
                                        cJSON* tags,
                                        const char** relay_urls,
                                        int relay_count,
                                        const char* reason_label,
                                        nostr_publish_result_t* out_result) {
    if (!g_cfg || !g_pool || !content || !relay_urls || relay_count <= 0) {
        return -1;
    }

    cJSON* tags_copy = NULL;
    if (tags) {
        tags_copy = cJSON_Duplicate(tags, 1);
        if (!tags_copy) {
            return -1;
        }
    }

    cJSON* event = nostr_create_and_sign_event(kind, content, tags_copy, g_cfg->keys.private_key, time(NULL));
    if (tags_copy) {
        cJSON_Delete(tags_copy);
    }
    if (!event) {
        return -1;
    }

    int sent = nostr_relay_pool_publish_async(
        g_pool,
        relay_urls,
        relay_count,
        event,
        NULL,
        NULL);

    for (int i = 0; i < relay_count; i++) {
        DEBUG_INFO("[didactyl] kind %d event published to %s (async%s%s)",
                   kind,
                   relay_urls[i],
                   reason_label ? ", reason=" : "",
                   reason_label ? reason_label : "");
    }

    cJSON* event_id = cJSON_GetObjectItemCaseSensitive(event, "id");
    const char* event_id_hex = (event_id && cJSON_IsString(event_id) && event_id->valuestring)
                                   ? event_id->valuestring
                                   : "";
    fill_publish_result(out_result,
                        kind,
                        tags,
                        event_id_hex,
                        relay_urls,
                        relay_count,
                        sent);

    if (sent > 0) {
        pthread_mutex_lock(&g_self_skill_mutex);
        self_skill_cache_upsert_event_locked(event);
        pthread_mutex_unlock(&g_self_skill_mutex);
    }

    cJSON_Delete(event);
    return sent > 0 ? 0 : -1;
}

int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result) {
    if (!g_cfg || !g_pool || !content) {
        return -1;
    }

    log_publish_targets("publish kind event");

    const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
    if (!connected_relays) {
        return -1;
    }

    int connected_count = 0;
    for (int i = 0; i < g_cfg->relay_count; i++) {
        if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
            connected_relays[connected_count++] = g_cfg->relays[i];
        }
    }

    if (connected_count <= 0) {
        DEBUG_WARN("[didactyl] kind %d event not queued: no connected relays", kind);
        free(connected_relays);
        return -1;
    }

    int rc = publish_kind_event_to_relays(kind,
                                          content,
                                          tags,
                                          connected_relays,
                                          connected_count,
                                          "manual_publish",
                                          out_result);
    free(connected_relays);

    DEBUG_INFO("[didactyl] published kind %d event via %d connected relay(s)", kind, connected_count);
    return rc;
}

char* nostr_handler_query_json(cJSON* filter, int timeout_ms) {
    if (!g_cfg || !g_pool || !filter) {
        return NULL;
    }

    int event_count = 0;
    cJSON** events = nostr_relay_pool_query_sync(
        g_pool,
        (const char**)g_cfg->relays,
        g_cfg->relay_count,
        filter,
        &event_count,
        timeout_ms);

    cJSON* arr = cJSON_CreateArray();
    if (!arr) {
        return NULL;
    }

    if (events && event_count > 0) {
        for (int i = 0; i < event_count; i++) {
            if (!events[i]) {
                continue;
            }
            if (nostr_block_list_is_event_filtered(events[i])) {
                cJSON_Delete(events[i]);
                continue;
            }
            cJSON* dup = cJSON_Duplicate(events[i], 1);
            if (dup) {
                cJSON_AddItemToArray(arr, dup);
            }
            cJSON_Delete(events[i]);
        }
        free(events);
    }

    char* out = cJSON_PrintUnformatted(arr);
    cJSON_Delete(arr);
    return out;
}

static void external_subscription_on_event_filtered(cJSON* event, const char* relay_url, void* user_data) {
    external_subscription_wrapper_t* w = (external_subscription_wrapper_t*)user_data;
    if (!w || !w->on_event) return;
    if (nostr_block_list_is_event_filtered(event)) return;
    w->on_event(event, relay_url, w->user_data);
}

static void external_subscription_on_eose_passthrough(cJSON** events, int event_count, void* user_data) {
    external_subscription_wrapper_t* w = (external_subscription_wrapper_t*)user_data;
    if (!w || !w->on_eose) return;
    w->on_eose(events, event_count, w->user_data);
}

static void external_subscription_wrapper_track(external_subscription_wrapper_t* w) {
    if (!w) return;
    pthread_mutex_lock(&g_subscription_mutex);
    w->next = g_external_sub_wrappers;
    g_external_sub_wrappers = w;
    pthread_mutex_unlock(&g_subscription_mutex);
}

static void external_subscription_wrapper_untrack_and_free(nostr_pool_subscription_t* handle) {
    if (!handle) return;
    pthread_mutex_lock(&g_subscription_mutex);
    external_subscription_wrapper_t* prev = NULL;
    external_subscription_wrapper_t* cur = g_external_sub_wrappers;
    while (cur) {
        if (cur->handle == handle) {
            if (prev) prev->next = cur->next;
            else g_external_sub_wrappers = cur->next;
            free(cur);
            break;
        }
        prev = cur;
        cur = cur->next;
    }
    pthread_mutex_unlock(&g_subscription_mutex);
}

nostr_pool_subscription_t* nostr_handler_subscribe_with_filter(
    cJSON* filter,
    void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
    void (*on_eose)(cJSON** events, int event_count, void* user_data),
    void* user_data,
    int close_on_eose,
    int enable_deduplication,
    nostr_pool_eose_result_mode_t result_mode,
    int relay_timeout_seconds,
    int eose_timeout_seconds) {
    if (!g_cfg || !g_pool || !filter || !on_event) {
        return NULL;
    }

    external_subscription_wrapper_t* w = (external_subscription_wrapper_t*)calloc(1, sizeof(*w));
    if (!w) return NULL;
    w->on_event = on_event;
    w->on_eose = on_eose;
    w->user_data = user_data;

    nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
        g_pool,
        (const char**)g_cfg->relays,
        g_cfg->relay_count,
        filter,
        external_subscription_on_event_filtered,
        on_eose ? external_subscription_on_eose_passthrough : NULL,
        w,
        close_on_eose,
        enable_deduplication,
        result_mode,
        relay_timeout_seconds,
        eose_timeout_seconds);

    if (!sub) {
        free(w);
        return NULL;
    }

    w->handle = sub;
    external_subscription_wrapper_track(w);
    return sub;
}

int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription) {
    if (!subscription) {
        return 0;
    }
    int rc = nostr_pool_subscription_close(subscription);
    external_subscription_wrapper_untrack_and_free(subscription);
    return rc;
}

static void publish_pending_startup_events_for_relay_index(int relay_index, const char* reason) {
    if (!g_cfg || !g_pool || !g_startup_publish_tracking_enabled || !g_startup_published) {
        return;
    }
    if (relay_index < 0 || relay_index >= g_cfg->relay_count) {
        return;
    }

    const char* relay_url = g_cfg->relays[relay_index];
    if (nostr_relay_pool_get_relay_status(g_pool, relay_url) != NOSTR_POOL_RELAY_CONNECTED) {
        return;
    }

    for (int i = 0; i < g_cfg->startup_event_count; i++) {
        startup_event_t* se = &g_cfg->startup_events[i];
        if (!se->content) {
            continue;
        }
        if (se->kind == 1 && g_startup_kind1_already_exists) {
            continue;
        }

        size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)relay_index;
        if (g_startup_published[slot]) {
            continue;
        }

        cJSON* tags = NULL;
        if (se->tags_json) {
            tags = cJSON_Parse(se->tags_json);
            if (!tags || !cJSON_IsArray(tags)) {
                cJSON_Delete(tags);
                tags = NULL;
            }
        }

        char kind1_content[512];
        const char* content_to_publish = se->content;
        if (se->kind == 1) {
            build_startup_kind1_content(kind1_content, sizeof(kind1_content), se->content);
            content_to_publish = kind1_content;
        }

        const char* one_relay[1] = { relay_url };
        if (publish_kind_event_to_relays(se->kind, content_to_publish, tags, one_relay, 1, reason, NULL) == 0) {
            g_startup_published[slot] = 1;
            if (se->kind == 1) {
                g_startup_kind1_already_exists = 1;
            }
        } else {
            DEBUG_WARN("[didactyl] startup event publish failed for kind=%d relay=%s", se->kind, relay_url);
        }

        cJSON_Delete(tags);
    }
}

int nostr_handler_reconcile_startup_events(void) {
    if (!g_cfg || !g_pool) {
        return -1;
    }

    free(g_system_context);
    g_system_context = NULL;

    free(g_startup_published);
    g_startup_published = NULL;
    g_startup_publish_tracking_enabled = 0;

    if (g_cfg->startup_event_count > 0 && g_cfg->relay_count > 0) {
        size_t total = (size_t)g_cfg->startup_event_count * (size_t)g_cfg->relay_count;
        g_startup_published = (unsigned char*)calloc(total, 1U);
        if (!g_startup_published) {
            return -1;
        }
        g_startup_publish_tracking_enabled = 1;
    }

    if (g_cfg->default_skill.content && g_cfg->default_skill.content[0] != '\0') {
        g_system_context = strdup(g_cfg->default_skill.content);
    }

    load_startup_display_name();
    g_startup_kind1_already_exists = startup_self_kind1_exists();
    if (g_startup_kind1_already_exists) {
        DEBUG_INFO("[didactyl] startup phase: existing self kind-1 note found; skipping startup kind-1 publish");
    }

    if (!g_system_context) {
        char default_ctx[512];
        snprintf(default_ctx,
                 sizeof(default_ctx),
                 "You are %s, a sovereign AI agent living on Nostr.",
                 g_startup_display_name[0] != '\0' ? g_startup_display_name : "the agent");
        g_system_context = strdup(default_ctx);
    }

    for (int relay_index = 0; relay_index < g_cfg->relay_count; relay_index++) {
        publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile");
    }

    seed_default_skill_into_cache();

    return 0;
}

int nostr_handler_load_system_context_from_adopted_skills(void) {
    if (!g_cfg || !g_pool || g_cfg->keys.public_key_hex[0] == '\0') {
        return -1;
    }

    cJSON* adoption_filter = cJSON_CreateObject();
    cJSON* kinds = cJSON_CreateArray();
    cJSON* authors = cJSON_CreateArray();
    if (!adoption_filter || !kinds || !authors) {
        cJSON_Delete(adoption_filter);
        cJSON_Delete(kinds);
        cJSON_Delete(authors);
        return -1;
    }

    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123));
    cJSON_AddItemToObject(adoption_filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
    cJSON_AddItemToObject(adoption_filter, "authors", authors);
    cJSON_AddNumberToObject(adoption_filter, "limit", 1);

    char* adoption_events_json = nostr_handler_query_json(adoption_filter, 3000);
    cJSON_Delete(adoption_filter);
    if (!adoption_events_json) {
        return -1;
    }

    cJSON* adoption_events = cJSON_Parse(adoption_events_json);
    free(adoption_events_json);
    if (!adoption_events || !cJSON_IsArray(adoption_events) || cJSON_GetArraySize(adoption_events) <= 0) {
        cJSON_Delete(adoption_events);
        return -1;
    }

    cJSON* adoption = cJSON_GetArrayItem(adoption_events, 0);
    cJSON* tags = adoption ? cJSON_GetObjectItemCaseSensitive(adoption, "tags") : NULL;
    if (!tags || !cJSON_IsArray(tags)) {
        cJSON_Delete(adoption_events);
        return -1;
    }

    const char* first_addr = NULL;
    int tag_n = cJSON_GetArraySize(tags);
    for (int i = 0; i < tag_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, "a") == 0 && v->valuestring[0] != '\0') {
            first_addr = v->valuestring;
            break;
        }
    }

    if (!first_addr) {
        cJSON_Delete(adoption_events);
        return -1;
    }

    int skill_kind = 0;
    char skill_pubkey_hex[65] = {0};
    char skill_d_tag[65] = {0};
    if (parse_skill_address_local(first_addr, &skill_kind, skill_pubkey_hex, skill_d_tag) != 0) {
        cJSON_Delete(adoption_events);
        return -1;
    }

    cJSON* skill_filter = cJSON_CreateObject();
    cJSON* skill_kinds = cJSON_CreateArray();
    cJSON* skill_authors = cJSON_CreateArray();
    cJSON* d_tags = cJSON_CreateArray();
    if (!skill_filter || !skill_kinds || !skill_authors || !d_tags) {
        cJSON_Delete(adoption_events);
        cJSON_Delete(skill_filter);
        cJSON_Delete(skill_kinds);
        cJSON_Delete(skill_authors);
        cJSON_Delete(d_tags);
        return -1;
    }

    cJSON_AddItemToArray(skill_kinds, cJSON_CreateNumber(skill_kind));
    cJSON_AddItemToObject(skill_filter, "kinds", skill_kinds);
    cJSON_AddItemToArray(skill_authors, cJSON_CreateString(skill_pubkey_hex));
    cJSON_AddItemToObject(skill_filter, "authors", skill_authors);
    cJSON_AddItemToArray(d_tags, cJSON_CreateString(skill_d_tag));
    cJSON_AddItemToObject(skill_filter, "#d", d_tags);
    cJSON_AddNumberToObject(skill_filter, "limit", 1);

    char* skill_events_json = nostr_handler_query_json(skill_filter, 3000);
    cJSON_Delete(skill_filter);
    cJSON_Delete(adoption_events);
    if (!skill_events_json) {
        return -1;
    }

    cJSON* skill_events = cJSON_Parse(skill_events_json);
    free(skill_events_json);
    if (!skill_events || !cJSON_IsArray(skill_events) || cJSON_GetArraySize(skill_events) <= 0) {
        cJSON_Delete(skill_events);
        return -1;
    }

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

    free(g_system_context);
    g_system_context = strdup(content->valuestring);
    cJSON_Delete(skill_events);

    return g_system_context ? 0 : -1;
}

const char* nostr_handler_get_system_context(void) {
    return g_system_context;
}

char* nostr_handler_get_self_events_by_kind_json(int kind) {
    cJSON* out = cJSON_CreateArray();
    if (!out) {
        return NULL;
    }

    pthread_mutex_lock(&g_self_skill_mutex);
    if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
        int n = cJSON_GetArraySize(g_self_skill_events);
        for (int i = 0; i < n; i++) {
            cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
            cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
            if (!ev_kind || !cJSON_IsNumber(ev_kind) || (int)ev_kind->valuedouble != kind) {
                continue;
            }
            cJSON* dup = cJSON_Duplicate(ev, 1);
            if (dup) {
                cJSON_AddItemToArray(out, dup);
            }
        }
    }
    pthread_mutex_unlock(&g_self_skill_mutex);

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    return json ? json : strdup("[]");
}

char* nostr_handler_get_self_skill_events_json(void) {
    cJSON* out = cJSON_CreateArray();
    if (!out) {
        return NULL;
    }

    pthread_mutex_lock(&g_self_skill_mutex);
    if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
        int n = cJSON_GetArraySize(g_self_skill_events);
        for (int i = 0; i < n; i++) {
            cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
            cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
            if (!ev_kind || !cJSON_IsNumber(ev_kind)) {
                continue;
            }
            int k = (int)ev_kind->valuedouble;
            if (k != 31123 && k != 31124) {
                continue;
            }
            cJSON* dup = cJSON_Duplicate(ev, 1);
            if (dup) {
                cJSON_AddItemToArray(out, dup);
            }
        }
    }
    pthread_mutex_unlock(&g_self_skill_mutex);

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    return json ? json : strdup("[]");
}

int nostr_handler_is_event_in_self_cache(const char* event_id_hex) {
    if (!event_id_hex || strlen(event_id_hex) != 64U) {
        return 0;
    }

    int found = 0;

    pthread_mutex_lock(&g_self_skill_mutex);
    if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
        int n = cJSON_GetArraySize(g_self_skill_events);
        for (int i = 0; i < n; i++) {
            cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
            cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL;
            if (!id || !cJSON_IsString(id) || !id->valuestring) {
                continue;
            }
            if (strcmp(id->valuestring, event_id_hex) == 0) {
                found = 1;
                break;
            }
        }
    }
    pthread_mutex_unlock(&g_self_skill_mutex);

    return found;
}

const char* nostr_handler_get_startup_display_name(void) {
    return g_startup_display_name;
}

int nostr_handler_connected_relay_count(void) {
    if (!g_cfg || !g_pool) {
        return 0;
    }

    int connected = 0;
    for (int i = 0; i < g_cfg->relay_count; i++) {
        if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
            connected++;
        }
    }

    return connected;
}

char* nostr_handler_relay_status_json(void) {
    if (!g_pool) {
        return NULL;
    }

    char** relay_urls = NULL;
    nostr_pool_relay_status_t* statuses = NULL;
    int relay_count = nostr_relay_pool_list_relays(g_pool, &relay_urls, &statuses);
    if (relay_count < 0) {
        return NULL;
    }

    cJSON* root = cJSON_CreateObject();
    cJSON* relays = cJSON_CreateArray();
    if (!root || !relays) {
        cJSON_Delete(root);
        cJSON_Delete(relays);
        if (relay_urls) {
            for (int i = 0; i < relay_count; i++) {
                free(relay_urls[i]);
            }
            free(relay_urls);
        }
        free(statuses);
        return NULL;
    }

    int connected_count = 0;
    for (int i = 0; i < relay_count; i++) {
        cJSON* relay = cJSON_CreateObject();
        if (!relay) {
            continue;
        }

        const char* url = (relay_urls && relay_urls[i]) ? relay_urls[i] : "";
        nostr_pool_relay_status_t st = statuses ? statuses[i] : NOSTR_POOL_RELAY_DISCONNECTED;
        if (st == NOSTR_POOL_RELAY_CONNECTED) {
            connected_count++;
        }

        const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, url);

        cJSON_AddStringToObject(relay, "url", url);
        cJSON_AddStringToObject(relay, "status", relay_status_str(st));

        if (stats) {
            cJSON_AddNumberToObject(relay, "events_received", stats->events_received);
            cJSON_AddNumberToObject(relay, "events_published", stats->events_published);
            cJSON_AddNumberToObject(relay, "events_published_ok", stats->events_published_ok);
            cJSON_AddNumberToObject(relay, "events_published_failed", stats->events_published_failed);
            cJSON_AddNumberToObject(relay, "ping_latency_current", stats->ping_latency_current);
            cJSON_AddNumberToObject(relay, "ping_latency_avg", stats->ping_latency_avg);
            cJSON_AddNumberToObject(relay, "query_latency_avg", stats->query_latency_avg);
            cJSON_AddNumberToObject(relay, "publish_latency_avg", stats->publish_latency_avg);
            cJSON_AddNumberToObject(relay, "connection_uptime_start", (double)stats->connection_uptime_start);
            cJSON_AddNumberToObject(relay, "last_event_time", (double)stats->last_event_time);
        }

        const char* last_pub_err = nostr_relay_pool_get_relay_last_publish_error(g_pool, url);
        const char* last_conn_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, url);
        if (last_pub_err && last_pub_err[0] != '\0') {
            cJSON_AddStringToObject(relay, "last_publish_error", last_pub_err);
        }
        if (last_conn_err && last_conn_err[0] != '\0') {
            cJSON_AddStringToObject(relay, "last_connection_error", last_conn_err);
        }

        cJSON_AddItemToArray(relays, relay);
    }

    cJSON_AddNumberToObject(root, "relay_count", relay_count);
    cJSON_AddNumberToObject(root, "connected_count", connected_count);
    cJSON_AddItemToObject(root, "relays", relays);

    char* out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);

    if (relay_urls) {
        for (int i = 0; i < relay_count; i++) {
            free(relay_urls[i]);
        }
        free(relay_urls);
    }
    free(statuses);

    return out;
}

char* nostr_handler_relay_info_json(const char* relay_url) {
    if (!relay_url || relay_url[0] == '\0') {
        return NULL;
    }

    nostr_relay_info_t* info = NULL;
    if (nostr_nip11_fetch_relay_info(relay_url, &info, 10) != NOSTR_SUCCESS || !info) {
        return NULL;
    }

    cJSON* root = cJSON_CreateObject();
    if (!root) {
        nostr_nip11_relay_info_free(info);
        return NULL;
    }

    cJSON* basic = cJSON_CreateObject();
    if (!basic) {
        cJSON_Delete(root);
        nostr_nip11_relay_info_free(info);
        return NULL;
    }

    if (info->basic.name) cJSON_AddStringToObject(basic, "name", info->basic.name);
    if (info->basic.description) cJSON_AddStringToObject(basic, "description", info->basic.description);
    if (info->basic.pubkey) cJSON_AddStringToObject(basic, "pubkey", info->basic.pubkey);
    if (info->basic.contact) cJSON_AddStringToObject(basic, "contact", info->basic.contact);
    if (info->basic.software) cJSON_AddStringToObject(basic, "software", info->basic.software);
    if (info->basic.version) cJSON_AddStringToObject(basic, "version", info->basic.version);

    cJSON* supported_nips = cJSON_CreateArray();
    if (!supported_nips) {
        cJSON_Delete(root);
        cJSON_Delete(basic);
        nostr_nip11_relay_info_free(info);
        return NULL;
    }
    for (size_t i = 0; i < info->basic.supported_nips_count; i++) {
        cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(info->basic.supported_nips[i]));
    }
    cJSON_AddItemToObject(basic, "supported_nips", supported_nips);
    cJSON_AddItemToObject(root, "basic", basic);

    if (info->has_limitations) {
        cJSON* limitations = cJSON_CreateObject();
        if (!limitations) {
            cJSON_Delete(root);
            nostr_nip11_relay_info_free(info);
            return NULL;
        }

        cJSON_AddNumberToObject(limitations, "max_message_length", info->limitations.max_message_length);
        cJSON_AddNumberToObject(limitations, "max_subscriptions", info->limitations.max_subscriptions);
        cJSON_AddNumberToObject(limitations, "max_filters", info->limitations.max_filters);
        cJSON_AddNumberToObject(limitations, "max_limit", info->limitations.max_limit);
        cJSON_AddNumberToObject(limitations, "max_subid_length", info->limitations.max_subid_length);
        cJSON_AddNumberToObject(limitations, "min_prefix", info->limitations.min_prefix);
        cJSON_AddNumberToObject(limitations, "max_event_tags", info->limitations.max_event_tags);
        cJSON_AddNumberToObject(limitations, "max_content_length", info->limitations.max_content_length);
        cJSON_AddNumberToObject(limitations, "min_pow_difficulty", info->limitations.min_pow_difficulty);
        cJSON_AddNumberToObject(limitations, "auth_required", info->limitations.auth_required);
        cJSON_AddNumberToObject(limitations, "payment_required", info->limitations.payment_required);
        cJSON_AddNumberToObject(limitations, "restricted_writes", info->limitations.restricted_writes);
        cJSON_AddNumberToObject(limitations, "created_at_lower_limit", (double)info->limitations.created_at_lower_limit);
        cJSON_AddNumberToObject(limitations, "created_at_upper_limit", (double)info->limitations.created_at_upper_limit);
        cJSON_AddItemToObject(root, "limitations", limitations);
    }

    if (info->has_content_limitations) {
        cJSON* countries = cJSON_CreateArray();
        if (!countries) {
            cJSON_Delete(root);
            nostr_nip11_relay_info_free(info);
            return NULL;
        }
        for (size_t i = 0; i < info->content_limitations.relay_countries_count; i++) {
            if (info->content_limitations.relay_countries[i]) {
                cJSON_AddItemToArray(countries, cJSON_CreateString(info->content_limitations.relay_countries[i]));
            }
        }
        cJSON_AddItemToObject(root, "relay_countries", countries);
    }

    if (info->has_community_preferences) {
        cJSON* prefs = cJSON_CreateObject();
        if (!prefs) {
            cJSON_Delete(root);
            nostr_nip11_relay_info_free(info);
            return NULL;
        }

        cJSON* language_tags = cJSON_CreateArray();
        cJSON* tags = cJSON_CreateArray();
        if (!language_tags || !tags) {
            cJSON_Delete(root);
            cJSON_Delete(prefs);
            cJSON_Delete(language_tags);
            cJSON_Delete(tags);
            nostr_nip11_relay_info_free(info);
            return NULL;
        }

        for (size_t i = 0; i < info->community_preferences.language_tags_count; i++) {
            if (info->community_preferences.language_tags[i]) {
                cJSON_AddItemToArray(language_tags, cJSON_CreateString(info->community_preferences.language_tags[i]));
            }
        }
        for (size_t i = 0; i < info->community_preferences.tags_count; i++) {
            if (info->community_preferences.tags[i]) {
                cJSON_AddItemToArray(tags, cJSON_CreateString(info->community_preferences.tags[i]));
            }
        }

        cJSON_AddItemToObject(prefs, "language_tags", language_tags);
        cJSON_AddItemToObject(prefs, "tags", tags);
        if (info->community_preferences.posting_policy) {
            cJSON_AddStringToObject(prefs, "posting_policy", info->community_preferences.posting_policy);
        }
        cJSON_AddItemToObject(root, "community_preferences", prefs);
    }

    if (info->has_icon && info->icon.icon) {
        cJSON_AddStringToObject(root, "icon", info->icon.icon);
    }

    char* out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    nostr_nip11_relay_info_free(info);
    return out;
}

char* nostr_handler_subscription_status_json(void) {
    cJSON* root = cJSON_CreateObject();
    cJSON* subs = cJSON_CreateArray();
    if (!root || !subs) {
        cJSON_Delete(root);
        cJSON_Delete(subs);
        return NULL;
    }

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();

    for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
        managed_subscription_t* sub = &g_managed_subs[i];

        cJSON* item = cJSON_CreateObject();
        if (!item) {
            continue;
        }

        cJSON_AddStringToObject(item, "name", sub->name ? sub->name : "");
        cJSON_AddBoolToObject(item, "enabled", sub->enabled ? 1 : 0);
        cJSON_AddBoolToObject(item, "active", sub->handle ? 1 : 0);
        cJSON_AddBoolToObject(item, "has_filter", sub->filter ? 1 : 0);
        cJSON_AddNumberToObject(item, "close_on_eose", sub->close_on_eose);
        cJSON_AddNumberToObject(item, "enable_deduplication", sub->enable_deduplication);
        cJSON_AddNumberToObject(item, "relay_timeout_seconds", sub->relay_timeout_seconds);
        cJSON_AddNumberToObject(item, "eose_timeout_seconds", sub->eose_timeout_seconds);

        if (sub->filter) {
            cJSON* filter_dup = cJSON_Duplicate(sub->filter, 1);
            if (filter_dup) {
                cJSON_AddItemToObject(item, "filter", filter_dup);
            }
        }

        cJSON_AddItemToArray(subs, item);
    }

    pthread_mutex_unlock(&g_subscription_mutex);

    cJSON_AddNumberToObject(root, "count", MANAGED_SUB_COUNT);
    cJSON_AddItemToObject(root, "subscriptions", subs);

    char* out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    return out;
}

int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int enabled, int enabled_set) {
    if (!name || name[0] == '\0') {
        return -1;
    }

    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();

    managed_subscription_id_t id = MANAGED_SUB_COUNT;
    if (managed_subscription_id_from_name(name, &id) != 0) {
        pthread_mutex_unlock(&g_subscription_mutex);
        return -1;
    }

    managed_subscription_t* sub = &g_managed_subs[id];

    cJSON* filter_to_use = sub->filter;
    if (filter) {
        filter_to_use = filter;
    }

    int enabled_to_use = enabled_set ? (enabled ? 1 : 0) : sub->enabled;

    int rc = managed_subscription_register_locked(id,
                                                  filter_to_use,
                                                  sub->on_event,
                                                  sub->on_eose,
                                                  sub->user_data,
                                                  sub->close_on_eose,
                                                  sub->enable_deduplication,
                                                  sub->result_mode,
                                                  sub->relay_timeout_seconds,
                                                  sub->eose_timeout_seconds,
                                                  enabled_to_use);

    pthread_mutex_unlock(&g_subscription_mutex);
    return rc;
}

int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) {
    if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') {
        return -1;
    }

    const char* target_relays[NIP17_MAX_RELAYS] = {0};
    int target_count = 0;

    for (int i = 0; i < g_cfg->relay_count && target_count < NIP17_MAX_RELAYS; i++) {
        if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
            target_relays[target_count++] = g_cfg->relays[i];
        }
    }

    DEBUG_TRACE("[didactyl] NIP-17 send prep: recipient=%.16s... connected_targets=%d",
                recipient_pubkey_hex,
                target_count);

    if (target_count <= 0) {
        DEBUG_WARN("[didactyl] NIP-17 send aborted: no connected relays for recipient %.16s...",
                   recipient_pubkey_hex);
        return -1;
    }

    const char* recipients[1] = { recipient_pubkey_hex };
    cJSON* chat_event = nostr_nip17_create_chat_event(
        message,
        recipients,
        1,
        (subject && subject[0] != '\0') ? subject : NULL,
        NULL,
        NULL,
        g_cfg->keys.public_key_hex);
    if (!chat_event) {
        return -1;
    }

    cJSON* gift_wraps[NIP17_MAX_GIFT_WRAPS] = {0};
    int gift_count = nostr_nip17_send_dm(chat_event,
                                         recipients,
                                         1,
                                         g_cfg->keys.private_key,
                                         gift_wraps,
                                         NIP17_MAX_GIFT_WRAPS,
                                         0);
    cJSON_Delete(chat_event);
    if (gift_count <= 0) {
        DEBUG_WARN("[didactyl] NIP-17 send aborted: gift_wrap creation failed for %.16s...",
                   recipient_pubkey_hex);
        return -1;
    }

    int any_sent = 0;
    int sent_count = 0;

    for (int i = 0; i < gift_count; i++) {
        if (!gift_wraps[i]) {
            continue;
        }

        int sent = nostr_relay_pool_publish_async(g_pool,
                                                  target_relays,
                                                  target_count,
                                                  gift_wraps[i],
                                                  NULL,
                                                  NULL);
        if (sent > 0) {
            any_sent = 1;
            sent_count += sent;
        }

        for (int r = 0; r < target_count; r++) {
            DEBUG_INFO("[didactyl] kind 1059 event published to %s (async)", target_relays[r]);
        }

        cJSON_Delete(gift_wraps[i]);
    }

    DEBUG_TRACE("[didactyl] NIP-17 send complete: recipient=%.16s... sent_count=%d",
                recipient_pubkey_hex,
                sent_count);

    if (!any_sent) {
        DEBUG_WARN("[didactyl] NIP-17 send failed: no relay accepted publish for recipient %.16s...",
                   recipient_pubkey_hex);
        return -1;
    }

    dm_history_remember(recipient_pubkey_hex,
                        message,
                        0,
                        time(NULL));

    return 0;
}

char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit) {
    if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U) {
        return strdup("[]");
    }

    int max_take = limit > 0 ? limit : 64;
    if (max_take > DM_HISTORY_RING_SIZE) {
        max_take = DM_HISTORY_RING_SIZE;
    }

    cJSON* arr = cJSON_CreateArray();
    if (!arr) {
        return NULL;
    }

    pthread_mutex_lock(&g_dm_history_mutex);

    int total = g_dm_history_count;
    int matches = 0;
    for (int i = 0; i < total; i++) {
        int idx = (g_dm_history_count < DM_HISTORY_RING_SIZE)
                  ? i
                  : (g_dm_history_next + i) % DM_HISTORY_RING_SIZE;
        dm_history_entry_t* entry = &g_dm_history_ring[idx];
        if (entry->peer_pubkey_hex[0] == '\0' || !entry->content) {
            continue;
        }
        if (strcmp(entry->peer_pubkey_hex, peer_pubkey_hex) == 0) {
            matches++;
        }
    }

    int skip = matches > max_take ? (matches - max_take) : 0;
    int seen = 0;
    for (int i = 0; i < total; i++) {
        int idx = (g_dm_history_count < DM_HISTORY_RING_SIZE)
                  ? i
                  : (g_dm_history_next + i) % DM_HISTORY_RING_SIZE;
        dm_history_entry_t* entry = &g_dm_history_ring[idx];
        if (entry->peer_pubkey_hex[0] == '\0' || !entry->content) {
            continue;
        }
        if (strcmp(entry->peer_pubkey_hex, peer_pubkey_hex) != 0) {
            continue;
        }
        if (seen++ < skip) {
            continue;
        }

        cJSON* item = cJSON_CreateObject();
        if (!item) {
            continue;
        }
        cJSON_AddStringToObject(item, "role", entry->incoming ? "user" : "assistant");
        cJSON_AddStringToObject(item, "content", entry->content);
        cJSON_AddNumberToObject(item, "created_at", (double)entry->created_at);
        cJSON_AddItemToArray(arr, item);
    }

    pthread_mutex_unlock(&g_dm_history_mutex);

    char* out = cJSON_PrintUnformatted(arr);
    cJSON_Delete(arr);
    return out ? out : strdup("[]");
}

char* nostr_handler_get_admin_kind0_context(void) {
    if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_0) {
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);
    char* out = g_admin_kind0_json ? strdup(g_admin_kind0_json) : NULL;
    pthread_mutex_unlock(&g_admin_ctx_mutex);
    return out;
}

char* nostr_handler_get_admin_kind3_context(void) {
    if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_3) {
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);

    cJSON* out = cJSON_CreateArray();
    if (!out) {
        pthread_mutex_unlock(&g_admin_ctx_mutex);
        return NULL;
    }

    for (int i = 0; i < g_admin_wot_contact_count; i++) {
        const char* pk = g_admin_wot_contacts[i];
        if (!pk || pk[0] == '\0') continue;
        cJSON_AddItemToArray(out, cJSON_CreateString(pk));
    }

    pthread_mutex_unlock(&g_admin_ctx_mutex);

    char* out_json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    return out_json;
}

char* nostr_handler_get_admin_kind10002_context(void) {
    if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_10002) {
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);
    char* out = g_admin_kind10002_json ? strdup(g_admin_kind10002_json) : NULL;
    pthread_mutex_unlock(&g_admin_ctx_mutex);
    return out;
}

char* nostr_handler_get_admin_kind1_notes_context(void) {
    if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_1) {
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);

    if (g_admin_kind1_note_count <= 0 || !g_admin_kind1_notes) {
        pthread_mutex_unlock(&g_admin_ctx_mutex);
        return NULL;
    }

    size_t total = strlen("Administrator recent public notes:\n") + 1U;
    for (int i = 0; i < g_admin_kind1_note_count; i++) {
        total += strlen("- ") + strlen(g_admin_kind1_notes[i].content ? g_admin_kind1_notes[i].content : "") + 1U;
    }

    char* out = (char*)malloc(total);
    if (!out) {
        pthread_mutex_unlock(&g_admin_ctx_mutex);
        return NULL;
    }

    out[0] = '\0';
    strcat(out, "Administrator recent public notes:\n");
    for (int i = 0; i < g_admin_kind1_note_count; i++) {
        strcat(out, "- ");
        strcat(out, g_admin_kind1_notes[i].content ? g_admin_kind1_notes[i].content : "");
        strcat(out, "\n");
    }

    pthread_mutex_unlock(&g_admin_ctx_mutex);
    return out;
}

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

    cJSON* root = cJSON_Parse(events_json);
    if (!root || !cJSON_IsArray(root)) {
        cJSON_Delete(root);
        return NULL;
    }

    char* best_content = NULL;
    time_t best_created_at = 0;

    int n = cJSON_GetArraySize(root);
    for (int i = 0; i < n; i++) {
        cJSON* ev = cJSON_GetArrayItem(root, i);
        if (!ev || !cJSON_IsObject(ev)) {
            continue;
        }

        cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
        cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content");
        cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
        if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 0 ||
            !content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
            continue;
        }

        time_t ev_created_at = 0;
        if (created_at && cJSON_IsNumber(created_at)) {
            ev_created_at = (time_t)created_at->valuedouble;
        }

        if (!best_content || ev_created_at >= best_created_at) {
            char* dup = strdup(content->valuestring);
            if (!dup) {
                continue;
            }
            free(best_content);
            best_content = dup;
            best_created_at = ev_created_at;
        }
    }

    cJSON_Delete(root);
    return best_content;
}

char* nostr_handler_get_agent_kind0_context(void) {
    if (!g_cfg) {
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);
    char* out = g_agent_kind0_json ? strdup(g_agent_kind0_json) : NULL;
    pthread_mutex_unlock(&g_admin_ctx_mutex);
    if (out && out[0] != '\0') {
        return out;
    }
    free(out);

    cJSON* filter = cJSON_CreateObject();
    cJSON* kinds = cJSON_CreateArray();
    cJSON* authors = cJSON_CreateArray();
    if (!filter || !kinds || !authors) {
        cJSON_Delete(filter);
        cJSON_Delete(kinds);
        cJSON_Delete(authors);
        return NULL;
    }

    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
    cJSON_AddItemToObject(filter, "authors", authors);
    cJSON_AddNumberToObject(filter, "limit", 8);

    char* events_json = nostr_handler_query_json(filter, 3000);
    cJSON_Delete(filter);
    if (!events_json) {
        return NULL;
    }

    char* fetched_kind0 = extract_latest_kind0_content_from_events_json(events_json);
    free(events_json);
    if (!fetched_kind0 || fetched_kind0[0] == '\0') {
        free(fetched_kind0);
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);
    free(g_agent_kind0_json);
    g_agent_kind0_json = strdup(fetched_kind0);
    pthread_mutex_unlock(&g_admin_ctx_mutex);

    return fetched_kind0;
}

char* nostr_handler_get_agent_kind3_context(void) {
    if (!g_cfg) {
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);

    cJSON* out = cJSON_CreateArray();
    if (!out) {
        pthread_mutex_unlock(&g_admin_ctx_mutex);
        return NULL;
    }

    for (int i = 0; i < g_agent_contact_count; i++) {
        const char* pk = g_agent_contacts[i];
        if (!pk || pk[0] == '\0') continue;
        cJSON_AddItemToArray(out, cJSON_CreateString(pk));
    }

    pthread_mutex_unlock(&g_admin_ctx_mutex);

    char* out_json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    return out_json;
}

char* nostr_handler_get_agent_kind10002_context(void) {
    if (!g_cfg) {
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);
    char* out = g_agent_kind10002_json ? strdup(g_agent_kind10002_json) : NULL;
    pthread_mutex_unlock(&g_admin_ctx_mutex);
    return out;
}

char* nostr_handler_get_agent_kind1_notes_context(void) {
    if (!g_cfg) {
        return NULL;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);

    if (g_agent_kind1_note_count <= 0 || !g_agent_kind1_notes) {
        pthread_mutex_unlock(&g_admin_ctx_mutex);
        return NULL;
    }

    size_t total = strlen("Agent recent public notes:\n") + 1U;
    for (int i = 0; i < g_agent_kind1_note_count; i++) {
        total += strlen("- ") + strlen(g_agent_kind1_notes[i].content ? g_agent_kind1_notes[i].content : "") + 1U;
    }

    char* out = (char*)malloc(total);
    if (!out) {
        pthread_mutex_unlock(&g_admin_ctx_mutex);
        return NULL;
    }

    out[0] = '\0';
    strcat(out, "Agent recent public notes:\n");
    for (int i = 0; i < g_agent_kind1_note_count; i++) {
        strcat(out, "- ");
        strcat(out, g_agent_kind1_notes[i].content ? g_agent_kind1_notes[i].content : "");
        strcat(out, "\n");
    }

    pthread_mutex_unlock(&g_admin_ctx_mutex);
    return out;
}

int nostr_handler_is_wot_contact(const char* pubkey_hex) {
    if (!pubkey_hex || strlen(pubkey_hex) != 64U) {
        return 0;
    }

    pthread_mutex_lock(&g_admin_ctx_mutex);
    int found = 0;
    for (int i = 0; i < g_admin_wot_contact_count; i++) {
        if (g_admin_wot_contacts[i] && strcmp(g_admin_wot_contacts[i], pubkey_hex) == 0) {
            found = 1;
            break;
        }
    }
    pthread_mutex_unlock(&g_admin_ctx_mutex);
    return found;
}

int nostr_handler_poll(int timeout_ms) {
    if (!g_pool) {
        return -1;
    }

    double start_ms = -1.0;
    struct timespec ts_start;
    if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == 0) {
        start_ms = ts_start.tv_sec * 1000.0 + ts_start.tv_nsec / 1000000.0;
    }

    int rc = nostr_relay_pool_poll(g_pool, timeout_ms);
    g_poll_counter++;

    if (start_ms >= 0.0) {
        struct timespec ts_end;
        if (clock_gettime(CLOCK_MONOTONIC, &ts_end) == 0) {
            const double end_ms = ts_end.tv_sec * 1000.0 + ts_end.tv_nsec / 1000000.0;
            const double elapsed_ms = end_ms - start_ms;
            const double expected_ms = timeout_ms > 0 ? (double)timeout_ms : 0.0;

            if (elapsed_ms > expected_ms + 250.0) {
                DEBUG_WARN("[didactyl] poll latency spike: nostr_relay_pool_poll(timeout=%d) took %.1fms rc=%d count=%llu",
                           timeout_ms,
                           elapsed_ms,
                           rc,
                           (unsigned long long)g_poll_counter);
            }
        }
    }

    log_relay_state_changes();

    return rc;
}

void nostr_handler_cleanup(void) {
    pthread_mutex_lock(&g_subscription_mutex);
    managed_subscriptions_init_defaults();
    for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
        (void)managed_subscription_close_locked((managed_subscription_id_t)i);
        cJSON_Delete(g_managed_subs[i].filter);
        g_managed_subs[i].filter = NULL;
        g_managed_subs[i].enabled = 0;
        g_managed_subs[i].on_event = NULL;
        g_managed_subs[i].on_eose = NULL;
        g_managed_subs[i].user_data = NULL;
    }
    external_subscription_wrapper_t* ext = g_external_sub_wrappers;
    while (ext) {
        external_subscription_wrapper_t* next = ext->next;
        free(ext);
        ext = next;
    }
    g_external_sub_wrappers = NULL;

    pthread_mutex_unlock(&g_subscription_mutex);

    if (g_pool) {
        nostr_relay_pool_destroy(g_pool);
    }

    nostr_core_clear_log_bridge_if_supported();

    free(g_last_relay_statuses);
    g_last_relay_statuses = NULL;
    free(g_startup_published);
    g_startup_published = NULL;
    g_startup_publish_tracking_enabled = 0;
    g_startup_kind1_already_exists = 0;
    snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", "Didactyl");

    g_pool = NULL;
    g_cfg = NULL;
    g_dm_callback = NULL;
    g_dm_user_data = NULL;
    free(g_system_context);
    g_system_context = NULL;
    memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids));
    g_seen_dm_count = 0;
    g_seen_dm_next = 0;

    pthread_mutex_lock(&g_sender_protocol_mutex);
    memset(g_sender_protocol_cache, 0, sizeof(g_sender_protocol_cache));
    pthread_mutex_unlock(&g_sender_protocol_mutex);

    pthread_mutex_lock(&g_dm_history_mutex);
    dm_history_clear_locked();
    pthread_mutex_unlock(&g_dm_history_mutex);

    pthread_mutex_lock(&g_admin_ctx_mutex);
    free_admin_context_locked();
    free_agent_context_locked();
    pthread_mutex_unlock(&g_admin_ctx_mutex);

    pthread_mutex_lock(&g_self_skill_mutex);
    cJSON_Delete(g_self_skill_events);
    g_self_skill_events = NULL;
    pthread_mutex_unlock(&g_self_skill_mutex);
}
