#define _POSIX_C_SOURCE 200809L

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.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"
#include "signer_health.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 int systemd_notify_send(const char* state) {
    if (!state || state[0] == '\0') {
        return 0;
    }

    const char* notify_socket = getenv("NOTIFY_SOCKET");
    if (!notify_socket || notify_socket[0] == '\0') {
        return 0;
    }

    if (notify_socket[0] != '/' && notify_socket[0] != '@') {
        return -1;
    }

    int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
    if (fd < 0) {
        return -1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;

    size_t path_len = strlen(notify_socket);
    if (path_len >= sizeof(addr.sun_path)) {
        close(fd);
        return -1;
    }

    memcpy(addr.sun_path, notify_socket, path_len + 1);
    socklen_t addr_len = (socklen_t)(sizeof(sa_family_t) + path_len + 1U);
    if (notify_socket[0] == '@') {
        addr.sun_path[0] = '\0';
        addr_len = (socklen_t)(sizeof(sa_family_t) + path_len);
    }

    ssize_t sent = sendto(fd,
                          state,
                          strlen(state),
                          MSG_NOSIGNAL,
                          (const struct sockaddr*)&addr,
                          addr_len);
    int saved_errno = errno;
    close(fd);
    errno = saved_errno;

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

    return 1;
}

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"
            "  --signer <mode>\n"
            "      Signer provider: local | nsigner_unix | nsigner_tcp |\n"
            "      nsigner_serial | nsigner_fds | nsigner_qrexec.\n"
            "      Overrides signer.mode in config and DIDACTYL_SIGNER.\n"
            "      In nsigner_* modes --nsec/DIDACTYL_NSEC are not required.\n"
            "  --signer-socket <name>\n"
            "      Abstract socket name for nsigner_unix (without leading @).\n"
            "  --signer-role <role>\n"
            "      n_signer role selector (default: main).\n"
            "  --signer-timeout <ms>\n"
            "      Per-call signer timeout in milliseconds (default: 15000).\n"
            "  --signer-tcp <host:port>\n"
            "      Shorthand for --signer nsigner_tcp with host/port parsed.\n"
            "  --signer-serial <device>\n"
            "      Shorthand for --signer nsigner_serial with device path\n"
            "      (e.g. /dev/ttyACM0).\n"
            "  --signer-fds <read_fd:write_fd>\n"
            "      Shorthand for --signer nsigner_fds with a pre-connected\n"
            "      fd pair (runtime-only; not persisted to genesis).\n"
            "  --signer-qrexec <target_qube>\n"
            "      Shorthand for --signer nsigner_qrexec with the target Qubes\n"
            "      qube name (service defaults to qubes.NsignerRpc).\n"
            "  --signer-service <name>\n"
            "      qrexec service name for nsigner_qrexec (default: qubes.NsignerRpc).\n"
            "  --signer-path <bip44-path>\n"
            "      Full BIP-44 derivation path (e.g. m/44'/1237'/0'/0/0). REQUIRED for\n"
            "      all nsigner_* modes — n_signer rejects role-only with 2009 path_required.\n"
            "  --signer-derive-index <n>\n"
            "      Algorithm index for the derive (HMAC) verb. Runtime-only; not persisted.\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 (local mode only).\n"
            "  DIDACTYL_SIGNER\n"
            "      Fallback signer mode when --signer 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) Sign with a running n_signer (separate the nsec from the agent)\n"
            "     %s --signer nsigner_unix --signer-socket nsigner_hairy_dog \\\n"
            "        --signer-role main --signer-path \"m/44'/1237'/0'/0/0\" --admin npub1...\n"
            "\n"
            "  6) n_signer via TCP\n"
            "     %s --signer-tcp 127.0.0.1:7777 --signer-role main \\\n"
            "        --signer-path \"m/44'/1237'/0'/0/0\" --admin npub1...\n"
            "\n"
            "  7) n_signer via USB serial\n"
            "     %s --signer-serial /dev/ttyACM0 --signer-role main \\\n"
            "        --signer-path \"m/44'/1237'/0'/0/0\" --admin npub1...\n"
            "\n"
            "  8) n_signer in another Qubes qube (qrexec, no network)\n"
            "     %s --signer-qrexec nostr_signer --signer-role main \\\n"
            "        --signer-path \"m/44'/1237'/0'/0/0\" --admin npub1...\n"
            "\n"
            "  9) Dump tool schemas\n"
            "     %s --dump-schemas\n"
            "\n"
            "  10) Test a tool invocation\n"
            "     %s --test-tool config_recall '{\"config_name\":\"llm_config\"}'\n",
            p,
            p,
            p,
            p,
            p,
            p,
            p,
            p,
            p,
            p,
            p);
}

/* Parse "host:port" into host buffer and port out-param.
 * Returns 0 on success, -1 on malformed input. */
static int parse_host_port(const char* host_port, char* host_out, size_t host_size, int* port_out) {
    if (!host_port || !host_out || !port_out || host_size == 0) {
        return -1;
    }
    const char* colon = strrchr(host_port, ':');
    if (!colon || colon == host_port) {
        return -1;
    }
    size_t host_len = (size_t)(colon - host_port);
    if (host_len == 0 || host_len >= host_size) {
        return -1;
    }
    memcpy(host_out, host_port, host_len);
    host_out[host_len] = '\0';
    int port = atoi(colon + 1);
    if (port <= 0 || port > 65535) {
        return -1;
    }
    *port_out = port;
    return 0;
}

/* Returns 1 if the signer mode is a remote (nsigner_*) mode, 0 otherwise. */
static int signer_mode_is_remote_local(const char* mode) {
    if (!mode || mode[0] == '\0') {
        return 0;
    }
    return strcmp(mode, "nsigner_unix") == 0 ||
           strcmp(mode, "nsigner_tcp") == 0 ||
           strcmp(mode, "nsigner_serial") == 0 ||
           strcmp(mode, "nsigner_fds") == 0 ||
           strcmp(mode, "nsigner_qrexec") == 0;
}

