#define _POSIX_C_SOURCE 200809L

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include "main.h"
#include "agent.h"
#include "config.h"
#include "llm.h"
#include "nostr_handler.h"
#include "debug.h"
#include "trigger_manager.h"
#include "tools/tools.h"
#include "cjson/cJSON.h"
#include "http_api.h"
#include "setup_wizard.h"
#include "cashu_wallet.h"
#include "nostr_block_list.h"
#include "mongoose.h"

static volatile sig_atomic_t g_running = 1;

typedef struct {
    trigger_manager_t* trigger_manager;
    int loaded_once;
} deferred_trigger_load_ctx_t;

static void on_self_skill_eose_load_triggers(int event_count, void* user_data) {
    deferred_trigger_load_ctx_t* ctx = (deferred_trigger_load_ctx_t*)user_data;
    if (!ctx || !ctx->trigger_manager) {
        return;
    }

    if (ctx->loaded_once) {
        DEBUG_TRACE("[didactyl] deferred trigger load skipped (already loaded once) event_count=%d", event_count);
        return;
    }

    DEBUG_INFO("[didactyl] deferred trigger load begin after self-skill EOSE (event_count=%d)", event_count);
    if (trigger_manager_load_from_skills(ctx->trigger_manager) != 0) {
        DEBUG_WARN("[didactyl] deferred trigger load failed after self-skill EOSE");
        return;
    }

    ctx->loaded_once = 1;

    char* status = trigger_manager_status_json(ctx->trigger_manager);
    if (status) {
        DEBUG_INFO("[didactyl] deferred trigger load status: %s", status);
        free(status);
    }
}

static volatile sig_atomic_t g_sigint_count = 0;

static void signal_handler(int signum) {
    if (signum == SIGINT) {
        g_sigint_count++;
        if (g_sigint_count >= 2) {
            _exit(130);
        }
    }
    g_running = 0;
}

static void configure_mongoose_log_level_from_debug(void) {
    if (g_debug_level >= DEBUG_LEVEL_TRACE) {
        mg_log_set(MG_LL_INFO);
    } else if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
        mg_log_set(MG_LL_INFO);
    } else if (g_debug_level >= DEBUG_LEVEL_INFO) {
        mg_log_set(MG_LL_INFO);
    } else if (g_debug_level >= DEBUG_LEVEL_WARN) {
        mg_log_set(MG_LL_ERROR);
    } else {
        mg_log_set(MG_LL_NONE);
    }
}

static void print_usage(const char* prog) {
    const char* p = prog ? prog : "didactyl";

    fprintf(stderr,
            "Didactyl - sovereign Nostr agent\n\n"
            "Usage:\n"
            "  %s [options]\n\n"
            "Options:\n"
            "  -h, --help\n"
            "      Show this help text and exit.\n"
            "  -v, --version\n"
            "      Show version and exit.\n"
            "  --config <path>\n"
            "      Path to config/genesis JSONC file (default: ./genesis.jsonc).\n"
            "      If file is missing, startup falls back to minimal nsec-only mode.\n"
            "  --nsec <nsec_or_hex>\n"
            "      Private key as nsec1... or 64-char hex; overrides DIDACTYL_NSEC.\n"
            "  --admin <npub_or_hex>\n"
            "      Administrator pubkey override; accepts npub1... or 64-char hex.\n"
            "  --api-port <port>\n"
            "      Enable HTTP API and bind to this port (1-65535).\n"
            "  --api-bind <addr>\n"
            "      Enable HTTP API and bind address (example: 127.0.0.1).\n"
            "  --debug <0-5>\n"
            "      Log level (0=fatal, 1=error, 2=warn, 3=info, 4=debug, 5=trace).\n"
            "  --dump-schemas\n"
            "      Print tool schemas JSON and exit.\n"
            "  --test-tool <name> <args_json>\n"
            "      Execute one tool call locally and print JSON result, then exit.\n"
            "\n"
            "Environment:\n"
            "  DIDACTYL_NSEC\n"
            "      Fallback private key when --nsec is not provided.\n"
            "\n"
            "Examples:\n"
            "  1) Genesis-based startup\n"
            "     %s --config genesis.jsonc\n"
            "\n"
            "  2) nsec-only startup (no config file)\n"
            "     %s --nsec nsec1...\n"
            "\n"
            "  3) nsec via environment variable\n"
            "     DIDACTYL_NSEC=nsec1... %s\n"
            "\n"
            "  4) Override API bind/port\n"
            "     %s --config genesis.jsonc --api-bind 127.0.0.1 --api-port 8484\n"
            "\n"
            "  5) Dump tool schemas\n"
            "     %s --dump-schemas\n"
            "\n"
            "  6) Test a tool invocation\n"
            "     %s --test-tool config_recall '{\"config_name\":\"llm_config\"}'\n",
            p,
            p,
            p,
            p,
            p,
            p,
            p);
}

static int derive_keys_from_nsec(const char* nsec_or_hex, didactyl_config_t* cfg) {
    if (!nsec_or_hex || !cfg) return -1;

    size_t n = strlen(nsec_or_hex);
    if (n == 0 || n >= sizeof(cfg->keys.nsec)) {
        return -1;
    }

    snprintf(cfg->keys.nsec, sizeof(cfg->keys.nsec), "%s", nsec_or_hex);

    int dec_rc = -1;
    if (strncmp(nsec_or_hex, "nsec1", 5) == 0) {
        dec_rc = (nostr_decode_nsec(nsec_or_hex, cfg->keys.private_key) == 0) ? 0 : -1;
    } else if (strlen(nsec_or_hex) == 64U) {
        dec_rc = (nostr_hex_to_bytes(nsec_or_hex, cfg->keys.private_key, 32) == 0) ? 0 : -1;
    }
    if (dec_rc != 0) {
        return -1;
    }

    if (nostr_ec_public_key_from_private_key(cfg->keys.private_key, cfg->keys.public_key) != 0) {
        return -1;
    }
    nostr_bytes_to_hex(cfg->keys.public_key, 32, cfg->keys.public_key_hex);
    return 0;
}

static int is_first_run_from_kind10002(const didactyl_config_t* cfg) {
    if (!cfg || cfg->keys.public_key_hex[0] == '\0') {
        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(10002));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
    cJSON_AddItemToObject(filter, "authors", authors);
    cJSON_AddNumberToObject(filter, "limit", 1);

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

    int first_run = 1;
    cJSON* arr = cJSON_Parse(events_json);
    free(events_json);
    if (arr && cJSON_IsArray(arr) && cJSON_GetArraySize(arr) > 0) {
        first_run = 0;
    }
    cJSON_Delete(arr);
    return first_run;
}