/* Parse a "read_fd:write_fd" pair string into two ints.
 * Returns 0 on success, -1 on malformed input. */
static int parse_fd_pair(const char* s, int* read_fd_out, int* write_fd_out) {
    if (!s || !read_fd_out || !write_fd_out) return -1;
    const char* colon = strchr(s, ':');
    if (!colon) return -1;
    char read_buf[16] = {0};
    size_t read_len = (size_t)(colon - s);
    if (read_len == 0 || read_len >= sizeof(read_buf)) return -1;
    memcpy(read_buf, s, read_len);
    int r = atoi(read_buf);
    int w = atoi(colon + 1);
    if (r < 0 || w < 0) return -1;
    *read_fd_out = r;
    *write_fd_out = w;
    return 0;
}

/* Apply CLI/env signer overrides onto cfg->signer.
 * Precedence: CLI flag > env var > genesis signer block > default.
 * Returns 0 on success, -1 on invalid override (with stderr message). */
static int apply_signer_overrides(didactyl_config_t* cfg,
                                  const char* cli_mode,
                                  const char* cli_socket,
                                  const char* cli_role,
                                  int cli_timeout,
                                  const char* cli_tcp,
                                  const char* cli_serial,
                                  const char* cli_fds,
                                  const char* cli_qrexec,
                                  const char* cli_service,
                                  const char* cli_path,
                                  int cli_derive_index) {
    if (!cfg) {
        return -1;
    }

    /* --signer-tcp shorthand: forces mode=nsigner_tcp and parses host:port. */
    if (cli_tcp && cli_tcp[0] != '\0') {
        if (parse_host_port(cli_tcp, cfg->signer.tcp_host,
                            sizeof(cfg->signer.tcp_host),
                            &cfg->signer.tcp_port) != 0) {
            fprintf(stderr, "Invalid --signer-tcp value (expected host:port, got '%s')\n", cli_tcp);
            return -1;
        }
        snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", "nsigner_tcp");
    } else if (cli_serial && cli_serial[0] != '\0') {
        /* --signer-serial shorthand: forces mode=nsigner_serial. */
        snprintf(cfg->signer.serial_device, sizeof(cfg->signer.serial_device), "%s", cli_serial);
        snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", "nsigner_serial");
    } else if (cli_fds && cli_fds[0] != '\0') {
        /* --signer-fds shorthand: forces mode=nsigner_fds and parses the pair. */
        if (parse_fd_pair(cli_fds, &cfg->signer.fds_read_fd, &cfg->signer.fds_write_fd) != 0) {
            fprintf(stderr, "Invalid --signer-fds value (expected read_fd:write_fd, got '%s')\n", cli_fds);
            return -1;
        }
        snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", "nsigner_fds");
    } else if (cli_qrexec && cli_qrexec[0] != '\0') {
        /* --signer-qrexec shorthand: forces mode=nsigner_qrexec. */
        snprintf(cfg->signer.target_qube, sizeof(cfg->signer.target_qube), "%s", cli_qrexec);
        snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", "nsigner_qrexec");
    } else if (cli_mode && cli_mode[0] != '\0') {
        snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", cli_mode);
    } else {
        const char* env_signer = getenv("DIDACTYL_SIGNER");
        if (env_signer && env_signer[0] != '\0') {
            snprintf(cfg->signer.mode, sizeof(cfg->signer.mode), "%s", env_signer);
        }
    }

    /* --signer-service overrides the qrexec service name regardless of mode. */
    if (cli_service && cli_service[0] != '\0') {
        snprintf(cfg->signer.service_name, sizeof(cfg->signer.service_name), "%s", cli_service);
    }

    /* Validate the final mode. */
    const char* mode = cfg->signer.mode;
    if (mode[0] == '\0' ||
        (strcmp(mode, "local") != 0 &&
         strcmp(mode, "nsigner_unix") != 0 &&
         strcmp(mode, "nsigner_tcp") != 0 &&
         strcmp(mode, "nsigner_serial") != 0 &&
         strcmp(mode, "nsigner_fds") != 0 &&
         strcmp(mode, "nsigner_qrexec") != 0)) {
        fprintf(stderr, "Unknown signer mode '%s' (expected: local, nsigner_unix, "
                        "nsigner_tcp, nsigner_serial, nsigner_fds, nsigner_qrexec)\n",
                mode[0] ? mode : "<empty>");
        return -1;
    }

    /* Mode-specific required-field checks. */
    if (strcmp(mode, "nsigner_serial") == 0 && cfg->signer.serial_device[0] == '\0') {
        fprintf(stderr, "nsigner_serial mode requires --signer-serial <device> or "
                        "signer.serial_device in config (mode=nsigner_serial)\n");
        return -1;
    }
    if (strcmp(mode, "nsigner_fds") == 0 &&
        (cfg->signer.fds_read_fd < 0 || cfg->signer.fds_write_fd < 0)) {
        fprintf(stderr, "nsigner_fds mode requires --signer-fds <read:write> "
                        "(mode=nsigner_fds); fds cannot be loaded from genesis\n");
        return -1;
    }
    if (strcmp(mode, "nsigner_qrexec") == 0 && cfg->signer.target_qube[0] == '\0') {
        fprintf(stderr, "nsigner_qrexec mode requires --signer-qrexec <target_qube> or "
                        "signer.target_qube in config (mode=nsigner_qrexec)\n");
        return -1;
    }
    /* Default the qrexec service name if unset. */
    if (strcmp(mode, "nsigner_qrexec") == 0 && cfg->signer.service_name[0] == '\0') {
        snprintf(cfg->signer.service_name, sizeof(cfg->signer.service_name), "%s", "qubes.NsignerRpc");
    }

    if (cli_socket && cli_socket[0] != '\0') {
        snprintf(cfg->signer.socket_name, sizeof(cfg->signer.socket_name), "%s", cli_socket);
    }
    if (cli_role && cli_role[0] != '\0') {
        snprintf(cfg->signer.role, sizeof(cfg->signer.role), "%s", cli_role);
    }
    if (cli_timeout > 0) {
        cfg->signer.timeout_ms = cli_timeout;
    }
    /* --signer-path overrides the genesis role_path. Required for all nsigner_*
     * modes — n_signer now rejects role-only selectors with 2009 path_required
     * and bare nostr_index with 2006 nostr_index_deprecated. */
    if (cli_path && cli_path[0] != '\0') {
        snprintf(cfg->signer.role_path, sizeof(cfg->signer.role_path), "%s", cli_path);
    }
    /* --signer-derive-index sets the algorithm index for the derive (HMAC) verb.
     * Runtime-only; not persisted to genesis. -1 means unset. */
    if (cli_derive_index >= 0) {
        cfg->signer.derive_index = cli_derive_index;
    }

    /* For all remote (nsigner_*) modes, require BOTH role and role_path.
     * n_signer rejects role-only with 2009 path_required and path-only with
     * 2008 role_required. This catches misconfiguration early with a clear
     * message instead of a cryptic RPC error at first sign attempt. */
    if (signer_mode_is_remote_local(mode)) {
        if (cfg->signer.role[0] == '\0') {
            fprintf(stderr, "%s mode requires --signer-role <role> or signer.role in "
                            "config (n_signer rejects path-only with 2008 role_required)\n",
                    mode);
            return -1;
        }
        if (cfg->signer.role_path[0] == '\0') {
            fprintf(stderr, "%s mode requires --signer-path <bip44-path> or signer.role_path "
                            "in config (n_signer rejects role-only with 2009 path_required). "
                            "Example: --signer-path \"m/44'/1237'/0'/0/0\"\n",
                    mode);
            return -1;
        }
    }

    return 0;
}

/* Construct the process-lifetime nostr_signer_t* from cfg->signer.
 * In local mode uses cfg->keys.private_key (existing behavior).
 * In remote modes delegates to the nsigner client.
 * Runs nostr_signer_get_public_key as a connectivity check and populates
 * cfg->keys.public_key/public_key_hex when remote.
 * Returns the signer handle on success, NULL on failure (with stderr message). */
static nostr_signer_t* construct_signer(didactyl_config_t* cfg) {
    if (!cfg) {
        return NULL;
    }

    const char* mode = cfg->signer.mode;
    nostr_signer_t* signer = NULL;

    if (strcmp(mode, "local") == 0) {
        signer = nostr_signer_local(cfg->keys.private_key);
        if (!signer) {
            fprintf(stderr, "Failed to construct local signer (mode=local): "
                            "nostr_signer_local returned NULL\n");
            signer_health_set_mode("local");
            signer_health_record_failure(NOSTR_ERROR_CRYPTO_FAILED,
                                         "nostr_signer_local returned NULL");
            return NULL;
        }
        /* Pubkey already derived from nsec in config_load/derive_keys path. */
        signer_health_set_mode("local");
        signer_health_record_ok();
        return signer;
    }

#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
    if (strcmp(mode, "nsigner_unix") == 0) {
        const char* sock = cfg->signer.socket_name[0] ? cfg->signer.socket_name : NULL;
        signer = nostr_signer_nsigner_unix(sock, cfg->signer.role, cfg->signer.timeout_ms);
    } else if (strcmp(mode, "nsigner_tcp") == 0) {
        if (cfg->signer.tcp_host[0] == '\0' || cfg->signer.tcp_port <= 0) {
            fprintf(stderr, "nsigner_tcp mode requires --signer-tcp <host:port> or "
                            "signer.tcp_host/tcp_port in config (mode=nsigner_tcp)\n");
            signer_health_set_mode("nsigner_tcp");
            signer_health_record_failure(NOSTR_ERROR_INVALID_INPUT,
                                         "missing tcp_host/tcp_port");
            return NULL;
        }
        signer = nostr_signer_nsigner_tcp(cfg->signer.tcp_host, cfg->signer.tcp_port,
                                          cfg->signer.role, cfg->signer.timeout_ms);
        if (signer && cfg->signer.auth_privkey_hex[0] != '\0') {
            unsigned char auth_key[32];
            if (nostr_hex_to_bytes(cfg->signer.auth_privkey_hex, auth_key, 32) == 0) {
                if (nostr_signer_nsigner_set_auth(signer, auth_key, "didactyl") != NOSTR_SUCCESS) {
                    fprintf(stderr, "Warning: failed to set nsigner TCP auth envelope "
                                    "(mode=nsigner_tcp, host=%s:%d)\n",
                            cfg->signer.tcp_host, cfg->signer.tcp_port);
                }
            } else {
                fprintf(stderr, "Warning: signer.auth_privkey_hex is not valid 64-char hex; "
                                "TCP auth not set (mode=nsigner_tcp, host=%s:%d)\n",
                        cfg->signer.tcp_host, cfg->signer.tcp_port);
            }
        }
    } else if (strcmp(mode, "nsigner_serial") == 0) {
        if (cfg->signer.serial_device[0] == '\0') {
            fprintf(stderr, "nsigner_serial mode requires --signer-serial <device> or "
                            "signer.serial_device in config (mode=nsigner_serial)\n");
            signer_health_set_mode("nsigner_serial");
            signer_health_record_failure(NOSTR_ERROR_INVALID_INPUT,
                                         "missing serial_device");
            return NULL;
        }
        signer = nostr_signer_nsigner_serial(cfg->signer.serial_device,
                                             cfg->signer.role, cfg->signer.timeout_ms);
    } else if (strcmp(mode, "nsigner_fds") == 0) {
        if (cfg->signer.fds_read_fd < 0 || cfg->signer.fds_write_fd < 0) {
            fprintf(stderr, "nsigner_fds mode requires --signer-fds <read:write> "
                            "(mode=nsigner_fds); fds cannot be loaded from genesis\n");
            signer_health_set_mode("nsigner_fds");
            signer_health_record_failure(NOSTR_ERROR_INVALID_INPUT,
                                         "missing fds_read_fd/fds_write_fd");
            return NULL;
        }
        signer = nostr_signer_nsigner_fds(cfg->signer.fds_read_fd, cfg->signer.fds_write_fd,
                                          cfg->signer.role, cfg->signer.timeout_ms);
    } else if (strcmp(mode, "nsigner_qrexec") == 0) {
        if (cfg->signer.target_qube[0] == '\0') {
            fprintf(stderr, "nsigner_qrexec mode requires --signer-qrexec <target_qube> or "
                            "signer.target_qube in config (mode=nsigner_qrexec)\n");
            signer_health_set_mode("nsigner_qrexec");
            signer_health_record_failure(NOSTR_ERROR_INVALID_INPUT,
                                         "missing target_qube");
            return NULL;
        }
        const char* svc = cfg->signer.service_name[0] ? cfg->signer.service_name : "qubes.NsignerRpc";
        signer = nostr_signer_nsigner_qrexec(cfg->signer.target_qube, svc,
                                             cfg->signer.role, cfg->signer.timeout_ms);
    } else {
        fprintf(stderr, "Unknown signer mode '%s' (expected: local, nsigner_unix, "
                        "nsigner_tcp, nsigner_serial, nsigner_fds, nsigner_qrexec)\n", mode);
        signer_health_set_mode(mode);
        signer_health_record_failure(NOSTR_ERROR_INVALID_INPUT, "unknown signer mode");
        return NULL;
    }

    /* Apply role_path selector (required for all nsigner_* modes — n_signer
     * rejects role-only with 2009 path_required). The role was already passed
     * to the factory constructor; set_role_path adds the full BIP-44 path. */
    if (signer && cfg->signer.role_path[0] != '\0') {
        if (nostr_signer_nsigner_set_role_path(signer, cfg->signer.role_path) != NOSTR_SUCCESS) {
            fprintf(stderr, "Warning: failed to set role_path='%s' on signer (mode=%s)\n",
                    cfg->signer.role_path, mode);
        }
    }
    /* Apply derive_index for the algorithm-based derive (HMAC) verb. -1 = unset. */
    if (signer && cfg->signer.derive_index >= 0) {
        if (nostr_signer_nsigner_set_derive_index(signer, cfg->signer.derive_index) != NOSTR_SUCCESS) {
            fprintf(stderr, "Warning: failed to set derive_index=%d on signer (mode=%s)\n",
                    cfg->signer.derive_index, mode);
        }
    }

    signer_health_set_mode(mode);
    if (!signer) {
        const char* sock = cfg->signer.socket_name[0] ? cfg->signer.socket_name : "<auto>";
        fprintf(stderr, "Failed to construct remote signer (mode=%s, socket=%s, role=%s, "
                        "role_path=%s, timeout_ms=%d): nostr_signer_nsigner_* returned NULL\n",
                mode, strcmp(mode, "nsigner_unix") == 0 ? sock : "<other>",
                cfg->signer.role[0] ? cfg->signer.role : "main",
                cfg->signer.role_path[0] ? cfg->signer.role_path : "<unset>",
                cfg->signer.timeout_ms);
        signer_health_record_failure(NOSTR_ERROR_IO_FAILED, "signer construction returned NULL");
        return NULL;
    }

    /* Connectivity check + pubkey discovery for remote modes. */
    char pubkey_hex[65] = {0};
    int gpk_rc = nostr_signer_get_public_key(signer, pubkey_hex);
    if (gpk_rc != NOSTR_SUCCESS) {
        const char* sock = cfg->signer.socket_name[0] ? cfg->signer.socket_name : "<auto>";
        const char* last_err = nostr_signer_last_error(signer);
        fprintf(stderr, "Signer connectivity check failed (mode=%s, socket=%s, role=%s, "
                        "role_path=%s, timeout_ms=%d, rc=%d): could not retrieve public key "
                        "from n_signer. Is the n_signer process running and reachable?\n",
                mode, strcmp(mode, "nsigner_unix") == 0 ? sock : "<other>",
                cfg->signer.role[0] ? cfg->signer.role : "main",
                cfg->signer.role_path[0] ? cfg->signer.role_path : "<unset>",
                cfg->signer.timeout_ms, gpk_rc);
        if (last_err && last_err[0] != '\0') {
            fprintf(stderr, "  n_signer error: %s\n", last_err);
            signer_health_record_failure(gpk_rc, last_err);
        } else {
            signer_health_record_failure(gpk_rc, "startup connectivity check (get_public_key)");
        }
        nostr_signer_free(signer);
        return NULL;
    }

    /* Populate cfg.keys.public_key/public_key_hex from the signer. */
    if (cfg->keys.public_key_hex[0] == '\0') {
        snprintf(cfg->keys.public_key_hex, sizeof(cfg->keys.public_key_hex), "%s", pubkey_hex);
        if (nostr_hex_to_bytes(pubkey_hex, cfg->keys.public_key, 32) != 0) {
            fprintf(stderr, "Warning: signer returned non-hex pubkey '%s' (mode=%s)\n",
                    pubkey_hex, mode);
        }
    }
    signer_health_record_ok();
    DEBUG_INFO("[didactyl] signer connected (mode=%s, agent pubkey=%.16s...)", mode, pubkey_hex);
    return signer;
#else
    fprintf(stderr, "Signer mode '%s' requires nsigner client support, which is not compiled in\n", mode);
    return NULL;
#endif
}

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 void startup_failure_marker_path(const didactyl_config_t* cfg, char* out, size_t out_size) {
    if (!out || out_size == 0) {
        return;
    }

    const char* cwd = ".";
    if (cfg && cfg->tools.shell.working_directory[0] != '\0') {
        cwd = cfg->tools.shell.working_directory;
    }

    size_t cwd_len = strlen(cwd);
    if (cwd_len > 0 && cwd[cwd_len - 1] == '/') {
        snprintf(out, out_size, "%s.didactyl_startup_failure", cwd);
    } else {
        snprintf(out, out_size, "%s/.didactyl_startup_failure", cwd);
    }
}