static int tool_result_success(const char* json) {
    if (!json) {
        return 0;
    }

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

    cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
    int ok = (success && cJSON_IsBool(success) && cJSON_IsTrue(success)) ? 1 : 0;
    cJSON_Delete(root);
    return ok;
}

static int wait_for_connected_relays(int timeout_ms) {
    if (timeout_ms <= 0) {
        return nostr_handler_connected_relay_count() > 0 ? 1 : 0;
    }

    const int step_ms = 100;
    int elapsed_ms = 0;

    while (elapsed_ms < timeout_ms) {
        if (nostr_handler_connected_relay_count() > 0) {
            return 1;
        }

        (void)nostr_handler_poll(step_ms);
        elapsed_ms += step_ms;
    }

    return nostr_handler_connected_relay_count() > 0 ? 1 : 0;
}

static void startup_step_begin(int step, const char* label) {
    DEBUG_INFO("[didactyl] startup checklist [%02d] %s: begin", step, label);
    fprintf(stdout, "[startup %02d] %s ...\n", step, label);
    fflush(stdout);
}

static void startup_step_ok(int step, const char* label, const char* detail) {
    if (detail && detail[0] != '\0') {
        DEBUG_INFO("[didactyl] startup checklist [%02d] %s: ok (%s)", step, label, detail);
        fprintf(stdout, "[startup %02d] %s: OK (%s)\n", step, label, detail);
    } else {
        DEBUG_INFO("[didactyl] startup checklist [%02d] %s: ok", step, label);
        fprintf(stdout, "[startup %02d] %s: OK\n", step, label);
    }
    fflush(stdout);
}

static void startup_step_fail(int step, const char* label, const char* detail) {
    if (detail && detail[0] != '\0') {
        DEBUG_ERROR("[didactyl] startup checklist [%02d] %s: failed (%s)", step, label, detail);
        fprintf(stderr, "[startup %02d] %s: FAILED (%s)\n", step, label, detail);
    } else {
        DEBUG_ERROR("[didactyl] startup checklist [%02d] %s: failed", step, label);
        fprintf(stderr, "[startup %02d] %s: FAILED\n", step, label);
    }
    fflush(stderr);
}

static int relay_list_contains(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(char** relays, int relay_count) {
    if (!relays) {
        return;
    }

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

static int query_self_kind10002_relays(const didactyl_config_t* cfg,
                                       int timeout_ms,
                                       char*** out_relays,
                                       int* out_relay_count) {
    if (!cfg || !out_relays || !out_relay_count || cfg->keys.public_key_hex[0] == '\0') {
        return -1;
    }

    *out_relays = NULL;
    *out_relay_count = 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(10002));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
    cJSON_AddItemToObject(filter, "authors", authors);
    cJSON_AddNumberToObject(filter, "limit", 16);

    DEBUG_INFO("[didactyl] kind10002 query begin: timeout_ms=%d author=%.16s... relay_count=%d",
               timeout_ms,
               cfg->keys.public_key_hex,
               cfg->relay_count);

    char* relay_status_snapshot = nostr_handler_relay_status_json();
    if (relay_status_snapshot) {
        DEBUG_INFO("[didactyl] kind10002 query relay snapshot: %s", relay_status_snapshot);
        free(relay_status_snapshot);
    }

    char* events_json = nostr_handler_query_json(filter, timeout_ms);
    cJSON_Delete(filter);
    if (!events_json) {
        DEBUG_WARN("[didactyl] kind10002 query returned NULL JSON payload");
        return 0;
    }

    size_t events_json_len = strlen(events_json);
    cJSON* events = cJSON_Parse(events_json);
    free(events_json);
    if (!events || !cJSON_IsArray(events)) {
        DEBUG_WARN("[didactyl] kind10002 query parse failed or non-array payload (len=%zu)", events_json_len);
        cJSON_Delete(events);
        return 0;
    }

    DEBUG_INFO("[didactyl] kind10002 query received event_count=%d", cJSON_GetArraySize(events));

    cJSON* best_tags = NULL;
    double best_created_at = -1.0;

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

        cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
        cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
        if (!tags || !cJSON_IsArray(tags)) {
            continue;
        }

        double ts = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
        if (!best_tags || ts >= best_created_at) {
            best_tags = tags;
            best_created_at = ts;
        }
    }

    if (!best_tags) {
        if (event_count == 0) {
            DEBUG_WARN("[didactyl] kind10002 query returned zero events");
        } else {
            DEBUG_WARN("[didactyl] kind10002 query found events but none had valid tags array");
        }
        cJSON_Delete(events);
        return 0;
    }

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

    int tag_count = cJSON_GetArraySize(best_tags);
    for (int i = 0; i < tag_count; i++) {
        cJSON* tag = cJSON_GetArrayItem(best_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;
        }

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

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

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

        relay_count++;
    }

    cJSON_Delete(events);

    if (relay_count <= 0) {
        free(relays);
        return 0;
    }

    *out_relays = relays;
    *out_relay_count = relay_count;

    cJSON* relays_debug = cJSON_CreateArray();
    if (relays_debug) {
        for (int i = 0; i < relay_count; i++) {
            if (relays[i]) {
                cJSON_AddItemToArray(relays_debug, cJSON_CreateString(relays[i]));
            }
        }
        char* relays_debug_json = cJSON_PrintUnformatted(relays_debug);
        if (relays_debug_json) {
            DEBUG_INFO("[didactyl] kind10002 query extracted relays=%s", relays_debug_json);
            free(relays_debug_json);
        }
        cJSON_Delete(relays_debug);
    }

    return relay_count;
}

static int merge_discovered_relays_into_config(didactyl_config_t* cfg,
                                               char** discovered_relays,
                                               int discovered_count) {
    if (!cfg || !discovered_relays || discovered_count <= 0) {
        return 0;
    }

    int old_count = cfg->relay_count;
    int max_count = old_count + discovered_count;
    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++) {
        if (!cfg->relays || !cfg->relays[i] || cfg->relays[i][0] == '\0') {
            continue;
        }

        if (relay_list_contains(merged, merged_count, cfg->relays[i])) {
            continue;
        }

        merged[merged_count] = strdup(cfg->relays[i]);
        if (!merged[merged_count]) {
            free_relay_list(merged, merged_count);
            return -1;
        }
        merged_count++;
    }

    int added = 0;
    for (int i = 0; i < discovered_count; i++) {
        if (!discovered_relays[i] || discovered_relays[i][0] == '\0') {
            continue;
        }

        if (relay_list_contains(merged, merged_count, discovered_relays[i])) {
            continue;
        }

        merged[merged_count] = strdup(discovered_relays[i]);
        if (!merged[merged_count]) {
            free_relay_list(merged, merged_count);
            return -1;
        }
        merged_count++;
        added++;
    }

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

    cfg->relays = merged;
    cfg->relay_count = merged_count;
    return added;
}