static int startup_failure_already_notified(const didactyl_config_t* cfg, int step) {
    char marker_path[512] = {0};
    startup_failure_marker_path(cfg, marker_path, sizeof(marker_path));

    FILE* fp = fopen(marker_path, "rb");
    if (!fp) {
        return 0;
    }

    int marker_step = -1;
    char line[512];
    while (fgets(line, sizeof(line), fp)) {
        if (sscanf(line, "step=%d", &marker_step) == 1) {
            break;
        }
    }
    fclose(fp);

    return marker_step == step ? 1 : 0;
}

static void startup_failure_marker_write(const didactyl_config_t* cfg,
                                         int step,
                                         const char* label,
                                         const char* detail) {
    char marker_path[512] = {0};
    startup_failure_marker_path(cfg, marker_path, sizeof(marker_path));

    FILE* fp = fopen(marker_path, "wb");
    if (!fp) {
        DEBUG_WARN("[didactyl] startup failure marker write failed: path=%s errno=%d", marker_path, errno);
        return;
    }

    const char* effective_label = (label && label[0] != '\0') ? label : "Unknown startup step";
    const char* effective_detail = (detail && detail[0] != '\0') ? detail : "no detail provided";
    time_t now = time(NULL);

    fprintf(fp, "step=%d\n", step);
    fprintf(fp, "label=%s\n", effective_label);
    fprintf(fp, "detail=%s\n", effective_detail);
    fprintf(fp, "last_sent_epoch=%ld\n", (long)now);
    fclose(fp);
}