static int wait_for_self_kind10002_relays(const didactyl_config_t* cfg,
                                          int timeout_ms,
                                          int poll_ms,
                                          char*** out_relays,
                                          int* out_relay_count) {
    if (!cfg || !out_relays || !out_relay_count) {
        return -1;
    }

    *out_relays = NULL;
    *out_relay_count = 0;

    if (poll_ms <= 0) {
        poll_ms = 100;
    }

    const int query_timeout_ms = 5000;

    if (timeout_ms <= 0) {
        return query_self_kind10002_relays(cfg, query_timeout_ms, out_relays, out_relay_count);
    }

    struct timespec start;
    if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) {
        return query_self_kind10002_relays(cfg, query_timeout_ms, out_relays, out_relay_count);
    }

    for (;;) {
        char** relays = NULL;
        int relay_count = 0;
        int rc = query_self_kind10002_relays(cfg, query_timeout_ms, &relays, &relay_count);
        if (rc > 0 && relay_count > 0 && relays) {
            DEBUG_INFO("[didactyl] kind10002 wait success: relay_count=%d", relay_count);
            for (int i = 0; i < relay_count; i++) {
                DEBUG_INFO("[didactyl] kind10002 relay[%d]=%s", i, relays[i] ? relays[i] : "");
            }
            char* status_after = nostr_handler_relay_status_json();
            if (status_after) {
                DEBUG_INFO("[didactyl] kind10002 wait success relay snapshot: %s", status_after);
                free(status_after);
            }
            *out_relays = relays;
            *out_relay_count = relay_count;
            return relay_count;
        }
        free_relay_list(relays, relay_count);

        char* status_before_poll = nostr_handler_relay_status_json();
        if (status_before_poll) {
            DEBUG_INFO("[didactyl] kind10002 wait retry: rc=%d relay_count=%d snapshot=%s",
                       rc,
                       relay_count,
                       status_before_poll);
            free(status_before_poll);
        } else {
            DEBUG_INFO("[didactyl] kind10002 wait retry: rc=%d relay_count=%d snapshot=<unavailable>", rc, relay_count);
        }

        (void)nostr_handler_poll(poll_ms);

        struct timespec now;
        if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
            break;
        }

        long elapsed_ms = (long)((now.tv_sec - start.tv_sec) * 1000L)
                        + (long)((now.tv_nsec - start.tv_nsec) / 1000000L);
        if (elapsed_ms >= timeout_ms) {
            break;
        }
    }

    DEBUG_WARN("[didactyl] kind10002 wait timed out after %dms", timeout_ms);
    return 0;
}

static int llm_config_is_complete(const llm_config_t* llm) {
    if (!llm) return 0;
    return llm->api_key[0] != '\0' &&
           llm->model[0] != '\0' &&
           llm->base_url[0] != '\0';
}

static int admin_config_is_complete(const didactyl_config_t* cfg) {
    return cfg && cfg->admin.pubkey[0] != '\0';
}

static const char* dm_protocol_to_string(dm_protocol_t protocol) {
    switch (protocol) {
        case DM_PROTOCOL_NIP17: return "nip17";
        case DM_PROTOCOL_BOTH: return "both";
        case DM_PROTOCOL_NIP04:
        default:
            return "nip04";
    }
}

static void dm_protocol_from_string(const char* s, didactyl_config_t* cfg) {
    if (!cfg || !s) return;
    if (strcmp(s, "nip17") == 0) {
        cfg->dm_protocol = DM_PROTOCOL_NIP17;
    } else if (strcmp(s, "both") == 0) {
        cfg->dm_protocol = DM_PROTOCOL_BOTH;
    } else {
        cfg->dm_protocol = DM_PROTOCOL_NIP04;
    }
}

static int decode_pubkey_hex_or_npub_local(const char* input, char out_hex[65]) {
    if (!input || !out_hex) return -1;

    if (strncmp(input, "npub1", 5) == 0) {
        unsigned char pubkey[32];
        if (nostr_decode_npub(input, pubkey) != 0) {
            return -1;
        }
        nostr_bytes_to_hex(pubkey, 32, out_hex);
        return 0;
    }

    if (strlen(input) == 64U) {
        unsigned char buf[32];
        if (nostr_hex_to_bytes(input, buf, 32) != 0) {
            return -1;
        }
        snprintf(out_hex, 65, "%s", input);
        return 0;
    }

    return -1;
}

static int fetch_self_config_plaintext(const didactyl_config_t* cfg, const char* d_tag, char** out_plaintext) {
    if (!cfg || !d_tag || !out_plaintext || cfg->keys.public_key_hex[0] == '\0') {
        return -1;
    }

    *out_plaintext = NULL;

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

    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(30078));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
    cJSON_AddItemToObject(filter, "authors", authors);
    cJSON_AddItemToArray(d_vals, cJSON_CreateString(d_tag));
    cJSON_AddItemToObject(filter, "#d", d_vals);
    cJSON_AddNumberToObject(filter, "limit", 1);

    char* events_json = nostr_handler_query_json(filter, 4000);
    cJSON_Delete(filter);
    if (!events_json) {
        return -1;
    }

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

    cJSON* ev = cJSON_GetArrayItem(arr, 0);
    cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
    if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
        cJSON_Delete(arr);
        return -1;
    }

    size_t out_cap = strlen(content->valuestring) + 1024U;
    char* plaintext = (char*)malloc(out_cap);
    if (!plaintext) {
        cJSON_Delete(arr);
        return -1;
    }

    int dec_rc = nostr_nip44_decrypt(cfg->keys.private_key,
                                     cfg->keys.public_key,
                                     content->valuestring,
                                     plaintext,
                                     out_cap);
    cJSON_Delete(arr);
    if (dec_rc != NOSTR_SUCCESS) {
        free(plaintext);
        return -1;
    }

    *out_plaintext = plaintext;
    return 0;
}