static void startup_failure_marker_clear(const didactyl_config_t* cfg) {
    char marker_path[512] = {0};
    startup_failure_marker_path(cfg, marker_path, sizeof(marker_path));
    if (remove(marker_path) != 0 && errno != ENOENT) {
        DEBUG_WARN("[didactyl] startup failure marker clear failed: path=%s errno=%d", marker_path, errno);
    }
}

static const char* startup_step_needed_events(int step) {
    if (step == 14) {
        return "kind 31123/31124 (skills) and kind 10123 (adoption list) for this agent pubkey";
    }
    if (step == 11) {
        return "kind 10002 relay list for this agent pubkey";
    }
    return NULL;
}

static void notify_admin_startup_failure(const didactyl_config_t* cfg,
                                         int step,
                                         const char* label,
                                         const char* detail) {
    if (!cfg) {
        DEBUG_WARN("[didactyl] startup failure DM skipped: config unavailable (step=%d)", step);
        return;
    }

    if (cfg->admin.pubkey[0] == '\0') {
        DEBUG_WARN("[didactyl] startup failure DM skipped: admin pubkey unavailable (step=%d)", step);
        return;
    }

    int connected_relays = nostr_handler_connected_relay_count();
    if (connected_relays <= 0) {
        DEBUG_WARN("[didactyl] startup failure DM skipped: no connected relays (step=%d)", step);
        return;
    }

    if (startup_failure_already_notified(cfg, step)) {
        DEBUG_INFO("[didactyl] startup failure DM suppressed (already notified for step %d)", step);
        return;
    }

    const char* effective_label = (label && label[0] != '\0') ? label : "Unknown startup step";
    const char* effective_detail = (detail && detail[0] != '\0') ? detail : "no detail provided";
    const char* startup_name = nostr_handler_get_startup_display_name();
    const char* needed_events = startup_step_needed_events(step);
    char* relay_summary = nostr_handler_relay_state_summary();

    time_t now = time(NULL);
    struct tm tm_now;
    char time_str[32] = {0};
    if (localtime_r(&now, &tm_now)) {
        (void)strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &tm_now);
    } else {
        snprintf(time_str, sizeof(time_str), "%ld", (long)now);
    }

    char failure_dm[1536];
    if (needed_events && needed_events[0] != '\0') {
        snprintf(failure_dm,
                 sizeof(failure_dm),
                 "%s FAILED to start at %s. Startup step [%02d] %s failed: %s. Version %s. Connected relays: %d/%d. Relays: %s. Needed events: %s. The agent is NOT online and will not answer DMs until fixed.",
                 (startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
                 time_str,
                 step,
                 effective_label,
                 effective_detail,
                 DIDACTYL_VERSION,
                 connected_relays,
                 cfg->relay_count,
                 relay_summary ? relay_summary : "unavailable",
                 needed_events);
    } else {
        snprintf(failure_dm,
                 sizeof(failure_dm),
                 "%s FAILED to start at %s. Startup step [%02d] %s failed: %s. Version %s. Connected relays: %d/%d. Relays: %s. The agent is NOT online and will not answer DMs until fixed.",
                 (startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
                 time_str,
                 step,
                 effective_label,
                 effective_detail,
                 DIDACTYL_VERSION,
                 connected_relays,
                 cfg->relay_count,
                 relay_summary ? relay_summary : "unavailable");
    }

    if (nostr_handler_send_dm_auto(cfg->admin.pubkey, failure_dm) != 0) {
        DEBUG_WARN("[didactyl] startup failure DM send failed (step=%d)", step);
        free(relay_summary);
        return;
    }

    startup_failure_marker_write(cfg, step, effective_label, effective_detail);

    DEBUG_INFO("[didactyl] startup failure DM sent to admin for step %d", step);

    for (int i = 0; i < 4; i++) {
        (void)nostr_handler_poll(100);
    }

    free(relay_summary);
}

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,
                                       nostr_signer_t* signer,
                                       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", 20);

    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* newest_ev = NULL;
    long newest_created_at = -1;
    int n = cJSON_GetArraySize(arr);
    for (int i = 0; i < n; i++) {
        cJSON* ev = cJSON_GetArrayItem(arr, i);
        if (!ev || !cJSON_IsObject(ev)) {
            continue;
        }
        cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content");
        if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
            continue;
        }
        cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
        long ts = (created_at && cJSON_IsNumber(created_at)) ? (long)created_at->valuedouble : 0;
        if (!newest_ev || ts > newest_created_at) {
            newest_ev = ev;
            newest_created_at = ts;
        }
    }

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

    /* Phase 3 item 13: route self-decrypt through the process signer when
     * available so remote signer modes keep the nsec out of the agent. The
     * signer verb returns a heap string via out-param. Falls back to the
     * legacy raw-key path when no signer is passed. */
    char* plaintext = NULL;
    if (signer) {
        int dec_rc = nostr_signer_nip44_decrypt(signer,
                                                 cfg->keys.public_key_hex,
                                                 content->valuestring,
                                                 &plaintext);
        cJSON_Delete(arr);
        if (dec_rc != NOSTR_SUCCESS || !plaintext) {
            return -1;
        }
        *out_plaintext = plaintext;
        return 0;
    }

    size_t out_cap = strlen(content->valuestring) + 1024U;
    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_user_settings(didactyl_config_t* cfg,
                                        const char* plaintext,
                                        int* out_backfill_needed) {
    if (!cfg || !plaintext) return -1;
    if (out_backfill_needed) {
        *out_backfill_needed = 0;
    }

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

    cJSON* global_llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
    cJSON* didactyl = cJSON_GetObjectItemCaseSensitive(root, "didactyl");
    if (!global_llm || !cJSON_IsObject(global_llm)) {
        cJSON_Delete(root);
        return -1;
    }

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

    cJSON* admin_pubkey = NULL;
    cJSON* dm_protocol = NULL;
    cJSON* max_turns = NULL;
    if (didactyl && cJSON_IsObject(didactyl)) {
        admin_pubkey = cJSON_GetObjectItemCaseSensitive(didactyl, "admin_pubkey");
        dm_protocol = cJSON_GetObjectItemCaseSensitive(didactyl, "dm_protocol");
        max_turns = cJSON_GetObjectItemCaseSensitive(didactyl, "max_turns");
    } else if (out_backfill_needed) {
        *out_backfill_needed = 1;
    }

    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;
    }

    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) {
            cJSON_Delete(root);
            return -1;
        }
        snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
    } else if (out_backfill_needed) {
        *out_backfill_needed = 1;
    }

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

    if (max_turns && cJSON_IsNumber(max_turns)) {
        int parsed = (int)max_turns->valuedouble;
        cfg->tools.max_turns = (parsed > 0) ? parsed : 40;
    }

    cJSON_Delete(root);
    return 0;
}

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

    /* Phase 3 item 13: route self-encrypt through the process signer when
     * available. Falls back to the legacy raw-key path when no signer. */
    char* ciphertext = NULL;
    if (signer) {
        int enc_rc = nostr_signer_nip44_encrypt(signer,
                                                 cfg->keys.public_key_hex,
                                                 content_json,
                                                 &ciphertext);
        if (enc_rc != NOSTR_SUCCESS || !ciphertext) {
            return -1;
        }
    } else {
        size_t cipher_cap = (strlen(content_json) * 4U) + 1024U;
        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,
                                           nostr_signer_t* signer,
                                           int persist_llm_config) {
    if (!cfg) return -1;

    if (!persist_llm_config) {
        DEBUG_INFO("[didactyl] startup phase: skipping user-settings publish because runtime config was recalled from Nostr");
        return 0;
    }

    cJSON* user_settings = cJSON_CreateObject();
    cJSON* global_llm = cJSON_CreateObject();
    cJSON* didactyl = cJSON_CreateObject();
    if (!user_settings || !global_llm || !didactyl) {
        cJSON_Delete(user_settings);
        cJSON_Delete(global_llm);
        cJSON_Delete(didactyl);
        return -1;
    }

    cJSON_AddNumberToObject(user_settings, "v", 2);
    cJSON_AddNumberToObject(user_settings, "updatedAt", (double)time(NULL));

    cJSON_AddStringToObject(global_llm, "provider", cfg->llm.provider);
    cJSON_AddStringToObject(global_llm, "api_key", cfg->llm.api_key);
    cJSON_AddStringToObject(global_llm, "model", cfg->llm.model);
    cJSON_AddStringToObject(global_llm, "base_url", cfg->llm.base_url);
    cJSON_AddNumberToObject(global_llm, "max_tokens", cfg->llm.max_tokens);
    cJSON_AddNumberToObject(global_llm, "temperature", cfg->llm.temperature);
    cJSON_AddItemToObject(user_settings, "global_llm", global_llm);

    cJSON_AddStringToObject(didactyl, "admin_pubkey", cfg->admin.pubkey);
    cJSON_AddStringToObject(didactyl, "dm_protocol", dm_protocol_to_string(cfg->dm_protocol));
    cJSON_AddNumberToObject(didactyl, "max_turns", cfg->tools.max_turns > 0 ? cfg->tools.max_turns : 40);
    cJSON_AddItemToObject(user_settings, "didactyl", didactyl);

    char* user_settings_json = cJSON_PrintUnformatted(user_settings);
    cJSON_Delete(user_settings);
    if (!user_settings_json) {
        return -1;
    }

    int rc = publish_encrypted_self_config(cfg, signer, "user-settings", user_settings_json);
    free(user_settings_json);
    return rc;
}