static int apply_recalled_llm_config(didactyl_config_t* cfg, const char* plaintext) {
    if (!cfg || !plaintext) return -1;

    cJSON* root = cJSON_Parse(plaintext);
    if (!root || !cJSON_IsObject(root)) {
        cJSON_Delete(root);
        return -1;
    }

    cJSON* provider = cJSON_GetObjectItemCaseSensitive(root, "provider");
    cJSON* api_key = cJSON_GetObjectItemCaseSensitive(root, "api_key");
    cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model");
    cJSON* base_url = cJSON_GetObjectItemCaseSensitive(root, "base_url");
    cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(root, "max_tokens");
    cJSON* temperature = cJSON_GetObjectItemCaseSensitive(root, "temperature");

    if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' ||
        !model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' ||
        !base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') {
        cJSON_Delete(root);
        return -1;
    }

    if (provider && cJSON_IsString(provider) && provider->valuestring && provider->valuestring[0] != '\0') {
        snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider->valuestring);
    } else if (cfg->llm.provider[0] == '\0') {
        snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai");
    }

    snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key->valuestring);
    snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model->valuestring);
    snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url->valuestring);
    if (max_tokens && cJSON_IsNumber(max_tokens)) {
        cfg->llm.max_tokens = (int)max_tokens->valuedouble;
    }
    if (temperature && cJSON_IsNumber(temperature)) {
        cfg->llm.temperature = temperature->valuedouble;
    }

    cJSON_Delete(root);
    return 0;
}

static int apply_recalled_agent_config(didactyl_config_t* cfg, const char* plaintext) {
    if (!cfg || !plaintext) return -1;

    cJSON* root = cJSON_Parse(plaintext);
    if (!root || !cJSON_IsObject(root)) {
        cJSON_Delete(root);
        return -1;
    }

    cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(root, "admin_pubkey");
    cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol");

    if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') {
        char decoded[65] = {0};
        if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) == 0) {
            snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
        }
    }

    if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') {
        dm_protocol_from_string(dm_protocol->valuestring, cfg);
    }

    cJSON_Delete(root);
    return 0;
}

static int publish_encrypted_self_config(const didactyl_config_t* cfg, const char* d_tag, const char* content_json) {
    if (!cfg || !d_tag || !content_json || d_tag[0] == '\0') return -1;

    size_t cipher_cap = (strlen(content_json) * 4U) + 1024U;
    char* ciphertext = (char*)malloc(cipher_cap);
    if (!ciphertext) return -1;

    int enc_rc = nostr_nip44_encrypt(cfg->keys.private_key,
                                     cfg->keys.public_key,
                                     content_json,
                                     ciphertext,
                                     cipher_cap);
    if (enc_rc != NOSTR_SUCCESS) {
        free(ciphertext);
        return -1;
    }

    cJSON* tags = cJSON_CreateArray();
    cJSON* d = cJSON_CreateArray();
    cJSON* app = cJSON_CreateArray();
    if (!tags || !d || !app) {
        cJSON_Delete(tags);
        cJSON_Delete(d);
        cJSON_Delete(app);
        free(ciphertext);
        return -1;
    }

    cJSON_AddItemToArray(d, cJSON_CreateString("d"));
    cJSON_AddItemToArray(d, cJSON_CreateString(d_tag));
    cJSON_AddItemToArray(tags, d);

    cJSON_AddItemToArray(app, cJSON_CreateString("app"));
    cJSON_AddItemToArray(app, cJSON_CreateString("didactyl"));
    cJSON_AddItemToArray(tags, app);

    nostr_publish_result_t result;
    memset(&result, 0, sizeof(result));
    int rc = nostr_handler_publish_kind_event(30078, ciphertext, tags, &result);
    nostr_handler_publish_result_free(&result);
    cJSON_Delete(tags);
    free(ciphertext);
    return rc;
}

static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg, int persist_llm_config) {
    if (!cfg) return -1;

    int llm_rc = 0;
    if (persist_llm_config) {
        cJSON* llm = cJSON_CreateObject();
        if (!llm) return -1;
        cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
        cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
        cJSON_AddStringToObject(llm, "model", cfg->llm.model);
        cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
        cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
        cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);

        char* llm_json = cJSON_PrintUnformatted(llm);
        cJSON_Delete(llm);
        if (!llm_json) return -1;

        llm_rc = publish_encrypted_self_config(cfg, "llm_config", llm_json);
        free(llm_json);
    } else {
        DEBUG_INFO("[didactyl] startup phase: skipping llm_config publish because it was recalled from Nostr");
    }

    cJSON* agent = cJSON_CreateObject();
    if (!agent) return llm_rc;
    cJSON_AddStringToObject(agent, "admin_pubkey", cfg->admin.pubkey);
    cJSON_AddStringToObject(agent, "dm_protocol", dm_protocol_to_string(cfg->dm_protocol));
    char* agent_json = cJSON_PrintUnformatted(agent);
    cJSON_Delete(agent);
    if (!agent_json) return llm_rc;

    int agent_rc = publish_encrypted_self_config(cfg, "agent_config", agent_json);
    free(agent_json);

    return (llm_rc == 0 && agent_rc == 0) ? 0 : -1;
}