static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg,
                                                     nostr_signer_t* signer,
                                                     int* out_backfill_needed) {
    if (!cfg) {
        return 0;
    }
    if (out_backfill_needed) {
        *out_backfill_needed = 0;
    }

    char* settings_plaintext = NULL;
    if (fetch_self_config_plaintext(cfg, signer, "user-settings", &settings_plaintext) != 0 || !settings_plaintext) {
        DEBUG_WARN("[didactyl] startup phase: user-settings recall unavailable");
        free(settings_plaintext);
        return 0;
    }

    int backfill_needed = 0;
    if (apply_recalled_user_settings(cfg, settings_plaintext, &backfill_needed) != 0) {
        DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled user-settings");
        free(settings_plaintext);
        return 0;
    }

    free(settings_plaintext);
    if (out_backfill_needed) {
        *out_backfill_needed = backfill_needed;
    }
    if (backfill_needed) {
        DEBUG_INFO("[didactyl] startup phase: recovered user-settings from Nostr (missing didactyl fields will be backfilled)");
    } else {
        DEBUG_INFO("[didactyl] startup phase: recovered user-settings from Nostr");
    }
    return 1;
}

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 = "{}";
    /* Signer CLI overrides (NULL/0 = not provided, use config/env/default). */
    const char* cli_signer_mode = NULL;
    const char* cli_signer_socket = NULL;
    const char* cli_signer_role = NULL;
    int cli_signer_timeout = 0;
    const char* cli_signer_tcp = NULL;     /* host:port shorthand */
    const char* cli_signer_serial = NULL;  /* device path shorthand */
    const char* cli_signer_fds = NULL;     /* read_fd:write_fd shorthand */
    const char* cli_signer_qrexec = NULL;  /* target qube shorthand */
    const char* cli_signer_service = NULL; /* qrexec service name */
    const char* cli_signer_path = NULL;    /* full BIP-44 derivation path */
    int cli_signer_derive_index = -1;      /* derive verb algorithm index (-1 = unset) */
    didactyl_config_t cfg;
    memset(&cfg, 0, sizeof(cfg));
    nostr_signer_t* g_signer = NULL;
    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], "--signer") == 0 && i + 1 < argc) {
                cli_signer_mode = argv[++i];
            } else if (strcmp(argv[i], "--signer-socket") == 0 && i + 1 < argc) {
                cli_signer_socket = argv[++i];
            } else if (strcmp(argv[i], "--signer-role") == 0 && i + 1 < argc) {
                cli_signer_role = argv[++i];
            } else if (strcmp(argv[i], "--signer-timeout") == 0 && i + 1 < argc) {
                cli_signer_timeout = atoi(argv[++i]);
            } else if (strcmp(argv[i], "--signer-tcp") == 0 && i + 1 < argc) {
                cli_signer_tcp = argv[++i];
            } else if (strcmp(argv[i], "--signer-serial") == 0 && i + 1 < argc) {
                cli_signer_serial = argv[++i];
            } else if (strcmp(argv[i], "--signer-fds") == 0 && i + 1 < argc) {
                cli_signer_fds = argv[++i];
            } else if (strcmp(argv[i], "--signer-qrexec") == 0 && i + 1 < argc) {
                cli_signer_qrexec = argv[++i];
            } else if (strcmp(argv[i], "--signer-service") == 0 && i + 1 < argc) {
                cli_signer_service = argv[++i];
            } else if (strcmp(argv[i], "--signer-path") == 0 && i + 1 < argc) {
                cli_signer_path = argv[++i];
            } else if (strcmp(argv[i], "--signer-derive-index") == 0 && i + 1 < argc) {
                cli_signer_derive_index = atoi(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 if (strcmp(argv[i], "--test-tool") == 0) {
                /* --test-tool requires exactly two arguments: <name> <args_json>.
                 * Without them the generic "unknown arg" usage dump is misleading
                 * (it looks like a startup/signer failure when the real issue is
                 * a missing args_json operand). Emit a precise error instead. */
                if (i + 1 >= argc) {
                    fprintf(stderr, "--test-tool requires <name> <args_json> (missing both)\n");
                } else {
                    fprintf(stderr, "--test-tool requires <name> <args_json> (missing args_json after '%s')\n", argv[i + 1]);
                }
                return 1;
            } 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;
        }
    }

    /* Apply signer CLI/env overrides (precedence: CLI > env > config > default).
     * Done before the nsec-required check so remote modes can skip the nsec. */
    if (apply_signer_overrides(&cfg, cli_signer_mode, cli_signer_socket,
                               cli_signer_role, cli_signer_timeout,
                               cli_signer_tcp, cli_signer_serial,
                               cli_signer_fds, cli_signer_qrexec,
                               cli_signer_service, cli_signer_path,
                               cli_signer_derive_index) != 0) {
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    /* In local mode an nsec is required (existing behavior). In remote
     * (nsigner_*) modes the nsec is optional and ignored; the pubkey is
     * learned from the signer. */
    if (!signer_mode_is_remote_local(cfg.signer.mode)) {
        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;
            }
        }
    } else {
        DEBUG_INFO("[didactyl] signer mode '%s': nsec not required, agent pubkey will be discovered from signer",
                   cfg.signer.mode);
    }

    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 (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);
        }
    }

    /* Construct the process-lifetime signer handle. In local mode this wraps
     * cfg.keys.private_key (output-equivalent to the legacy raw-key path).
     * In remote modes this performs the mandatory startup connectivity check
     * and populates cfg.keys.public_key/public_key_hex from the signer.
     *
     * This must run before config_ensure_startup_skill_adoption() so the
     * agent pubkey is known (from the signer in remote mode) before skill
     * adoption events are synthesized. */
    g_signer = construct_signer(&cfg);
    if (!g_signer) {
        config_free(&cfg);
        nostr_cleanup();
        return 1;
    }

    /* Wire the signer into the nostr handler so relay pool AUTH is routed
     * through it. In local mode this is output-equivalent to the legacy
     * raw-key path. The handler does not own the signer. */
    nostr_handler_set_signer(g_signer);

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

    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);
        nostr_block_list_set_signer(g_signer);
        (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;
        }
        tools_set_signer(&tools_ctx, g_signer);

        int wallet_initialized = 0;
        int exit_code = 0;

        cashu_wallet_set_signer(g_signer);
        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_signer_free(g_signer);
        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);
    nostr_block_list_set_signer(g_signer);
    (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 user_settings_backfill_needed = 0;
    int llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg, g_signer, &user_settings_backfill_needed);
    startup_step_ok(2,
                    "Recover runtime config from Nostr",
                    llm_recalled_from_nostr
                        ? (user_settings_backfill_needed ? "user-settings recalled (backfill required)" : "user-settings recalled")
                        : NULL);

    startup_step_begin(3, "Validate LLM config");
    if (!llm_config_is_complete(&cfg.llm)) {
        startup_step_fail(3, "Validate LLM config", "missing user-settings.global_llm (model/base_url/api_key)");
        notify_admin_startup_failure(&cfg,
                                     3,
                                     "Validate LLM config",
                                     "missing user-settings.global_llm (model/base_url/api_key)");
        fprintf(stderr,
                "Missing LLM config: provide genesis file values or store d=user-settings with global_llm\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");
        notify_admin_startup_failure(&cfg, 4, "Validate admin config", "missing admin pubkey");
        fprintf(stderr,
                "Missing admin config: provide admin.pubkey in genesis, pass --admin, or store d=user-settings with didactyl.admin_pubkey\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");
        notify_admin_startup_failure(&cfg, 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");
            notify_admin_startup_failure(&cfg,
                                         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 (bootstrap_mode || first_run || user_settings_backfill_needed) {
        if (persist_runtime_config_to_nostr(&cfg,
                                            g_signer,
                                            (llm_recalled_from_nostr && !user_settings_backfill_needed) ? 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");
        }
    } else {
        DEBUG_INFO("[didactyl] startup phase: skipping encrypted runtime config publish on subsequent run");
    }

    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");
        notify_admin_startup_failure(&cfg, 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;
    }
    agent_set_signer(g_signer);
    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");
        notify_admin_startup_failure(&cfg, 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");
        notify_admin_startup_failure(&cfg,
                                     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");
        notify_admin_startup_failure(&cfg,
                                     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");
        notify_admin_startup_failure(&cfg,
                                     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_sync_attempt_timeout_ms = 4000;
    const int self_skill_sync_total_budget_ms = 15000;
    const int self_skill_retry_step_ms = 250;
    int self_skill_waited_ms = 0;
    int self_skill_refresh_attempts = 0;
    int self_skill_refresh_rc = -1;
    int self_skill_validate_rc = -1;
    int startup_skill_count = 0;
    int startup_adoption_count = 0;

    while (self_skill_waited_ms <= self_skill_sync_total_budget_ms) {
        int connected_relays = nostr_handler_connected_relay_count();
        if (connected_relays > 0) {
            self_skill_refresh_attempts++;
            self_skill_refresh_rc =
                nostr_handler_refresh_self_skill_cache_from_relays(self_skill_sync_attempt_timeout_ms);

            if (self_skill_refresh_rc == 0) {
                self_skill_validate_rc =
                    nostr_handler_validate_self_skill_cache(&startup_skill_count, &startup_adoption_count);
                if (self_skill_validate_rc == 0) {
                    DEBUG_INFO("[didactyl] startup phase: self-skill sync+validate succeeded (attempt=%d connected=%d/%d skills=%d adoptions=%d)",
                               self_skill_refresh_attempts,
                               connected_relays,
                               cfg.relay_count,
                               startup_skill_count,
                               startup_adoption_count);
                    break;
                }

                DEBUG_WARN("[didactyl] startup phase: self-skill sync returned no skill events yet (attempt=%d connected=%d/%d)",
                           self_skill_refresh_attempts,
                           connected_relays,
                           cfg.relay_count);
            } else {
                DEBUG_WARN("[didactyl] startup phase: self-skill sync refresh failed (attempt=%d connected=%d/%d)",
                           self_skill_refresh_attempts,
                           connected_relays,
                           cfg.relay_count);
            }
        } else {
            DEBUG_INFO("[didactyl] startup phase: waiting for connected relay before self-skill sync refresh");
        }

        if (self_skill_waited_ms >= self_skill_sync_total_budget_ms) {
            break;
        }

        int step_ms = self_skill_retry_step_ms;
        if (self_skill_waited_ms + step_ms > self_skill_sync_total_budget_ms) {
            step_ms = self_skill_sync_total_budget_ms - self_skill_waited_ms;
        }
        if (step_ms <= 0) {
            break;
        }

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

    if (self_skill_validate_rc != 0) {
        const char* fail_detail = (self_skill_refresh_rc == 0)
                                      ? "no skill events found after sync query retries"
                                      : "self-skill sync query failed within timeout";
        startup_step_fail(14,
                          "Subscribe self-skill cache",
                          fail_detail);
        notify_admin_startup_failure(&cfg,
                                     14,
                                     "Subscribe self-skill cache",
                                     fail_detail);
        if (self_skill_refresh_rc == 0) {
            fprintf(stderr,
                    "Startup aborted: no skill events (kind 31123/31124) found in self-skill cache after sync query retries\n");
            fprintf(stderr,
                    "Attempts=%d connected=%d/%d\n",
                    self_skill_refresh_attempts,
                    nostr_handler_connected_relay_count(),
                    cfg.relay_count);
            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");
        } else {
            fprintf(stderr,
                    "Startup aborted: failed to sync self-skill cache from connected relays within %d ms\n",
                    self_skill_sync_total_budget_ms);
            fprintf(stderr,
                    "Attempts=%d connected=%d/%d\n",
                    self_skill_refresh_attempts,
                    nostr_handler_connected_relay_count(),
                    cfg.relay_count);
            fprintf(stderr,
                    "Check relay connectivity and ensure required self-skill events exist on at least one connected relay\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");
        notify_admin_startup_failure(&cfg, 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");
    cashu_wallet_set_signer(g_signer);
    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_failure_marker_clear(&cfg);

    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);

    (void)systemd_notify_send("READY=1");
    time_t last_watchdog_ping = 0;

    while (g_running) {
        (void)nostr_handler_poll(100);
        (void)trigger_manager_poll(&trigger_manager);
        if (http_api_started) {
            (void)http_api_poll(0);
        }

        time_t now = time(NULL);
        if (now != (time_t)-1 && (last_watchdog_ping == 0 || (now - last_watchdog_ping) >= 10)) {
            (void)systemd_notify_send("WATCHDOG=1");
            last_watchdog_ping = now;
        }

        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_signer_free(g_signer);
    nostr_cleanup();

    return 0;
}