static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
    if (!cfg) {
        return 0;
    }

    int llm_recalled_from_nostr = 0;

    if (!llm_config_is_complete(&cfg->llm)) {
        char* llm_plaintext = NULL;
        if (fetch_self_config_plaintext(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
            if (apply_recalled_llm_config(cfg, llm_plaintext) == 0) {
                llm_recalled_from_nostr = 1;
                DEBUG_INFO("[didactyl] startup phase: recovered llm_config from Nostr");
            } else {
                DEBUG_WARN("[didactyl] startup phase: failed to apply recalled llm_config");
            }
        } else {
            DEBUG_WARN("[didactyl] startup phase: llm_config recall unavailable");
        }
        free(llm_plaintext);
    }

    char* agent_plaintext = NULL;
    if (fetch_self_config_plaintext(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
        didactyl_config_t recalled = *cfg;
        if (apply_recalled_agent_config(&recalled, agent_plaintext) == 0) {
            if (!admin_config_is_complete(cfg)) {
                snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", recalled.admin.pubkey);
                cfg->dm_protocol = recalled.dm_protocol;
                DEBUG_INFO("[didactyl] startup phase: recovered agent_config from Nostr");
            } else if (recalled.admin.pubkey[0] != '\0' && strcmp(cfg->admin.pubkey, recalled.admin.pubkey) != 0) {
                DEBUG_WARN("[didactyl] startup phase: agent_config admin pubkey differs from loaded config (loaded=%.16s..., nostr=%.16s...); loaded config wins",
                           cfg->admin.pubkey,
                           recalled.admin.pubkey);
            }
        } else {
            DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled agent_config");
        }
    } else {
        DEBUG_WARN("[didactyl] startup phase: agent_config recall unavailable");
    }
    free(agent_plaintext);

    return llm_recalled_from_nostr;
}

int main(int argc, char** argv) {
    const char* config_path = "./genesis.jsonc";
    const char* cli_nsec = NULL;
    const char* cli_admin_pubkey = NULL;
    const char* api_bind_override = NULL;
    int api_port_override = 0;
    int debug_level = DEBUG_LEVEL_NONE;
    int dump_schemas = 0;
    const char* test_tool_name = NULL;
    const char* test_tool_args = "{}";

    didactyl_config_t cfg;
    memset(&cfg, 0, sizeof(cfg));
    int config_preloaded = 0;
    int bootstrap_mode = 0;

    if (argc == 1) {
        if (!isatty(STDIN_FILENO)) {
            print_usage(argv[0]);
            return 1;
        }

        debug_level = DEBUG_LEVEL_INFO;
        debug_init(debug_level);
        configure_mongoose_log_level_from_debug();
        DEBUG_INFO("%s %s starting (interactive setup)", DIDACTYL_NAME, DIDACTYL_VERSION);

        if (nostr_init() != NOSTR_SUCCESS) {
            fprintf(stderr, "Failed to initialize nostr core\n");
            return 1;
        }

        char genesis_path[256] = {0};
        int wizard_rc = setup_wizard_run(&cfg, genesis_path, sizeof(genesis_path));
        if (wizard_rc == SETUP_WIZARD_RC_ABORT) {
            config_free(&cfg);
            nostr_cleanup();
            return 1;
        }
        if (wizard_rc == SETUP_WIZARD_RC_EXIT) {
            config_free(&cfg);
            nostr_cleanup();
            return 0;
        }

        if (wizard_rc == SETUP_WIZARD_RC_BOOTSTRAP) {
            bootstrap_mode = 1;
        }

        config_preloaded = 1;
    }

    if (!config_preloaded) {
        int config_path_explicit = 0;
        for (int i = 1; i < argc; i++) {
            if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
                print_usage(argv[0]);
                return 0;
            } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) {
                printf("%s %s\n", DIDACTYL_NAME, DIDACTYL_VERSION);
                return 0;
            } else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) {
                config_path = argv[++i];
                config_path_explicit = 1;
            } else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) {
                debug_level = atoi(argv[++i]);
            } else if (strcmp(argv[i], "--nsec") == 0 && i + 1 < argc) {
                cli_nsec = argv[++i];
            } else if (strcmp(argv[i], "--admin") == 0 && i + 1 < argc) {
                cli_admin_pubkey = argv[++i];
            } else if (strcmp(argv[i], "--api-port") == 0 && i + 1 < argc) {
                api_port_override = atoi(argv[++i]);
            } else if (strcmp(argv[i], "--api-bind") == 0 && i + 1 < argc) {
                api_bind_override = argv[++i];
            } else if (strcmp(argv[i], "--dump-schemas") == 0) {
                dump_schemas = 1;
            } else if (strcmp(argv[i], "--test-tool") == 0 && i + 2 < argc) {
                test_tool_name = argv[++i];
                test_tool_args = argv[++i];
            } else {
                print_usage(argv[0]);
                return 1;
            }
        }

        if (config_path_explicit) {
            bootstrap_mode = 1;
        }

        debug_init(debug_level);
        configure_mongoose_log_level_from_debug();

        DEBUG_INFO("%s %s starting", DIDACTYL_NAME, DIDACTYL_VERSION);

        if (nostr_init() != NOSTR_SUCCESS) {
            fprintf(stderr, "Failed to initialize nostr core\n");
            return 1;
        }

        if (config_load(config_path, &cfg) != 0) {
            fprintf(stderr, "Failed to load config: %s\n", config_path);
            fprintf(stderr, "Config error: %s\n", config_last_error());
            nostr_cleanup();
            return 1;
        }
    }

    if (cfg.keys.nsec[0] == '\0') {
        const char* env_nsec = getenv("DIDACTYL_NSEC");
        const char* input_nsec = (cli_nsec && cli_nsec[0] != '\0') ? cli_nsec : env_nsec;
        if (!input_nsec || input_nsec[0] == '\0') {
            fprintf(stderr, "No nsec available: provide key.nsec in config, --nsec, or DIDACTYL_NSEC\n");
            config_free(&cfg);
            nostr_cleanup();
            return 1;
        }
        if (derive_keys_from_nsec(input_nsec, &cfg) != 0) {
            fprintf(stderr, "Invalid nsec provided via --nsec or DIDACTYL_NSEC\n");
            config_free(&cfg);
            nostr_cleanup();
            return 1;
        }
    }

    if (cli_admin_pubkey && cli_admin_pubkey[0] != '\0') {
        char decoded_admin[65] = {0};
        if (decode_pubkey_hex_or_npub_local(cli_admin_pubkey, decoded_admin) != 0) {
            fprintf(stderr, "Invalid admin pubkey provided via --admin (expected npub1... or 64-char hex)\n");
            config_free(&cfg);
            nostr_cleanup();
            return 1;
        }
        snprintf(cfg.admin.pubkey, sizeof(cfg.admin.pubkey), "%s", decoded_admin);
        DEBUG_INFO("[didactyl] startup phase: admin override applied from --admin (%.16s...)", cfg.admin.pubkey);
    }

    if (config_ensure_startup_skill_adoption(&cfg) != 0) {
        fprintf(stderr, "Failed to synthesize startup skill adoption events\n");
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    if (api_port_override > 0 && api_port_override <= 65535) {
        cfg.api.enabled = 1;
        cfg.api.port = api_port_override;
    }
    if (api_bind_override && api_bind_override[0] != '\0') {
        cfg.api.enabled = 1;
        snprintf(cfg.api.bind_address, sizeof(cfg.api.bind_address), "%s", api_bind_override);
    }

    {
        const char* ca_bundle = nostr_http_detect_ca_bundle();
        if (ca_bundle && ca_bundle[0] != '\0') {
            nostr_http_set_ca_bundle(ca_bundle);
        }
    }

    if (dump_schemas || test_tool_name) {
        if (llm_init(&cfg.llm) != 0) {
            fprintf(stderr, "Failed to initialize llm client\n");
            config_free(&cfg);
            nostr_cleanup();
            return 1;
        }

        if (nostr_handler_init(&cfg) != 0) {
            fprintf(stderr, "Failed to initialize nostr handler\n");
            config_free(&cfg);
            llm_cleanup();
            nostr_cleanup();
            return 1;
        }

        (void)nostr_block_list_init(&cfg);
        (void)nostr_block_list_load_from_relays();

        tools_context_t tools_ctx;
        if (tools_init(&tools_ctx, &cfg) != 0) {
            fprintf(stderr, "Failed to initialize tools context\n");
            nostr_block_list_cleanup();
            nostr_handler_cleanup();
            config_free(&cfg);
            llm_cleanup();
            nostr_cleanup();
            return 1;
        }

        int wallet_initialized = 0;
        int exit_code = 0;

        if (cashu_wallet_init(&cfg) == 0) {
            wallet_initialized = 1;
            if (cfg.cashu_wallet.enabled) {
                (void)nostr_handler_subscribe_wallet_events();
                int wallet_rc = 0;
                if (cfg.cashu_wallet.auto_load) {
                    wallet_rc = cashu_wallet_load_from_relays();
                }
                if (wallet_rc != 0 && cfg.cashu_wallet.mint_count > 0) {
                    (void)cashu_wallet_create_new_from_config();
                }
            }
        }

        if (test_tool_name) {
            const int timeout_ms = 15000;
            if (!wait_for_connected_relays(timeout_ms)) {
                fprintf(stderr,
                        "No relay connection established within %d ms; refusing to run --test-tool '%s'\n",
                        timeout_ms,
                        test_tool_name);
                tools_cleanup(&tools_ctx);
                nostr_block_list_cleanup();
                nostr_handler_cleanup();
                config_free(&cfg);
                llm_cleanup();
                nostr_cleanup();
                return 1;
            }
        }

        if (dump_schemas) {
            char* schemas = tools_build_openai_schema_json(&tools_ctx);
            if (!schemas) {
                fprintf(stderr, "Failed to build tool schemas\n");
                exit_code = 1;
            } else {
                printf("%s\n", schemas);
                free(schemas);
            }
        } else {
            char* result = tools_execute(&tools_ctx, test_tool_name, test_tool_args);
            if (!result) {
                fprintf(stderr, "Failed to execute tool\n");
                exit_code = 1;
            } else {
                printf("%s\n", result);
                exit_code = tool_result_success(result) ? 0 : 1;
                free(result);
            }
        }

        tools_cleanup(&tools_ctx);
        if (wallet_initialized) {
            cashu_wallet_cleanup();
        }
        nostr_block_list_cleanup();
        nostr_handler_cleanup();
        config_free(&cfg);
        llm_cleanup();
        nostr_cleanup();
        return exit_code;
    }

    if (nostr_handler_init(&cfg) != 0) {
        fprintf(stderr, "Failed to initialize nostr handler\n");
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    (void)nostr_block_list_init(&cfg);
    (void)nostr_block_list_load_from_relays();

    const int startup_connect_timeout_ms = 15000;
    startup_step_begin(1, "Relay connectivity");
    if (!wait_for_connected_relays(startup_connect_timeout_ms)) {
        startup_step_fail(1, "Relay connectivity", "no relay connected within timeout; continuing best-effort");
        DEBUG_WARN("[didactyl] startup phase: no relay connected within %d ms; continuing with best effort", startup_connect_timeout_ms);
    } else {
        char detail[64];
        snprintf(detail, sizeof(detail), "connected relays: %d/%d", nostr_handler_connected_relay_count(), cfg.relay_count);
        startup_step_ok(1, "Relay connectivity", detail);
    }

    startup_step_begin(2, "Recover runtime config from Nostr");
    int llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg);
    startup_step_ok(2, "Recover runtime config from Nostr", llm_recalled_from_nostr ? "llm_config recalled" : NULL);

    startup_step_begin(3, "Validate LLM config");
    if (!llm_config_is_complete(&cfg.llm)) {
        startup_step_fail(3, "Validate LLM config", "missing llm_config (model/base_url/api_key)");
        fprintf(stderr,
                "Missing LLM config: provide genesis file values or store d=llm_config via config_store\n");
        nostr_block_list_cleanup();
        nostr_handler_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }
    startup_step_ok(3, "Validate LLM config", NULL);

    startup_step_begin(4, "Validate admin config");
    if (!admin_config_is_complete(&cfg)) {
        startup_step_fail(4, "Validate admin config", "missing admin pubkey");
        fprintf(stderr,
                "Missing admin config: provide admin.pubkey in genesis, pass --admin, or store d=agent_config via config_store\n");
        nostr_block_list_cleanup();
        nostr_handler_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }
    startup_step_ok(4, "Validate admin config", NULL);

    startup_step_begin(5, "Initialize LLM client");
    if (llm_init(&cfg.llm) != 0) {
        startup_step_fail(5, "Initialize LLM client", "llm_init failed");
        fprintf(stderr, "Failed to initialize llm client\n");
        nostr_block_list_cleanup();
        nostr_handler_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }
    startup_step_ok(5, "Initialize LLM client", NULL);

    startup_step_begin(6, "Detect first run via kind 10002");
    int first_run = is_first_run_from_kind10002(&cfg);
    DEBUG_INFO("[didactyl] startup phase: first-run detection via kind 10002 => %s", first_run ? "first-run" : "subsequent-run");
    startup_step_ok(6, "Detect first run via kind 10002", first_run ? "first-run" : "subsequent-run");

    startup_step_begin(7, "Reconcile/persist startup state");
    if (bootstrap_mode || first_run) {
        if (nostr_handler_reconcile_startup_events() != 0) {
            startup_step_fail(7, "Reconcile/persist startup state", "startup event reconciliation failed");
            fprintf(stderr, "Failed to publish startup events; aborting startup\n");
            nostr_block_list_cleanup();
            nostr_handler_cleanup();
            llm_cleanup();
            config_free(&cfg);
            nostr_cleanup();
            return 1;
        }
    } else {
        DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run");
    }

    if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) {
        DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr");
    } else {
        DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr");
    }

    startup_step_ok(7, "Reconcile/persist startup state", NULL);

    startup_step_begin(8, "Initialize agent");
    if (agent_init(&cfg) != 0) {
        startup_step_fail(8, "Initialize agent", "agent_init failed");
        fprintf(stderr, "Failed to initialize agent\n");
        nostr_block_list_cleanup();
        nostr_handler_cleanup();
        llm_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }
    startup_step_ok(8, "Initialize agent", NULL);

    startup_step_begin(9, "Initialize trigger manager");
    trigger_manager_t trigger_manager;
    if (trigger_manager_init(&trigger_manager, &cfg) != 0) {
        startup_step_fail(9, "Initialize trigger manager", "trigger_manager_init failed");
        fprintf(stderr, "Failed to initialize trigger manager\n");
        agent_cleanup();
        nostr_block_list_cleanup();
        nostr_handler_cleanup();
        llm_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }
    startup_step_ok(9, "Initialize trigger manager", NULL);
    agent_set_trigger_manager(&trigger_manager);
    nostr_handler_set_trigger_manager(&trigger_manager);

    deferred_trigger_load_ctx_t deferred_trigger_ctx = {
        .trigger_manager = &trigger_manager,
        .loaded_once = 0
    };
    nostr_handler_set_self_skill_eose_callback(on_self_skill_eose_load_triggers, &deferred_trigger_ctx);

    DEBUG_INFO("[didactyl] startup phase: deferred trigger load will run after self-skill EOSE");

    startup_step_begin(10, "Load startup triggers");
    DEBUG_INFO("[didactyl] startup phase: startup-config trigger load begin");
    if (trigger_manager_load_from_startup_events(&trigger_manager) != 0) {
        DEBUG_WARN("[didactyl] startup phase: startup-config trigger load failed (continuing)");
    }
    DEBUG_INFO("[didactyl] startup phase: startup-config trigger load end");
    startup_step_ok(10, "Load startup triggers", NULL);

    startup_step_begin(11, "Discover self relay list via kind 10002");
    const int kind10002_timeout_ms = 25000;
    char** discovered_relays = NULL;
    int discovered_count = 0;
    if (wait_for_self_kind10002_relays(&cfg, kind10002_timeout_ms, 200, &discovered_relays, &discovered_count) <= 0) {
        startup_step_fail(11, "Discover self relay list via kind 10002", "required relay list not retrievable; aborting startup");
        fprintf(stderr, "Startup aborted: could not retrieve valid self kind 10002 within %d ms\n", kind10002_timeout_ms);
        agent_cleanup();
        nostr_block_list_cleanup();
        trigger_manager_cleanup(&trigger_manager);
        nostr_handler_cleanup();
        llm_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    int added_relays = merge_discovered_relays_into_config(&cfg, discovered_relays, discovered_count);
    free_relay_list(discovered_relays, discovered_count);
    discovered_relays = NULL;

    if (added_relays < 0 || nostr_handler_sync_relays_from_config() != 0) {
        startup_step_fail(11, "Discover self relay list via kind 10002", "failed to apply discovered relay list to pool");
        agent_cleanup();
        nostr_block_list_cleanup();
        trigger_manager_cleanup(&trigger_manager);
        nostr_handler_cleanup();
        llm_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    if (!wait_for_connected_relays(startup_connect_timeout_ms)) {
        DEBUG_WARN("[didactyl] startup phase: no relay connected after relay-list expansion; continuing best effort");
    }

    char relay_detail[128];
    snprintf(relay_detail,
             sizeof(relay_detail),
             "kind10002=%d added=%d connected=%d/%d",
             discovered_count,
             added_relays,
             nostr_handler_connected_relay_count(),
             cfg.relay_count);
    startup_step_ok(11, "Discover self relay list via kind 10002", relay_detail);

    startup_step_begin(12, "Subscribe admin context");
    DEBUG_INFO("[didactyl] startup phase: subscribe admin context begin");
    if (nostr_handler_subscribe_admin_context() != 0) {
        DEBUG_WARN("[didactyl] startup phase: subscribe admin context failed (continuing)");
    }
    DEBUG_INFO("[didactyl] startup phase: subscribe admin context end");
    startup_step_ok(12, "Subscribe admin context", NULL);

    startup_step_begin(13, "Subscribe agent self context");
    DEBUG_INFO("[didactyl] startup phase: subscribe agent self context begin");
    if (nostr_handler_subscribe_agent_context() != 0) {
        DEBUG_WARN("[didactyl] startup phase: subscribe agent self context failed (continuing)");
    }
    DEBUG_INFO("[didactyl] startup phase: subscribe agent self context end");
    startup_step_ok(13, "Subscribe agent self context", NULL);

    startup_step_begin(14, "Subscribe self-skill cache");
    DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache begin");
    if (nostr_handler_subscribe_self_skills() != 0) {
        startup_step_fail(14, "Subscribe self-skill cache", "nostr_handler_subscribe_self_skills failed");
        DEBUG_ERROR("[didactyl] startup phase: subscribe self skill cache failed");
        fprintf(stderr, "Startup aborted: failed to subscribe self-skill cache\n");
        agent_cleanup();
        nostr_block_list_cleanup();
        trigger_manager_cleanup(&trigger_manager);
        nostr_handler_cleanup();
        llm_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    const int self_skill_eose_timeout_ms = 15000;
    if (nostr_handler_wait_for_self_skill_eose(self_skill_eose_timeout_ms) != 0) {
        startup_step_fail(14,
                          "Subscribe self-skill cache",
                          "self-skill EOSE not received within timeout");
        fprintf(stderr,
                "Startup aborted: self-skill EOSE not received within %d ms\n",
                self_skill_eose_timeout_ms);
        fprintf(stderr,
                "Check relay connectivity (connected=%d/%d) and ensure skill events exist on relays\n",
                nostr_handler_connected_relay_count(),
                cfg.relay_count);
        fprintf(stderr,
                "If this is a first run, use genesis to publish startup events\n");
        agent_cleanup();
        nostr_block_list_cleanup();
        trigger_manager_cleanup(&trigger_manager);
        nostr_handler_cleanup();
        llm_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    int startup_skill_count = 0;
    int startup_adoption_count = 0;
    if (nostr_handler_validate_self_skill_cache(&startup_skill_count, &startup_adoption_count) != 0) {
        startup_step_fail(14,
                          "Subscribe self-skill cache",
                          "no skill events found after EOSE");
        fprintf(stderr,
                "Startup aborted: no skill events (kind 31123/31124) found in self-skill cache\n");
        fprintf(stderr,
                "The agent requires at least one skill event to operate\n");
        fprintf(stderr,
                "Run genesis to publish the default skill, or publish a skill manually\n");
        agent_cleanup();
        nostr_block_list_cleanup();
        trigger_manager_cleanup(&trigger_manager);
        nostr_handler_cleanup();
        llm_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    char self_skill_detail[128];
    snprintf(self_skill_detail,
             sizeof(self_skill_detail),
             "skills=%d adoptions=%d",
             startup_skill_count,
             startup_adoption_count);
    DEBUG_INFO("[didactyl] startup phase: subscribe self skill cache end");
    startup_step_ok(14, "Subscribe self-skill cache", self_skill_detail);

    startup_step_begin(15, "Subscribe DMs");
    DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
    if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
        startup_step_fail(15, "Subscribe DMs", "nostr_handler_subscribe_dms failed");
        DEBUG_ERROR("[didactyl] startup phase: subscribe DMs failed");
        fprintf(stderr, "Failed to subscribe to DMs\n");
        agent_cleanup();
        nostr_block_list_cleanup();
        trigger_manager_cleanup(&trigger_manager);
        nostr_handler_cleanup();
        llm_cleanup();
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    DEBUG_INFO("[didactyl] startup phase: subscribe DMs end");
    startup_step_ok(15, "Subscribe DMs", NULL);

    startup_step_begin(16, "Subscribe wallet events");
    DEBUG_INFO("[didactyl] startup phase: subscribe wallet events begin");
    if (nostr_handler_subscribe_wallet_events() != 0) {
        DEBUG_WARN("[didactyl] startup phase: subscribe wallet events failed (continuing)");
    }
    DEBUG_INFO("[didactyl] startup phase: subscribe wallet events end");
    startup_step_ok(16, "Subscribe wallet events", NULL);

    startup_step_begin(17, "Initialize cashu wallet");
    if (cashu_wallet_init(&cfg) != 0) {
        DEBUG_WARN("[didactyl] startup phase: cashu_wallet_init failed (continuing without wallet)");
        startup_step_fail(17, "Initialize cashu wallet", "cashu_wallet_init failed; wallet tools may return errors");
    } else {
        int wallet_rc = 0;
        if (cfg.cashu_wallet.auto_load) {
            wallet_rc = cashu_wallet_load_from_relays();
        }
        if (wallet_rc != 0 && cfg.cashu_wallet.mint_count > 0) {
            wallet_rc = cashu_wallet_create_new_from_config();
        }
        if (wallet_rc != 0) {
            DEBUG_WARN("[didactyl] startup phase: cashu wallet load/create deferred to first tool call");
            startup_step_ok(17, "Initialize cashu wallet", "initialized; load/create deferred");
        } else {
            startup_step_ok(17, "Initialize cashu wallet", cfg.cashu_wallet.auto_load ? "loaded or created" : "initialized");
        }
    }

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    (void)sigaction(SIGINT, &sa, NULL);
    (void)sigaction(SIGTERM, &sa, NULL);

    struct sigaction sa_pipe;
    memset(&sa_pipe, 0, sizeof(sa_pipe));
    sa_pipe.sa_handler = SIG_IGN;
    sigemptyset(&sa_pipe.sa_mask);
    sa_pipe.sa_flags = 0;
    (void)sigaction(SIGPIPE, &sa_pipe, NULL);

    int http_api_started = 0;
    if (cfg.api.enabled) {
        http_api_context_t http_ctx;
        memset(&http_ctx, 0, sizeof(http_ctx));
        http_ctx.cfg = &cfg;
        http_ctx.tools_ctx = agent_tools_context();
        http_ctx.trigger_manager = &trigger_manager;

        if (http_api_init(&http_ctx) != 0) {
            fprintf(stderr,
                    "Failed to initialize HTTP API on %s:%d (continuing without HTTP API)\n",
                    cfg.api.bind_address,
                    cfg.api.port);
            DEBUG_WARN("[didactyl] HTTP API disabled due to bind/init failure; agent will continue without local API");
            http_api_started = 0;
        } else {
            http_api_started = 1;
            DEBUG_INFO("[didactyl] HTTP API listening at http://%s:%d",
                       cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
                       cfg.api.port > 0 ? cfg.api.port : 8484);
            DEBUG_INFO("[didactyl] HTTP API endpoints: http://%s:%d/api/context/current  http://%s:%d/api/context/parts",
                       cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
                       cfg.api.port > 0 ? cfg.api.port : 8484,
                       cfg.api.bind_address[0] ? cfg.api.bind_address : "127.0.0.1",
                       cfg.api.port > 0 ? cfg.api.port : 8484);
        }
    } else {
        DEBUG_INFO("[didactyl] HTTP API disabled (set api.enabled=true in config to enable)");
    }

    nostr_handler_refresh_relay_statuses();

    char startup_dm[768];
    const char* startup_name = nostr_handler_get_startup_display_name();
    int connected_relays = nostr_handler_connected_relay_count();
    time_t startup_now = time(NULL);
    struct tm startup_tm;
    char startup_time_str[32] = {0};
    if (localtime_r(&startup_now, &startup_tm)) {
        (void)strftime(startup_time_str, sizeof(startup_time_str), "%Y-%m-%d %H:%M:%S", &startup_tm);
    } else {
        snprintf(startup_time_str, sizeof(startup_time_str), "%ld", (long)startup_now);
    }
    snprintf(startup_dm,
             sizeof(startup_dm),
             "%s has started up and is online at %s (version %s, connected relays: %d/%d).",
             (startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
             startup_time_str,
             DIDACTYL_VERSION,
             connected_relays,
             cfg.relay_count);
    if (nostr_handler_send_dm_auto(cfg.admin.pubkey, startup_dm) != 0) {
        DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
    }

    startup_step_ok(18, "READY", "agent online; entering main poll loop");
    DEBUG_INFO("[didactyl] entering main poll loop");
    DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);

    while (g_running) {
        (void)nostr_handler_poll(100);
        (void)trigger_manager_poll(&trigger_manager);
        if (http_api_started) {
            (void)http_api_poll(0);
        }
        struct timespec ts = {0, 10 * 1000 * 1000};
        nanosleep(&ts, NULL);
    }

    DEBUG_INFO("[didactyl] shutting down");

    if (http_api_started) {
        http_api_cleanup();
    }

    cashu_wallet_cleanup();
    agent_cleanup();
    nostr_block_list_cleanup();
    trigger_manager_cleanup(&trigger_manager);
    nostr_handler_cleanup();
    llm_cleanup();
    config_free(&cfg);
    nostr_cleanup();

    return 0;
}
