#define _POSIX_C_SOURCE 200809L

#include "setup_wizard.h"

#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <limits.h>
#include <pwd.h>
#include <sys/wait.h>

#include "cjson/cJSON.h"
#include "llm.h"
#include "main.h"
#include "nostr_handler.h"
#include "default_events.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"

#define ANSI_UNDERLINE "\x1b[4m"
#define ANSI_RESET "\x1b[0m"
#define ANSI_YELLOW "\x1b[33m"
#define ANSI_RED "\x1b[31m"

#define WIZARD_LINE_MAX 1024

typedef struct {
    char key;
    const char* label;
} wizard_option_t;

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

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 (is_hex64(input)) {
        snprintf(out_hex, 65, "%s", input);
        return 0;
    }

    return -1;
}

static int derive_keys_from_nsec_local(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 line_is_quit(const char* s) {
    if (!s) return 0;
    while (*s && isspace((unsigned char)*s)) s++;
    char c = (char)tolower((unsigned char)*s);
    return c == 'q' || c == 'x';
}

static void trim_line(char* s) {
    if (!s) return;
    size_t n = strlen(s);
    while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r' || isspace((unsigned char)s[n - 1]))) {
        s[n - 1] = '\0';
        n--;
    }
    char* p = s;
    while (*p && isspace((unsigned char)*p)) p++;
    if (p != s) {
        memmove(s, p, strlen(p) + 1U);
    }
}

static int read_line_prompt(const char* prompt, char* out, size_t out_size) {
    if (!out || out_size == 0) return -1;
    out[0] = '\0';
    if (prompt && prompt[0] != '\0') {
        fprintf(stderr, "%s", prompt);
        fflush(stderr);
    }
    if (!fgets(out, (int)out_size, stdin)) {
        return -1;
    }
    trim_line(out);
    return 0;
}

static int read_secret_prompt(const char* prompt, char* out, size_t out_size) {
    if (!out || out_size == 0) return -1;
    out[0] = '\0';

    struct termios oldt;
    struct termios newt;
    int disable_echo = 0;

    if (tcgetattr(STDIN_FILENO, &oldt) == 0) {
        newt = oldt;
        newt.c_lflag &= (tcflag_t)~ECHO;
        if (tcsetattr(STDIN_FILENO, TCSANOW, &newt) == 0) {
            disable_echo = 1;
        }
    }

    if (prompt && prompt[0] != '\0') {
        fprintf(stderr, "%s", prompt);
        fflush(stderr);
    }

    if (!fgets(out, (int)out_size, stdin)) {
        if (disable_echo) {
            tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
            fprintf(stderr, "\n");
            fflush(stderr);
        }
        return -1;
    }

    if (disable_echo) {
        tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
        fprintf(stderr, "\n");
        fflush(stderr);
    }

    trim_line(out);
    return 0;
}

static char read_menu_choice(const wizard_option_t* options, int count) {
    char line[WIZARD_LINE_MAX];
    if (read_line_prompt("> ", line, sizeof(line)) != 0) {
        return 'q';
    }
    if (line_is_quit(line)) {
        return 'q';
    }
    char c = '\0';
    for (size_t i = 0; line[i] != '\0'; i++) {
        if (!isspace((unsigned char)line[i])) {
            c = (char)tolower((unsigned char)line[i]);
            break;
        }
    }
    if (c == '\0') {
        return '\0';
    }
    for (int i = 0; i < count; i++) {
        if ((char)tolower((unsigned char)options[i].key) == c) {
            return c;
        }
    }
    return '\0';
}

static void print_option(char key, const char* tail) {
    fprintf(stderr, "  %s%c%s%s\n", ANSI_UNDERLINE, (int)toupper((unsigned char)key), ANSI_RESET, tail ? tail : "");
}

static int get_terminal_width(void) {
    struct winsize w;
    if (ioctl(STDERR_FILENO, TIOCGWINSZ, &w) == 0) {
        return w.ws_col;
    }
    return 80; // Fallback
}

static void print_centered(const char* s, int width) {
    int len = (int)strlen(s);
    if (len >= width) {
        fprintf(stderr, "%s\n", s);
        return;
    }
    int padding = (width - len) / 2;
    for (int i = 0; i < padding; i++) fputc(' ', stderr);
    fprintf(stderr, "%s\n", s);
}

static void print_full_width_line(char c, int width) {
    for (int i = 0; i < width; i++) fputc(c, stderr);
    fputc('\n', stderr);
}

static void print_interactive_splash(void) {
    int width = get_terminal_width();
    fprintf(stderr, "\033[H\033[J\n\n\n\n");

    const char* art[] = {
        "$$$$$$$\\  $$\\       $$\\                       $$\\               $$\\ ",
        "$$  __$$\\ \\__|      $$ |                      $$ |              $$ |",
        "$$ |  $$ |$$\\  $$$$$$$ | $$$$$$\\   $$$$$$$\\ $$$$$$\\   $$\\   $$\\ $$ |",
        "$$ |  $$ |$$ |$$  __$$ | \\____$$\\ $$  _____|\\_$$  _|  $$ |  $$ |$$ |",
        "$$ |  $$ |$$ |$$ /  $$ | $$$$$$$ |$$ /        $$ |    $$ |  $$ |$$ |",
        "$$ |  $$ |$$ |$$ |  $$ |$$  __$$ |$$ |        $$ |$$\\ $$ |  $$ |$$ |",
        "$$$$$$$  |$$ |\\$$$$$$$ |\\$$$$$$$ |\\$$$$$$$\\   \\$$$$  |\\$$$$$$$ |$$ |",
        "\\_______/ \\__| \\_______| \\_______| \\_______|   \\____/  \\____$$ |\\__|",
        "                                                      $$\\   $$ |    ",
        "                                                      \\$$$$$$  |    ",
        "                                                       \\______/     "
    };

    for (size_t i = 0; i < sizeof(art) / sizeof(art[0]); i++) {
        print_centered(art[i], width);
    }
    fprintf(stderr, "\n");
}

static void render_wizard_page_header(const char* step_title, const char* section_title) {
    int width = get_terminal_width();
    char title[256];
    snprintf(title, sizeof(title), "Didactyl -- %s -- Interactive Setup", DIDACTYL_VERSION);

    print_interactive_splash();
    print_full_width_line('=', width);
    print_full_width_line('=', width);
    print_centered(title, width);
    print_centered("Not your keys, not your agent", width);
    if (section_title && section_title[0] != '\0') {
        print_centered(section_title, width);
    }
    if (step_title && step_title[0] != '\0') {
        print_centered(step_title, width);
    }
    print_full_width_line('=', width);
    print_full_width_line('=', width);
    fprintf(stderr, "\n");
}

static void config_set_defaults(didactyl_config_t* cfg) {
    if (!cfg) return;
    memset(cfg, 0, sizeof(*cfg));

    cfg->dm_protocol = DM_PROTOCOL_NIP04;

    cfg->tools.enabled = 1;
    cfg->tools.max_turns = 20;
    cfg->tools.trigger_max_turns = 12;
    cfg->tools.api_default_max_turns = 8;
    cfg->tools.api_max_turns_ceiling = 32;
    cfg->tools.stall_repeat_threshold = 3;
    cfg->tools.local_http_fetch_default_timeout_seconds = 20;
    cfg->tools.local_http_fetch_max_timeout_seconds = 120;
    cfg->tools.blossom_max_upload_bytes = 16 * 1024 * 1024;
    cfg->tools.blossom_max_download_bytes = 16 * 1024 * 1024;
    cfg->tools.shell.enabled = 1;
    cfg->tools.shell.timeout_seconds = 30;
    cfg->tools.shell.max_output_bytes = 65536;
    snprintf(cfg->tools.shell.working_directory, sizeof(cfg->tools.shell.working_directory), "%s", ".");

    cfg->security.verify_signatures = 1;
    cfg->security.admin.enabled = 1;
    cfg->security.admin.tools_enabled = 1;
    cfg->security.wot.enabled = 1;
    cfg->security.wot.tools_enabled = 0;
    cfg->security.stranger.enabled = 1;
    cfg->security.stranger.tools_enabled = 0;

    cfg->admin_context.enabled = 1;
    cfg->admin_context.track_kind_0 = 1;
    cfg->admin_context.track_kind_3 = 1;
    cfg->admin_context.track_kind_10002 = 1;
    cfg->admin_context.track_kind_1 = 1;
    cfg->admin_context.kind_1_limit = 10;

    cfg->triggers.enabled = 1;
    cfg->triggers.max_active = 16;
    cfg->triggers.cooldown_seconds = 60;
    cfg->triggers.llm_rate_limit_per_minute = 10;
    cfg->triggers.template_rate_limit_per_minute = 60;

    cfg->api.enabled = 1;
    cfg->api.port = 8484;
    snprintf(cfg->api.bind_address, sizeof(cfg->api.bind_address), "%s", "127.0.0.1");

    snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai");
    snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", "gpt-4o-mini");
    snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", "https://api.openai.com/v1");
    cfg->llm.max_tokens = 512;
    cfg->llm.temperature = 0.7;
}

static void free_relays_only(didactyl_config_t* cfg) {
    if (!cfg || !cfg->relays) return;
    for (int i = 0; i < cfg->relay_count; i++) {
        free(cfg->relays[i]);
    }
    free(cfg->relays);
    cfg->relays = NULL;
    cfg->relay_count = 0;
}

static int set_default_relays(didactyl_config_t* cfg) {
    if (!cfg) return -1;
    free_relays_only(cfg);
    cfg->relay_count = DIDACTYL_DEFAULT_RELAY_COUNT;
    cfg->relays = (char**)calloc((size_t)cfg->relay_count, sizeof(char*));
    if (!cfg->relays) {
        cfg->relay_count = 0;
        return -1;
    }
    for (int i = 0; i < cfg->relay_count; i++) {
        cfg->relays[i] = strdup(DIDACTYL_DEFAULT_RELAYS[i]);
        if (!cfg->relays[i]) {
            free_relays_only(cfg);
            return -1;
        }
    }
    return 0;
}

static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_tags_json) {
    if (!cfg || !out_tags_json || cfg->relay_count <= 0 || !cfg->relays) return -1;
    *out_tags_json = NULL;

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

    for (int i = 0; i < cfg->relay_count; i++) {
        if (!cfg->relays[i] || cfg->relays[i][0] == '\0') continue;
        cJSON* tag = cJSON_CreateArray();
        if (!tag) {
            cJSON_Delete(tags);
            return -1;
        }
        cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
        cJSON_AddItemToArray(tag, cJSON_CreateString(cfg->relays[i]));
        cJSON_AddItemToArray(tags, tag);
    }

    char* json = cJSON_PrintUnformatted(tags);
    cJSON_Delete(tags);
    if (!json) return -1;
    *out_tags_json = json;
    return 0;
}

static int tags_json_get_d_tag_local(const char* tags_json, char* out, size_t out_size) {
    if (!tags_json || !out || out_size == 0) {
        return -1;
    }

    out[0] = '\0';
    cJSON* tags = cJSON_Parse(tags_json);
    if (!tags || !cJSON_IsArray(tags)) {
        cJSON_Delete(tags);
        return -1;
    }

    int n = cJSON_GetArraySize(tags);
    for (int i = 0; i < n; i++) {
        cJSON* tag = cJSON_GetArrayItem(tags, i);
        if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
        cJSON* k = cJSON_GetArrayItem(tag, 0);
        cJSON* v = cJSON_GetArrayItem(tag, 1);
        if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue;
        if (strcmp(k->valuestring, "d") == 0 && v->valuestring[0] != '\0') {
            snprintf(out, out_size, "%s", v->valuestring);
            cJSON_Delete(tags);
            return 0;
        }
    }

    cJSON_Delete(tags);
    return -1;
}

static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) {
    if (!cfg || !content || !tags_json) {
        return -1;
    }

    int is_skill_kind = (kind == 31123 || kind == 31124);
    char incoming_d[128] = {0};
    int incoming_has_d = (is_skill_kind && tags_json_get_d_tag_local(tags_json, incoming_d, sizeof(incoming_d)) == 0);

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

        if (is_skill_kind) {
            if (!incoming_has_d) {
                continue;
            }
            char existing_d[128] = {0};
            if (tags_json_get_d_tag_local(se->tags_json, existing_d, sizeof(existing_d)) != 0) {
                continue;
            }
            if (strcmp(existing_d, incoming_d) != 0) {
                continue;
            }
        }

        char* content_dup = strdup(content);
        char* tags_dup = strdup(tags_json);
        if (!content_dup || !tags_dup) {
            free(content_dup);
            free(tags_dup);
            return -1;
        }

        free(se->content);
        free(se->tags_json);
        se->content = content_dup;
        se->tags_json = tags_dup;
        return 0;
    }

    startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events,
                                                        (size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t));
    if (!grown) {
        return -1;
    }
    cfg->startup_events = grown;

    startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
    memset(se, 0, sizeof(*se));
    se->kind = kind;
    se->content = strdup(content);
    se->tags_json = strdup(tags_json);
    if (!se->content || !se->tags_json) {
        free(se->content);
        free(se->tags_json);
        se->content = NULL;
        se->tags_json = NULL;
        return -1;
    }

    cfg->startup_event_count++;
    return 0;
}

static int apply_default_startup_events(didactyl_config_t* cfg, const char* agent_name) {
    if (!cfg || !agent_name || agent_name[0] == '\0' || cfg->admin.pubkey[0] == '\0') {
        return -1;
    }

    for (int i = 0; i < DIDACTYL_DEFAULT_STARTUP_EVENT_KIND_COUNT; i++) {
        int kind = DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[i];

        if (kind == DIDACTYL_STARTUP_KIND_PROFILE) {
            int needed = snprintf(NULL,
                                  0,
                                  DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE,
                                  agent_name,
                                  agent_name);
            if (needed <= 0) {
                return -1;
            }
            char* content_json = (char*)malloc((size_t)needed + 1U);
            if (!content_json) {
                return -1;
            }
            snprintf(content_json,
                     (size_t)needed + 1U,
                     DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE,
                     agent_name,
                     agent_name);

            int rc = upsert_startup_event(cfg, kind, content_json, "[]");
            free(content_json);
            if (rc != 0) {
                return -1;
            }
            continue;
        }

        if (kind == DIDACTYL_STARTUP_KIND_CONTACTS) {
            int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey);
            if (needed <= 0) {
                return -1;
            }
            char* tags_json = (char*)malloc((size_t)needed + 1U);
            if (!tags_json) {
                return -1;
            }
            snprintf(tags_json, (size_t)needed + 1U, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey);

            int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND3_CONTENT, tags_json);
            free(tags_json);
            if (rc != 0) {
                return -1;
            }
            continue;
        }

        if (kind == DIDACTYL_STARTUP_KIND_RELAYS) {
            char* tags_json = NULL;
            if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
                free(tags_json);
                return -1;
            }

            int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND10002_CONTENT, tags_json);
            free(tags_json);
            if (rc != 0) {
                return -1;
            }
            continue;
        }
    }

    return 0;
}

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

    if (upsert_startup_event(cfg,
                             DIDACTYL_DEFAULT_SKILL_KIND,
                             DIDACTYL_DEFAULT_SKILL_TEMPLATE,
                             DIDACTYL_DEFAULT_SKILL_TAGS_JSON) != 0) {
        return -1;
    }

    if (upsert_startup_event(cfg,
                             DIDACTYL_DEFAULT_DM_HISTORY_SKILL_KIND,
                             DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TEMPLATE,
                             DIDACTYL_DEFAULT_DM_HISTORY_SKILL_TAGS_JSON) != 0) {
        return -1;
    }

    return 0;
}

static int relay_contains(const didactyl_config_t* cfg, const char* relay) {
    if (!cfg || !relay || relay[0] == '\0') return 0;
    for (int i = 0; i < cfg->relay_count; i++) {
        if (cfg->relays[i] && strcmp(cfg->relays[i], relay) == 0) return 1;
    }
    return 0;
}

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

    char** grown = (char**)realloc(cfg->relays, (size_t)(cfg->relay_count + 1) * sizeof(char*));
    if (!grown) return -1;
    cfg->relays = grown;
    cfg->relays[cfg->relay_count] = strdup(relay);
    if (!cfg->relays[cfg->relay_count]) return -1;
    cfg->relay_count++;
    return 0;
}

static int relay_remove_index(didactyl_config_t* cfg, int idx) {
    if (!cfg || idx < 0 || idx >= cfg->relay_count || cfg->relay_count <= 1) return -1;
    free(cfg->relays[idx]);
    for (int i = idx; i < cfg->relay_count - 1; i++) {
        cfg->relays[i] = cfg->relays[i + 1];
    }
    cfg->relay_count--;
    if (cfg->relay_count == 0) {
        free(cfg->relays);
        cfg->relays = NULL;
        return 0;
    }
    char** shrunk = (char**)realloc(cfg->relays, (size_t)cfg->relay_count * sizeof(char*));
    if (shrunk) cfg->relays = shrunk;
    return 0;
}

static int wait_connected_relays_ms(int timeout_ms) {
    const int step = 100;
    int elapsed = 0;
    while (elapsed < timeout_ms) {
        if (nostr_handler_connected_relay_count() > 0) return 1;
        (void)nostr_handler_poll(step);
        elapsed += step;
    }
    return nostr_handler_connected_relay_count() > 0 ? 1 : 0;
}

static int query_self_kind10002_exists(const didactyl_config_t* cfg, int* out_exists) {
    if (!cfg || !out_exists || cfg->keys.public_key_hex[0] == '\0') return -1;
    *out_exists = 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", 1);

    char* events_json = nostr_handler_query_json(filter, 3000);
    cJSON_Delete(filter);

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

    return 0;
}

static int query_admin_kind0_name(const didactyl_config_t* cfg,
                                  char* out_name,
                                  size_t out_name_size,
                                  int* out_found) {
    if (!cfg || !out_name || out_name_size == 0 || !out_found || cfg->admin.pubkey[0] == '\0') {
        return -1;
    }
    out_name[0] = '\0';
    *out_found = 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(0));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->admin.pubkey));
    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;
    }

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

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

    cJSON* profile = cJSON_Parse(content->valuestring);
    if (!profile || !cJSON_IsObject(profile)) {
        cJSON_Delete(profile);
        cJSON_Delete(arr);
        return 0;
    }

    cJSON* display_name = cJSON_GetObjectItemCaseSensitive(profile, "display_name");
    cJSON* name = cJSON_GetObjectItemCaseSensitive(profile, "name");
    cJSON* chosen = NULL;

    if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
        chosen = display_name;
    } else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
        chosen = name;
    }

    if (chosen && chosen->valuestring) {
        snprintf(out_name, out_name_size, "%s", chosen->valuestring);
        *out_found = 1;
    }

    cJSON_Delete(profile);
    cJSON_Delete(arr);
    return 0;
}

static int validate_new_agent_identity_and_admin(const didactyl_config_t* cfg,
                                                 int* out_exists,
                                                 char* out_admin_name,
                                                 size_t out_admin_name_size,
                                                 int* out_admin_name_found) {
    if (!cfg || !out_exists || !out_admin_name || out_admin_name_size == 0 || !out_admin_name_found) {
        return -1;
    }

    *out_exists = 0;
    *out_admin_name_found = 0;
    out_admin_name[0] = '\0';

    didactyl_config_t tmp = *cfg;
    if (nostr_handler_init(&tmp) != 0) {
        return -1;
    }

    (void)wait_connected_relays_ms(4000);

    int rc_exists = query_self_kind10002_exists(cfg, out_exists);
    int rc_admin = query_admin_kind0_name(cfg, out_admin_name, out_admin_name_size, out_admin_name_found);

    nostr_handler_cleanup();
    return (rc_exists == 0 && rc_admin == 0) ? 0 : -1;
}

static int query_and_extract_kind10002_relays(didactyl_config_t* cfg) {
    if (!cfg || cfg->keys.public_key_hex[0] == '\0') return -1;

    didactyl_config_t tmp = *cfg;
    if (nostr_handler_init(&tmp) != 0) {
        return -1;
    }

    (void)wait_connected_relays_ms(5000);

    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);
        nostr_handler_cleanup();
        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", 8);

    char* events_json = nostr_handler_query_json(filter, 4000);
    cJSON_Delete(filter);

    if (!events_json) {
        nostr_handler_cleanup();
        return -1;
    }

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

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

    didactyl_config_t new_relays = {0};
    int n = cJSON_GetArraySize(tags);
    for (int i = 0; i < n; i++) {
        cJSON* tag = cJSON_GetArrayItem(tags, i);
        if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
        cJSON* key = cJSON_GetArrayItem(tag, 0);
        cJSON* val = cJSON_GetArrayItem(tag, 1);
        if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) continue;
        if (strcmp(key->valuestring, "r") == 0 && val->valuestring[0] != '\0') {
            (void)relay_add(&new_relays, val->valuestring);
        }
    }

    if (new_relays.relay_count > 0) {
        free_relays_only(cfg);
        cfg->relays = new_relays.relays;
        cfg->relay_count = new_relays.relay_count;
    } else {
        free_relays_only(&new_relays);
    }

    cJSON_Delete(arr);
    nostr_handler_cleanup();
    return 0;
}


static int recover_existing_config_from_nostr(didactyl_config_t* cfg, int* out_relay_found) {
    if (!cfg) return -1;
    if (out_relay_found) *out_relay_found = 0;

    if (query_and_extract_kind10002_relays(cfg) == 0) {
        if (out_relay_found) *out_relay_found = cfg->relay_count > 0 ? 1 : 0;
        return 0;
    }

    return -1;
}

static int query_self_kind0_name(const didactyl_config_t* cfg,
                                 char* out_name,
                                 size_t out_name_size,
                                 int* out_found) {
    if (!cfg || !out_name || out_name_size == 0 || !out_found || cfg->keys.public_key_hex[0] == '\0') {
        return -1;
    }

    out_name[0] = '\0';
    *out_found = 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(0));
    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;
    }

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

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

    cJSON* profile = cJSON_Parse(content->valuestring);
    if (!profile || !cJSON_IsObject(profile)) {
        cJSON_Delete(profile);
        cJSON_Delete(arr);
        return 0;
    }

    cJSON* display_name = cJSON_GetObjectItemCaseSensitive(profile, "display_name");
    cJSON* name = cJSON_GetObjectItemCaseSensitive(profile, "name");
    cJSON* chosen = NULL;

    if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
        chosen = display_name;
    } else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
        chosen = name;
    }

    if (chosen && chosen->valuestring) {
        snprintf(out_name, out_name_size, "%s", chosen->valuestring);
        *out_found = 1;
    }

    cJSON_Delete(profile);
    cJSON_Delete(arr);
    return 0;
}

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

    *out_plaintext = NULL;

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

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

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

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

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

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

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

    *out_plaintext = plaintext;
    return 0;
}

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

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

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

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

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

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

    cJSON_Delete(root);
    return 0;
}

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

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

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

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

    if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') {
        if (strcmp(dm_protocol->valuestring, "nip17") == 0) {
            cfg->dm_protocol = DM_PROTOCOL_NIP17;
        } else if (strcmp(dm_protocol->valuestring, "both") == 0) {
            cfg->dm_protocol = DM_PROTOCOL_BOTH;
        } else {
            cfg->dm_protocol = DM_PROTOCOL_NIP04;
        }
    }

    cJSON_Delete(root);
    return 0;
}

static int recover_full_config_from_nostr(didactyl_config_t* cfg,
                                          char* out_agent_name,
                                          size_t out_agent_name_size,
                                          int* out_agent_name_found) {
    if (!cfg || !out_agent_name || out_agent_name_size == 0 || !out_agent_name_found) {
        return -1;
    }

    out_agent_name[0] = '\0';
    *out_agent_name_found = 0;

    didactyl_config_t tmp = *cfg;
    if (nostr_handler_init(&tmp) != 0) {
        return -1;
    }

    (void)wait_connected_relays_ms(5000);

    (void)query_self_kind0_name(cfg, out_agent_name, out_agent_name_size, out_agent_name_found);

    char* llm_plaintext = NULL;
    if (fetch_and_decrypt_self_config_wizard(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
        (void)apply_recalled_llm_config_wizard(cfg, llm_plaintext);
    }
    free(llm_plaintext);

    char* agent_plaintext = NULL;
    if (fetch_and_decrypt_self_config_wizard(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
        (void)apply_recalled_agent_config_wizard(cfg, agent_plaintext);
    }
    free(agent_plaintext);

    nostr_handler_cleanup();
    return 0;
}

static const char* dm_protocol_to_string_local(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 int publish_encrypted_self_config_wizard(const didactyl_config_t* cfg,
                                                const char* d_tag,
                                                const char* content_json) {
    if (!cfg || !d_tag || !content_json || d_tag[0] == '\0') return -1;

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

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

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

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

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

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

static int persist_runtime_config_to_nostr_wizard(const didactyl_config_t* cfg) {
    if (!cfg) return -1;

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

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

    int llm_rc = publish_encrypted_self_config_wizard(cfg, "llm_config", llm_json);
    free(llm_json);

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

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

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

static int persist_runtime_config_to_nostr_wizard_online(const didactyl_config_t* cfg) {
    if (!cfg) return -1;

    didactyl_config_t tmp = *cfg;
    if (nostr_handler_init(&tmp) != 0) {
        return -1;
    }

    (void)wait_connected_relays_ms(5000);
    int rc = persist_runtime_config_to_nostr_wizard(cfg);

    /* Give relays time to acknowledge the published events before tearing
       down the connection.  Without this, the fire-and-forget publish may
       not reach any relay before cleanup closes the sockets. */
    for (int i = 0; i < 30; i++) {
        nostr_handler_poll(100);
    }

    nostr_handler_cleanup();
    return rc;
}

static int publish_kind10002_relays_wizard_online(const didactyl_config_t* cfg) {
    if (!cfg) return -1;

    didactyl_config_t tmp = *cfg;
    if (nostr_handler_init(&tmp) != 0) {
        return -1;
    }

    (void)wait_connected_relays_ms(5000);

    char* tags_json = NULL;
    if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
        free(tags_json);
        nostr_handler_cleanup();
        return -1;
    }

    cJSON* tags = cJSON_Parse(tags_json);
    free(tags_json);
    if (!tags || !cJSON_IsArray(tags)) {
        cJSON_Delete(tags);
        nostr_handler_cleanup();
        return -1;
    }

    nostr_publish_result_t result;
    memset(&result, 0, sizeof(result));
    int rc = nostr_handler_publish_kind_event(10002, "", tags, &result);
    nostr_handler_publish_result_free(&result);
    cJSON_Delete(tags);

    nostr_handler_cleanup();
    return rc;
}

static int ensure_startup_kind10002_from_current_relays(didactyl_config_t* cfg) {
    if (!cfg) return -1;

    char* tags_json = NULL;
    if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
        free(tags_json);
        return -1;
    }

    int rc = upsert_startup_event(cfg, 10002, "", tags_json);
    free(tags_json);
    return rc;
}

static int wizard_llm_test(const llm_config_t* llm_cfg) {
    if (!llm_cfg) return -1;
    if (llm_init(llm_cfg) != 0) {
        return -1;
    }
    char* response = llm_chat("You are a setup validator.", "Reply with exactly: OK");
    llm_cleanup();
    if (!response) {
        return -1;
    }
    free(response);
    return 0;
}

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

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

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

static void append_model_id_local(cJSON* ids, cJSON* item) {
    if (!ids || !item) return;
    if (cJSON_IsString(item) && item->valuestring) {
        cJSON_AddItemToArray(ids, cJSON_CreateString(item->valuestring));
        return;
    }
    if (cJSON_IsObject(item)) {
        cJSON* id = cJSON_GetObjectItemCaseSensitive(item, "id");
        if (id && cJSON_IsString(id) && id->valuestring) {
            cJSON_AddItemToArray(ids, cJSON_CreateString(id->valuestring));
        }
    }
}

static int fetch_models_for_llm_config(const llm_config_t* llm_cfg, cJSON** out_models) {
    if (!llm_cfg || !out_models) return -1;
    *out_models = NULL;

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

    char* raw = llm_list_models_json(NULL);
    llm_cleanup();
    if (!raw) {
        return -1;
    }

    cJSON* root = cJSON_Parse(raw);
    free(raw);
    if (!root) {
        cJSON_Delete(root);
        return -1;
    }

    cJSON* ids = cJSON_CreateArray();
    if (!ids) {
        cJSON_Delete(root);
        return -1;
    }

    if (cJSON_IsArray(root)) {
        int n = cJSON_GetArraySize(root);
        for (int i = 0; i < n; i++) {
            append_model_id_local(ids, cJSON_GetArrayItem(root, i));
        }
    } else if (cJSON_IsObject(root)) {
        cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data");
        if (data && cJSON_IsArray(data)) {
            int n = cJSON_GetArraySize(data);
            for (int i = 0; i < n; i++) {
                append_model_id_local(ids, cJSON_GetArrayItem(data, i));
            }
        }
    }

    cJSON_Delete(root);
    *out_models = ids;
    return 0;
}

static int extract_balance_value(const cJSON* root, double* out_balance) {
    if (!root || !out_balance || !cJSON_IsObject(root)) return -1;

    const char* keys[] = {
        "balance", "credits", "credit", "remaining", "remaining_credits", "available_balance"
    };

    for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
        cJSON* v = cJSON_GetObjectItemCaseSensitive((cJSON*)root, keys[i]);
        if (v && cJSON_IsNumber(v)) {
            *out_balance = v->valuedouble;
            return 0;
        }
    }

    cJSON* data = cJSON_GetObjectItemCaseSensitive((cJSON*)root, "data");
    if (data && cJSON_IsObject(data)) {
        for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
            cJSON* v = cJSON_GetObjectItemCaseSensitive(data, keys[i]);
            if (v && cJSON_IsNumber(v)) {
                *out_balance = v->valuedouble;
                return 0;
            }
        }
    }

    return -1;
}

static void wizard_check_provider_balance(const llm_config_t* llm_cfg) {
    if (!llm_cfg || llm_cfg->base_url[0] == '\0') return;

    const char* paths_ppq[] = {"/credits", "/credit", "/balance", "/v1/credits"};
    const char* paths_routstr[] = {"/balance", "/credits", "/v1/balance"};
    const char** paths = NULL;
    size_t path_count = 0;

    if (strcmp(llm_cfg->provider, "ppq") == 0) {
        paths = paths_ppq;
        path_count = sizeof(paths_ppq) / sizeof(paths_ppq[0]);
    } else if (strcmp(llm_cfg->provider, "routstr") == 0) {
        paths = paths_routstr;
        path_count = sizeof(paths_routstr) / sizeof(paths_routstr[0]);
    } else {
        fprintf(stderr, "  Balance check: skipped for provider '%s'\n", llm_cfg->provider);
        return;
    }

    if (llm_init(llm_cfg) != 0) {
        fprintf(stderr, "  Balance check: unavailable (LLM init failed)\n");
        return;
    }

    for (size_t i = 0; i < path_count; i++) {
        char* raw = llm_get_json_path(NULL, paths[i]);
        if (!raw) continue;

        cJSON* root = cJSON_Parse(raw);
        free(raw);
        if (!root) continue;

        double balance = 0.0;
        if (extract_balance_value(root, &balance) == 0) {
            fprintf(stderr, "  Balance check: available balance %.4f\n", balance);
            cJSON_Delete(root);
            llm_cleanup();
            return;
        }

        cJSON_Delete(root);
    }

    llm_cleanup();
    fprintf(stderr, "  Balance check: endpoint unavailable or unrecognized response (continuing)\n");
}

static int prompt_model_selection(const cJSON* models,
                                  const char* default_model,
                                  char* out_model,
                                  size_t out_model_size) {
    if (!out_model || out_model_size == 0 || !default_model || default_model[0] == '\0') return -1;

    char filter[WIZARD_LINE_MAX] = {0};
    for (;;) {
        int total = (models && cJSON_IsArray(models)) ? cJSON_GetArraySize((cJSON*)models) : 0;
        int matched_count = 0;
        int matched_idx[256] = {0};

        for (int i = 0; i < total && matched_count < (int)(sizeof(matched_idx) / sizeof(matched_idx[0])); i++) {
            cJSON* it = cJSON_GetArrayItem((cJSON*)models, i);
            if (!it || !cJSON_IsString(it) || !it->valuestring) continue;
            if (strcasestr_like(it->valuestring, filter)) {
                matched_idx[matched_count++] = i;
            }
        }

        fprintf(stderr, "\n  Model Search\n");
        fprintf(stderr, "  Filter [%s]  Matches: %d/%d\n", filter[0] ? filter : "(none)", matched_count, total);
        int show = matched_count < 20 ? matched_count : 20;
        for (int i = 0; i < show; i++) {
            cJSON* it = cJSON_GetArrayItem((cJSON*)models, matched_idx[i]);
            fprintf(stderr, "    %d. %s\n", i + 1, it->valuestring);
        }
        if (matched_count > show) {
            fprintf(stderr, "    ... (%d more matches)\n", matched_count - show);
        }

        fprintf(stderr,
                "\n  Enter a number to choose, '/text' to set filter, exact model id for manual entry,\n"
                "  or press Enter for default [%s]: ",
                default_model);

        char input[WIZARD_LINE_MAX] = {0};
        if (read_line_prompt(NULL, input, sizeof(input)) != 0) return -1;
        if (line_is_quit(input)) return -1;

        if (input[0] == '\0') {
            snprintf(out_model, out_model_size, "%s", default_model);
            return 0;
        }

        if (input[0] == '/') {
            snprintf(filter, sizeof(filter), "%s", input + 1);
            continue;
        }

        int n = atoi(input);
        if (n > 0 && n <= show) {
            cJSON* it = cJSON_GetArrayItem((cJSON*)models, matched_idx[n - 1]);
            if (it && cJSON_IsString(it) && it->valuestring) {
                snprintf(out_model, out_model_size, "%s", it->valuestring);
                return 0;
            }
        }

        snprintf(out_model, out_model_size, "%s", input);
        return 0;
    }
}

static int prompt_admin_pubkey_with_header(didactyl_config_t* cfg,
                                           const char* step_title,
                                           const char* section_title) {
    char input[WIZARD_LINE_MAX];
    for (;;) {
        render_wizard_page_header(step_title, section_title);
        if (read_line_prompt("  Enter the admin's Nostr public key (npub1... or hex):\n> ", input, sizeof(input)) != 0) {
            return -1;
        }
        if (line_is_quit(input)) return -1;

        char hex[65] = {0};
        if (decode_pubkey_hex_or_npub_local(input, hex) != 0) {
            fprintf(stderr, "%sInvalid admin pubkey. Use npub1... or 64-char hex.%s\n", ANSI_RED, ANSI_RESET);
            continue;
        }
        snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", hex);
        return 0;
    }
}

static int prompt_admin_pubkey(didactyl_config_t* cfg) {
    return prompt_admin_pubkey_with_header(cfg, "Step 3 of 7", "New Agent Setup -- Administrator");
}

static int prompt_llm_config_with_header(didactyl_config_t* cfg,
                                         const char* step_title,
                                         const char* section_title) {
    for (;;) {
        render_wizard_page_header(step_title, section_title);
        print_option('1', " ppq.ai");
        print_option('2', " Routstr");
        print_option('3', " OpenAI-compatible");
        print_option('q', "uit");
        wizard_option_t provider_opts[] = {{'1', ""}, {'2', ""}, {'3', ""}, {'q', ""}};
        char provider_choice = read_menu_choice(provider_opts, 4);
        if (provider_choice == 'q' || provider_choice == '\0') return -1;

        char provider[32] = {0};
        char base_url[OW_MAX_URL_LEN] = {0};
        if (provider_choice == '1') {
            snprintf(provider, sizeof(provider), "%s", "ppq");
            snprintf(base_url, sizeof(base_url), "%s", "https://api.ppq.ai");
        } else if (provider_choice == '2') {
            snprintf(provider, sizeof(provider), "%s", "routstr");
            snprintf(base_url, sizeof(base_url), "%s", "https://api.routstr.com");
        } else {
            snprintf(provider, sizeof(provider), "%s", "openai");
            const char* default_base_url = cfg->llm.base_url[0] ? cfg->llm.base_url : "https://api.openai.com/v1";
            char prompt[256];
            snprintf(prompt, sizeof(prompt), "  Base URL [%.200s]: ", default_base_url);
            if (read_line_prompt(prompt, base_url, sizeof(base_url)) != 0) return -1;
            if (line_is_quit(base_url)) return -1;
            if (base_url[0] == '\0') {
                snprintf(base_url, sizeof(base_url), "%s", default_base_url);
            }
        }

        char api_key[OW_MAX_KEY_LEN] = {0};
        if (read_secret_prompt("  API Key: ", api_key, sizeof(api_key)) != 0) return -1;
        if (line_is_quit(api_key)) return -1;

        llm_config_t probe = cfg->llm;
        snprintf(probe.provider, sizeof(probe.provider), "%s", provider);
        snprintf(probe.base_url, sizeof(probe.base_url), "%s", base_url);
        snprintf(probe.api_key, sizeof(probe.api_key), "%s", api_key);
        if (probe.model[0] == '\0') {
            snprintf(probe.model, sizeof(probe.model), "%s", "claude-haiku-4.5");
        }

        wizard_check_provider_balance(&probe);

        cJSON* models = NULL;
        if (fetch_models_for_llm_config(&probe, &models) != 0 || !models || !cJSON_IsArray(models)) {
            fprintf(stderr, "%sModel discovery failed (GET /models).%s\n", ANSI_RED, ANSI_RESET);
            print_option('r', "e-enter provider settings");
            print_option('m', "anually enter model and continue");
            print_option('q', "uit");
            wizard_option_t fail_opts[] = {{'r', ""}, {'m', ""}, {'q', ""}};
            char c = read_menu_choice(fail_opts, 3);
            if (c == 'q' || c == '\0') {
                cJSON_Delete(models);
                return -1;
            }
            if (c == 'r') {
                cJSON_Delete(models);
                continue;
            }

            char model[OW_MAX_MODEL_LEN] = {0};
            if (read_line_prompt("  Model [claude-haiku-4.5]: ", model, sizeof(model)) != 0) {
                cJSON_Delete(models);
                return -1;
            }
            if (line_is_quit(model)) {
                cJSON_Delete(models);
                return -1;
            }
            if (model[0] == '\0') {
                snprintf(model, sizeof(model), "%s", "claude-haiku-4.5");
            }

            snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider);
            snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url);
            snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key);
            snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model);
            cfg->llm.max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512;
            cfg->llm.temperature = cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7;
            cJSON_Delete(models);
            return 0;
        }

        char model[OW_MAX_MODEL_LEN] = {0};
        if (prompt_model_selection(models, "claude-haiku-4.5", model, sizeof(model)) != 0) {
            cJSON_Delete(models);
            return -1;
        }
        cJSON_Delete(models);

        int max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512;
        double temperature = cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7;

        snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider);
        snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url);
        snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key);
        snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model);
        cfg->llm.max_tokens = max_tokens;
        cfg->llm.temperature = temperature;

        if (wizard_llm_test(&cfg->llm) == 0) {
            fprintf(stderr, "%sLLM test: OK%s\n", ANSI_YELLOW, ANSI_RESET);
            return 0;
        }

        fprintf(stderr, "%sLLM test: FAILED%s\n\n", ANSI_RED, ANSI_RESET);
        wizard_option_t opts[] = {
            {'r', "e-enter LLM settings"},
            {'s', "kip test and continue anyway"},
            {'q', "uit"}
        };
        print_option('r', "e-enter LLM settings");
        print_option('s', "kip test and continue anyway");
        print_option('q', "uit");

        char c = read_menu_choice(opts, 3);
        if (c == 's') return 0;
        if (c == 'q' || c == '\0') return -1;
    }
}

static int prompt_llm_config(didactyl_config_t* cfg) {
    return prompt_llm_config_with_header(cfg, "Step 5 of 7", "New Agent Setup -- LLM Provider");
}

static int prompt_relay_configuration_with_header(didactyl_config_t* cfg,
                                                  const char* step_title,
                                                  const char* section_title) {
    char input[WIZARD_LINE_MAX];

    for (;;) {
        render_wizard_page_header(step_title, section_title);
        fprintf(stderr, "  Current relays:\n");
        for (int i = 0; i < cfg->relay_count; i++) {
            fprintf(stderr, "    %d. %s\n", i + 1, cfg->relays[i] ? cfg->relays[i] : "");
        }
        fprintf(stderr, "\n");
        print_option('a', "dd a relay");
        print_option('r', "emove a relay (by number)");
        print_option('d', "one -- use this list");
        print_option('b', "ack");
        print_option('q', "uit");

        wizard_option_t opts[] = {
            {'a', "dd"}, {'r', "emove"}, {'d', "one"}, {'b', "ack"}, {'q', "uit"}
        };
        char c = read_menu_choice(opts, 5);
        if (c == 'q') return -1;
        if (c == 'b') return 1;
        if (c == 'd') {
            if (cfg->relay_count <= 0) {
                fprintf(stderr, "%sAt least one relay is required.%s\n", ANSI_RED, ANSI_RESET);
                continue;
            }
            return 0;
        }
        if (c == 'a') {
            if (read_line_prompt("Relay URL (wss:// or ws://): ", input, sizeof(input)) != 0) return -1;
            if (line_is_quit(input)) return -1;
            if (!(strncmp(input, "wss://", 6) == 0 || strncmp(input, "ws://", 5) == 0)) {
                fprintf(stderr, "%sRelay must start with wss:// or ws://%s\n", ANSI_RED, ANSI_RESET);
                continue;
            }
            if (relay_add(cfg, input) != 0) {
                fprintf(stderr, "%sFailed to add relay.%s\n", ANSI_RED, ANSI_RESET);
            }
            continue;
        }
        if (c == 'r') {
            if (cfg->relay_count <= 1) {
                fprintf(stderr, "%sCannot remove last relay.%s\n", ANSI_RED, ANSI_RESET);
                continue;
            }
            if (read_line_prompt("Remove which number? ", input, sizeof(input)) != 0) return -1;
            if (line_is_quit(input)) return -1;
            int idx = atoi(input) - 1;
            if (relay_remove_index(cfg, idx) != 0) {
                fprintf(stderr, "%sInvalid relay number.%s\n", ANSI_RED, ANSI_RESET);
            }
        }
    }
}

static int prompt_relay_configuration(didactyl_config_t* cfg) {
    return prompt_relay_configuration_with_header(cfg, "Step 6 of 7", "New Agent Setup -- Relay Configuration");
}

static int run_command_local(char* const argv[]) {
    pid_t pid = fork();
    if (pid < 0) return -1;
    if (pid == 0) {
        execvp(argv[0], argv);
        _exit(127);
    }

    int status = 0;
    if (waitpid(pid, &status, 0) < 0) return -1;
    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return -1;
    return 0;
}

static int run_privileged_command_local(char* const argv[]) {
    if (!argv || !argv[0]) return -1;

    if (geteuid() == 0) {
        return run_command_local(argv);
    }

    char* sudo_argv[64] = {0};
    sudo_argv[0] = "sudo";

    int i = 0;
    for (; argv[i] && i < 62; i++) {
        sudo_argv[i + 1] = argv[i];
    }

    if (argv[i] != NULL) {
        return -1;
    }

    sudo_argv[i + 1] = NULL;
    return run_command_local(sudo_argv);
}

static int write_file_privileged_local(const char* path, const char* content, unsigned int mode) {
    if (!path || path[0] == '\0' || !content) return -1;

    char tmp_template[] = "/tmp/didactyl-wizard-XXXXXX";
    int fd = mkstemp(tmp_template);
    if (fd < 0) {
        return -1;
    }

    FILE* fp = fdopen(fd, "w");
    if (!fp) {
        close(fd);
        unlink(tmp_template);
        return -1;
    }

    size_t len = strlen(content);
    int ok = (fwrite(content, 1, len, fp) == len) ? 1 : 0;
    if (fclose(fp) != 0) {
        ok = 0;
    }

    if (!ok) {
        unlink(tmp_template);
        return -1;
    }

    char mode_buf[16] = {0};
    snprintf(mode_buf, sizeof(mode_buf), "%04o", mode & 07777U);

    char* install_argv[] = {"install", "-m", mode_buf, tmp_template, (char*)path, NULL};
    int rc = run_privileged_command_local(install_argv);

    unlink(tmp_template);
    return rc;
}

static void sanitize_service_user_from_name(const char* name, char* out, size_t out_size) {
    if (!out || out_size == 0) return;
    out[0] = '\0';

    const char* src = (name && name[0] != '\0') ? name : "didactyl";
    size_t j = 0;
    for (size_t i = 0; src[i] != '\0' && j + 1 < out_size; i++) {
        char c = (char)tolower((unsigned char)src[i]);
        if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
            out[j++] = c;
        } else if ((c == ' ' || c == '_' || c == '-') && j > 0 && out[j - 1] != '-') {
            out[j++] = '-';
        }
        if (j >= 31) break;
    }

    while (j > 0 && out[j - 1] == '-') j--;
    out[j] = '\0';

    if (out[0] == '\0' || !(out[0] >= 'a' && out[0] <= 'z')) {
        snprintf(out, out_size, "didactyl");
    }
}


static const char* detect_ca_bundle_path_local(void) {
    const char* env = getenv("SSL_CERT_FILE");
    if (env && env[0] != '\0' && access(env, R_OK) == 0) {
        return env;
    }
    static const char* candidates[] = {
        "/etc/ssl/certs/ca-certificates.crt",
        "/etc/ssl/cert.pem",
        "/etc/pki/tls/certs/ca-bundle.crt",
        "/etc/ssl/ca-bundle.pem"
    };
    for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
        if (access(candidates[i], R_OK) == 0) {
            return candidates[i];
        }
    }
    return NULL;
}

static int write_sudoers_file(const char* service_user) {
    if (!service_user || service_user[0] == '\0') return -1;

    char sudoers_path[PATH_MAX] = {0};
    snprintf(sudoers_path, sizeof(sudoers_path), "/etc/sudoers.d/%s", service_user);

    char content[4096] = {0};
    int n = snprintf(content,
                     sizeof(content),
                     "# Didactyl agent — scoped system maintenance privileges\n"
                     "# Generated by didactyl setup wizard\n"
                     "\n"
                     "# Service management\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl start *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-enabled *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl list-units *\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload\n"
                     "\n"
                     "# Log inspection\n"
                     "%s ALL=(ALL) NOPASSWD: /usr/bin/journalctl *\n",
                     service_user,
                     service_user,
                     service_user,
                     service_user,
                     service_user,
                     service_user,
                     service_user,
                     service_user,
                     service_user,
                     service_user,
                     service_user);
    if (n < 0 || (size_t)n >= sizeof(content)) {
        fprintf(stderr, "%sFailed to build sudoers content for %s.%s\n", ANSI_RED, service_user, ANSI_RESET);
        return -1;
    }

    if (write_file_privileged_local(sudoers_path, content, 0440U) != 0) {
        fprintf(stderr, "%sFailed to write sudoers file at %s.%s\n", ANSI_RED, sudoers_path, ANSI_RESET);
        return -1;
    }

    return 0;
}

static int install_system_service_with_dedicated_user(const didactyl_config_t* cfg, const char* agent_name) {
    if (!cfg || cfg->keys.nsec[0] == '\0') return -1;

    if (geteuid() != 0) {
        fprintf(stderr, "  Requesting sudo authentication for system install...\n");
        char* sudo_validate_argv[] = {"sudo", "-v", NULL};
        if (run_command_local(sudo_validate_argv) != 0) {
            fprintf(stderr, "%sSudo authentication failed. System install canceled.%s\n", ANSI_RED, ANSI_RESET);
            return -1;
        }
    }

    /* --- Derive service username from agent name --- */
    char service_user[64] = {0};
    sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));

    /* --- Create dedicated user if it doesn't exist --- */
    struct passwd* pw = getpwnam(service_user);
    if (!pw) {
        fprintf(stderr, "  Creating system user '%s'...\n", service_user);
        char comment[128] = {0};
        snprintf(comment, sizeof(comment), "Didactyl Nostr Agent (%s)", agent_name);
        char* useradd_argv[] = {"useradd", "-m", "-s", "/bin/bash", "-c", comment, service_user, NULL};
        if (run_privileged_command_local(useradd_argv) != 0) {
            fprintf(stderr, "%sFailed to create system user '%s'.%s\n", ANSI_RED, service_user, ANSI_RESET);
            return -1;
        }
        pw = getpwnam(service_user);
        if (!pw) {
            fprintf(stderr, "%sCreated user but failed to resolve passwd entry.%s\n", ANSI_RED, ANSI_RESET);
            return -1;
        }
    } else {
        fprintf(stderr, "  User '%s' already exists, reusing.\n", service_user);
    }

    const char* home = (pw->pw_dir && pw->pw_dir[0] != '\0') ? pw->pw_dir : NULL;
    if (!home) {
        fprintf(stderr, "%sUnable to determine service user home directory.%s\n", ANSI_RED, ANSI_RESET);
        return -1;
    }

    /* --- Copy binary --- */
    char self_path[PATH_MAX] = {0};
    ssize_t self_len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1);
    if (self_len <= 0) {
        fprintf(stderr, "%sUnable to resolve running binary path.%s\n", ANSI_RED, ANSI_RESET);
        return -1;
    }
    self_path[self_len] = '\0';

    char bin_path[PATH_MAX] = {0};
    snprintf(bin_path, sizeof(bin_path), "%s/didactyl", home);
    fprintf(stderr, "  Copying binary to %s...\n", bin_path);
    char* install_bin_argv[] = {"install", "-m", "0755", self_path, bin_path, NULL};
    if (run_privileged_command_local(install_bin_argv) != 0) {
        fprintf(stderr, "%sFailed to copy binary to %s.%s\n", ANSI_RED, bin_path, ANSI_RESET);
        return -1;
    }

    /* --- Set ownership on home directory and installed binary --- */
    char owner_spec[64] = {0};
    snprintf(owner_spec, sizeof(owner_spec), "%lu:%lu", (unsigned long)pw->pw_uid, (unsigned long)pw->pw_gid);

    char* chown_home_argv[] = {"chown", owner_spec, (char*)home, NULL};
    char* chmod_home_argv[] = {"chmod", "0750", (char*)home, NULL};
    char* chown_bin_argv[] = {"chown", owner_spec, bin_path, NULL};

    if (run_privileged_command_local(chown_home_argv) != 0 ||
        run_privileged_command_local(chmod_home_argv) != 0 ||
        run_privileged_command_local(chown_bin_argv) != 0) {
        fprintf(stderr, "%sFailed to set ownership/permissions for installed files.%s\n", ANSI_RED, ANSI_RESET);
        return -1;
    }

    /* --- Write sudoers file for scoped sudo privileges --- */
    fprintf(stderr, "  Writing sudoers file for '%s'...\n", service_user);
    if (write_sudoers_file(service_user) != 0) {
        fprintf(stderr, "%sWarning: Failed to write sudoers file. Agent will not have sudo access.%s\n",
                ANSI_YELLOW, ANSI_RESET);
        /* Non-fatal — agent can still run without sudo */
    }

    /* --- Detect CA bundle for TLS --- */
    const char* ca_bundle = detect_ca_bundle_path_local();

    /* --- Write systemd unit file --- */
    char service_name[96] = {0};
    snprintf(service_name, sizeof(service_name), "%s.service", service_user);

    char service_path[PATH_MAX] = {0};
    snprintf(service_path, sizeof(service_path), "/etc/systemd/system/%s", service_name);
    fprintf(stderr, "  Writing systemd unit to %s...\n", service_path);

    char env_line[PATH_MAX + 64] = {0};
    if (ca_bundle) {
        snprintf(env_line, sizeof(env_line), "Environment=SSL_CERT_FILE=%s\\n", ca_bundle);
    }

    char service_content[8192] = {0};
    int svc_len = snprintf(service_content,
                           sizeof(service_content),
                           "[Unit]\n"
                           "Description=Didactyl Nostr Agent (%s)\n"
                           "After=network-online.target\n"
                           "Wants=network-online.target\n\n"
                           "[Service]\n"
                           "Type=simple\n"
                           "User=%s\n"
                           "Group=%s\n"
                           "WorkingDirectory=%s\n"
                           "ExecStart=%s --nsec %s --debug 3\n"
                           "%s"
                           "SyslogIdentifier=%s\n"
                           "NoNewPrivileges=false\n"
                           "ProtectSystem=strict\n"
                           "ReadWritePaths=%s\n"
                           "ProtectHome=false\n"
                           "PrivateTmp=yes\n"
                           "Restart=on-failure\n"
                           "RestartSec=10\n"
                           "StandardOutput=journal\n"
                           "StandardError=journal\n\n"
                           "[Install]\n"
                           "WantedBy=multi-user.target\n",
                           service_user,
                           service_user,
                           service_user,
                           home,
                           bin_path,
                           cfg->keys.nsec,
                           env_line,
                           service_user,
                           home);
    if (svc_len < 0 || (size_t)svc_len >= sizeof(service_content)) {
        fprintf(stderr, "%sFailed to build service file content.%s\n", ANSI_RED, ANSI_RESET);
        return -1;
    }

    if (write_file_privileged_local(service_path, service_content, 0644U) != 0) {
        fprintf(stderr, "%sFailed to write service file at %s.%s\n", ANSI_RED, service_path, ANSI_RESET);
        return -1;
    }

    /* --- Enable and start the service --- */
    fprintf(stderr, "  Enabling and starting %s...\n", service_name);
    char* reload_argv[] = {"systemctl", "daemon-reload", NULL};
    char* enable_argv[] = {"systemctl", "enable", service_name, NULL};
    char* start_argv[] = {"systemctl", "start", service_name, NULL};

    if (run_privileged_command_local(reload_argv) != 0 ||
        run_privileged_command_local(enable_argv) != 0 ||
        run_privileged_command_local(start_argv) != 0) {
        fprintf(stderr, "%sFailed to enable/start systemd service %s.%s\n", ANSI_RED, service_name, ANSI_RESET);
        fprintf(stderr, "  You can manually start with:\n");
        fprintf(stderr, "    sudo systemctl daemon-reload\n");
        fprintf(stderr, "    sudo systemctl enable %s\n", service_name);
        fprintf(stderr, "    sudo systemctl start %s\n", service_name);
        return -1;
    }

    fprintf(stderr, "\n%sSystem service installed and started.%s\n", ANSI_YELLOW, ANSI_RESET);
    fprintf(stderr, "  Service:  %s\n", service_name);
    fprintf(stderr, "  User:     %s\n", service_user);
    fprintf(stderr, "  Home:     %s\n", home);
    fprintf(stderr, "  Binary:   %s\n", bin_path);
    fprintf(stderr, "  Nsec:     embedded directly in ExecStart\n");
    fprintf(stderr, "  Sudoers:  /etc/sudoers.d/%s\n", service_user);
    fprintf(stderr, "  Unit:     %s\n", service_path);
    fprintf(stderr, "\n  Useful commands:\n");
    fprintf(stderr, "    sudo systemctl status %s\n", service_name);
    fprintf(stderr, "    sudo journalctl -u %s -f\n", service_name);
    fprintf(stderr, "    sudo systemctl restart %s\n", service_name);
    return 0;
}

static __attribute__((unused)) int maybe_prompt_write_path(char* path_out, size_t path_out_size) {
    if (!path_out || path_out_size == 0) return -1;
    char input[WIZARD_LINE_MAX];
    if (read_line_prompt("Path [./genesis.jsonc]: ", input, sizeof(input)) != 0) return -1;
    if (line_is_quit(input)) return -1;
    if (input[0] == '\0') {
        snprintf(path_out, path_out_size, "%s", "./genesis.jsonc");
    } else {
        strncpy(path_out, input, path_out_size - 1U);
        path_out[path_out_size - 1U] = '\0';
    }

    if (access(path_out, F_OK) == 0) {
        fprintf(stderr, "%sFile exists: %s%s\n", ANSI_YELLOW, path_out, ANSI_RESET);
        print_option('o', "verwrite");
        print_option('c', "hoose different path");
        print_option('q', "uit");
        wizard_option_t opts[] = {{'o', ""}, {'c', ""}, {'q', ""}};
        char c = read_menu_choice(opts, 3);
        if (c == 'o') return 0;
        if (c == 'c') return maybe_prompt_write_path(path_out, path_out_size);
        return -1;
    }

    return 0;
}

static int new_agent_identity_step(didactyl_config_t* cfg) {
    for (;;) {
        render_wizard_page_header("Step 2 of 7", "New Agent Setup -- Identity");
        print_option('g', "enerate a new Nostr keypair");
        print_option('p', "rovide an existing nsec");
        print_option('b', "ack");
        print_option('q', "uit");
        wizard_option_t opts[] = {{'g', ""}, {'p', ""}, {'b', ""}, {'q', ""}};
        char c = read_menu_choice(opts, 4);
        if (c == 'q') return -1;
        if (c == 'b') return 1;

        if (c == 'g') {
            unsigned char priv[32];
            unsigned char pub[32];
            if (nostr_generate_keypair(priv, pub) != 0) {
                fprintf(stderr, "%sFailed to generate keypair.%s\n", ANSI_RED, ANSI_RESET);
                continue;
            }
            memcpy(cfg->keys.private_key, priv, 32);
            memcpy(cfg->keys.public_key, pub, 32);
            nostr_bytes_to_hex(cfg->keys.public_key, 32, cfg->keys.public_key_hex);

            char nsec[OW_MAX_KEY_LEN] = {0};
            char npub[OW_MAX_KEY_LEN] = {0};
            if (nostr_key_to_bech32(priv, "nsec", nsec) != 0 || nostr_key_to_bech32(pub, "npub", npub) != 0) {
                fprintf(stderr, "%sFailed to encode generated keypair.%s\n", ANSI_RED, ANSI_RESET);
                continue;
            }
            snprintf(cfg->keys.nsec, sizeof(cfg->keys.nsec), "%s", nsec);

            fprintf(stderr, "\nGenerated identity:\n");
            fprintf(stderr, "  npub: %s\n", npub);
            fprintf(stderr, "%s  nsec: %s%s\n", ANSI_YELLOW, nsec, ANSI_RESET);
            fprintf(stderr, "%sSave this nsec now. It will not be persisted unless you explicitly export with nsec.%s\n",
                    ANSI_YELLOW,
                    ANSI_RESET);
            char ok[WIZARD_LINE_MAX] = {0};
            if (read_line_prompt("Type 'ok' when saved (or q to quit): ", ok, sizeof(ok)) != 0) return -1;
            if (line_is_quit(ok)) return -1;
        }

        if (c == 'p') {
            char nsec_in[OW_MAX_KEY_LEN] = {0};
            if (read_secret_prompt("Enter nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1;
            if (line_is_quit(nsec_in)) return -1;
            if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) {
                fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET);
                continue;
            }
            char npub[OW_MAX_KEY_LEN] = {0};
            if (nostr_key_to_bech32(cfg->keys.public_key, "npub", npub) == 0) {
                fprintf(stderr, "Derived identity: %s\n", npub);
            } else {
                fprintf(stderr, "Derived pubkey (hex): %s\n", cfg->keys.public_key_hex);
            }
        }

        return 0;
    }
}

static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t genesis_path_out_size) {
    (void)genesis_path_out;
    (void)genesis_path_out_size;
    char agent_name[OW_MAX_NAME_LEN] = {0};
    render_wizard_page_header("Step 1 of 7", "New Agent Setup -- Agent Profile");
    if (read_line_prompt("  Agent name [Didactyl]: ", agent_name, sizeof(agent_name)) != 0) return -1;
    if (line_is_quit(agent_name)) return -1;
    if (agent_name[0] == '\0') {
        snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
    }

    int rc = new_agent_identity_step(cfg);
    if (rc != 0) return rc < 0 ? -1 : 1;

    if (prompt_admin_pubkey(cfg) != 0) return -1;

    int exists = 0;
    int admin_name_found = 0;
    char admin_name[128] = {0};
    if (validate_new_agent_identity_and_admin(cfg, &exists, admin_name, sizeof(admin_name), &admin_name_found) != 0) {
        fprintf(stderr, "%sWarning: Unable to validate identity/admin against Nostr right now.%s\n",
                ANSI_YELLOW,
                ANSI_RESET);
    } else {
        render_wizard_page_header("Step 4 of 7", "New Agent Setup -- Identity Validation");
        fprintf(stderr, "  Agent kind 10002: %s\n", exists ? "FOUND (may overwrite existing state)" : "Agent npub available for new setup");
        if (admin_name_found) {
            fprintf(stderr, "  Admin profile:   %s\n", admin_name);
        } else {
            fprintf(stderr, "  Admin profile:   NOT FOUND (you can still continue)\n");
        }
        fprintf(stderr, "\n");
        if (exists) {
            print_option('c', "ontinue anyway");
            print_option('a', "bort");
            wizard_option_t confirm[] = {{'c', ""}, {'a', ""}};
            char cc = read_menu_choice(confirm, 2);
            if (cc != 'c') {
                return -1;
            }
        }
    }

    if (prompt_llm_config(cfg) != 0) return -1;

    for (;;) {
        rc = prompt_relay_configuration(cfg);
        if (rc < 0) return -1;
        if (rc == 0) break;
        if (prompt_llm_config(cfg) != 0) return -1;
    }

    if (configure_default_skills_for_agent(cfg, agent_name) != 0) {
        fprintf(stderr, "%sFailed to prepare default startup skill events.%s\n", ANSI_RED, ANSI_RESET);
        return -1;
    }

    if (apply_default_startup_events(cfg, agent_name) != 0) {
        fprintf(stderr, "%sFailed to prepare startup events from defaults.%s\n", ANSI_RED, ANSI_RESET);
        return -1;
    }

    render_wizard_page_header("Step 7 of 7", "New Agent Setup -- Review");
    fprintf(stderr, "  Name:        %s\n", agent_name);
    fprintf(stderr, "  Identity:    %.16s...\n", cfg->keys.public_key_hex);
    fprintf(stderr, "  Admin:       %.16s...\n", cfg->admin.pubkey);
    fprintf(stderr, "  LLM:         %s @ %s\n", cfg->llm.model, cfg->llm.base_url);
    fprintf(stderr, "  Relays:      %d configured\n", cfg->relay_count);
    fprintf(stderr, "  DM Protocol: nip04\n\n");

    print_option('b', "oot the agent now");
    print_option('i', "nstall dedicated-user systemd service and boot");
    print_option('s', "tart over");
    print_option('q', "uit");

    wizard_option_t opts[] = {{'b', ""}, {'i', ""}, {'s', ""}, {'q', ""}};
    char c = read_menu_choice(opts, 4);
    if (c == 'q') return -1;
    if (c == 's') return 2;

    if (c == 'b') {
        /* Publish kind 30078 agent_config + llm_config so the --nsec-only
           restart path can recover them.  Non-fatal: main() will also
           publish during bootstrap, but doing it here is belt-and-suspenders. */
        if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) {
            fprintf(stderr, "%sWarning: failed to publish runtime config to Nostr (will retry on boot).%s\n",
                    ANSI_YELLOW, ANSI_RESET);
        }
        fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n",
                ANSI_YELLOW,
                ANSI_RESET);
        return 0;
    }

    if (c == 'i') {
        /* Publish kind 30078 agent_config + llm_config BEFORE installing the
           service.  The systemd service starts with --nsec only and depends
           on recovering these from relays. */
        if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) {
            fprintf(stderr,
                    "%sFailed to publish runtime config to Nostr; refusing install to avoid missing admin/LLM on first boot.%s\n",
                    ANSI_RED, ANSI_RESET);
            return -1;
        }

        if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) {
            fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
            return -1;
        }

        char service_user[64] = {0};
        sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));

        char service_name[96] = {0};
        snprintf(service_name, sizeof(service_name), "%s.service", service_user);

        sleep(2);
        char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL};
        if (run_privileged_command_local(is_active_argv) == 0) {
            fprintf(stderr,
                    "%sStep 7 of 7 -- Service %s is active. Setup is complete; this wizard will now exit. "
                    "Expect a startup message in the admin inbox.%s\n",
                    ANSI_YELLOW,
                    service_name,
                    ANSI_RESET);
        } else {
            fprintf(stderr,
                    "%sStep 7 of 7 -- Service installed but not active yet. This wizard will now exit. "
                    "Check status with: sudo systemctl status %s ; logs with: sudo journalctl -u %s -f%s\n",
                    ANSI_RED,
                    service_name,
                    service_name,
                    ANSI_RESET);
        }

        return 1;
    }

    return -1;
}

static int existing_agent_flow(didactyl_config_t* cfg) {
    char nsec_in[OW_MAX_KEY_LEN] = {0};
    char agent_name[OW_MAX_NAME_LEN] = {0};
    int agent_name_found = 0;
    int relay_changed = 0;

    render_wizard_page_header("Existing Agent", "Identity");
    if (read_secret_prompt("Enter your agent nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1;
    if (line_is_quit(nsec_in)) return -1;
    if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) {
        fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET);
        return -1;
    }

    int relay_found = 0;
    if (recover_existing_config_from_nostr(cfg, &relay_found) != 0 || !relay_found) {
        render_wizard_page_header("Existing Agent", "Config Recovery");
        fprintf(stderr, "  Relay list (kind 10002):  NOT FOUND\n\n");
        fprintf(stderr, "%sUnable to find a kind 10002 relay list for this identity.%s\n", ANSI_RED, ANSI_RESET);
        fprintf(stderr, "\n");
        print_option('n', "ew agent setup with this identity");
        print_option('q', "uit to main menu");
        wizard_option_t opts[] = {{'n', ""}, {'q', ""}};
        char c = read_menu_choice(opts, 2);
        if (c == 'n') {
            int rc = new_agent_flow(cfg, NULL, 0);
            if (rc == 0) return SETUP_WIZARD_RC_BOOTSTRAP;
            if (rc == 1) return SETUP_WIZARD_RC_EXIT;
            return -1;
        }
        return -1;
    }

    if (recover_full_config_from_nostr(cfg, agent_name, sizeof(agent_name), &agent_name_found) != 0) {
        fprintf(stderr, "%sWarning: unable to recover full config from Nostr, continuing with partial recovery.%s\n",
                ANSI_YELLOW,
                ANSI_RESET);
    }

    if (!agent_name_found || agent_name[0] == '\0') {
        snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
    }

    for (;;) {
        char masked_key[64] = {0};
        size_t api_len = strlen(cfg->llm.api_key);
        if (api_len >= 8U) {
            snprintf(masked_key,
                     sizeof(masked_key),
                     "%.4s...%s",
                     cfg->llm.api_key,
                     cfg->llm.api_key + api_len - 4U);
        } else if (api_len > 0U) {
            snprintf(masked_key, sizeof(masked_key), "****");
        } else {
            snprintf(masked_key, sizeof(masked_key), "(not set)");
        }

        render_wizard_page_header("Existing Agent", "Recovered Configuration");
        fprintf(stderr, "Your agent was found. Change any of the following:\n\n");
        fprintf(stderr, "  Name:         %s\n", agent_name);
        fprintf(stderr, "  Identity:     %.16s...\n", cfg->keys.public_key_hex);
        fprintf(stderr, "  Admin:        %s\n", cfg->admin.pubkey[0] ? cfg->admin.pubkey : "(not set)");
        fprintf(stderr, "  LLM Provider: %s\n", cfg->llm.provider[0] ? cfg->llm.provider : "(not set)");
        fprintf(stderr, "  LLM Model:    %s\n", cfg->llm.model[0] ? cfg->llm.model : "(not set)");
        fprintf(stderr, "  LLM Base URL: %s\n", cfg->llm.base_url[0] ? cfg->llm.base_url : "(not set)");
        fprintf(stderr, "  LLM API Key:  %s\n", masked_key);
        fprintf(stderr, "  Relays:       %d configured\n", cfg->relay_count);
        fprintf(stderr, "\n");

        print_option('a', "dmin pubkey");
        print_option('l', "lm config");
        print_option('r', "elay configuration");
        print_option('c', "ontinue to launch options");
        print_option('q', "uit");

        wizard_option_t opts[] = {{'a', ""}, {'l', ""}, {'r', ""}, {'c', ""}, {'q', ""}};
        char c = read_menu_choice(opts, 5);
        if (c == 'q') {
            return -1;
        }
        if (c == 'c') {
            break;
        }
        if (c == 'a') {
            if (prompt_admin_pubkey_with_header(cfg,
                                                "Existing Agent -- Review",
                                                "Existing Agent -- Administrator") != 0) {
                return -1;
            }
            continue;
        }
        if (c == 'l') {
            if (prompt_llm_config_with_header(cfg,
                                              "Existing Agent -- Review",
                                              "Existing Agent -- LLM Provider") != 0) {
                return -1;
            }
            continue;
        }
        if (c == 'r') {
            for (;;) {
                int rc = prompt_relay_configuration_with_header(cfg,
                                                                "Existing Agent -- Review",
                                                                "Existing Agent -- Relay Configuration");
                if (rc < 0) return -1;
                if (rc == 1) break;
                relay_changed = 1;
                break;
            }
            continue;
        }
    }

    render_wizard_page_header("Existing Agent", "Launch Options");
    fprintf(stderr, "  Name:        %s\n", agent_name);
    fprintf(stderr, "  Identity:    %.16s...\n", cfg->keys.public_key_hex);
    fprintf(stderr, "  Admin:       %.16s...\n", cfg->admin.pubkey);
    fprintf(stderr, "  LLM:         %s @ %s\n", cfg->llm.model, cfg->llm.base_url);
    fprintf(stderr, "  Relays:      %d configured\n\n", cfg->relay_count);

    print_option('b', "oot the agent now");
    print_option('i', "nstall dedicated-user systemd service and boot");
    print_option('q', "uit");

    wizard_option_t launch_opts[] = {{'b', ""}, {'i', ""}, {'q', ""}};
    char lc = read_menu_choice(launch_opts, 3);
    if (lc == 'q') return -1;

    if (lc == 'b') {
        if (relay_changed) {
            if (ensure_startup_kind10002_from_current_relays(cfg) != 0) {
                fprintf(stderr,
                        "%sFailed to stage updated relay list for startup publish.%s\n",
                        ANSI_RED,
                        ANSI_RESET);
                return -1;
            }
        }
        /* Always return BOOTSTRAP so main() re-publishes kind 30078.
           The wizard has the authoritative config in memory. */
        return SETUP_WIZARD_RC_BOOTSTRAP;
    }

    if (lc == 'i') {
        /* Always publish kind 30078 before installing the service, even if
           config was not changed.  The service depends on recovering these
           from relays and the event may have been purged. */
        if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) {
            fprintf(stderr,
                    "%sFailed to publish runtime config to Nostr; refusing install to avoid stale admin/LLM on first boot.%s\n",
                    ANSI_RED,
                    ANSI_RESET);
            return -1;
        }
        if (relay_changed) {
            if (publish_kind10002_relays_wizard_online(cfg) != 0) {
                fprintf(stderr,
                        "%sFailed to publish updated relay list (kind 10002) to Nostr; refusing install.%s\n",
                        ANSI_RED,
                        ANSI_RESET);
                return -1;
            }
        }

        if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) {
            fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
            return -1;
        }

        char service_user[64] = {0};
        sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));

        char service_name[96] = {0};
        snprintf(service_name, sizeof(service_name), "%s.service", service_user);

        sleep(2);
        char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL};
        if (run_privileged_command_local(is_active_argv) == 0) {
            fprintf(stderr,
                    "%sService %s is active. Setup is complete; this wizard will now exit.%s\n",
                    ANSI_YELLOW,
                    service_name,
                    ANSI_RESET);
        } else {
            fprintf(stderr,
                    "%sService installed but not active yet. Check status with: sudo systemctl status %s%s\n",
                    ANSI_RED,
                    service_name,
                    ANSI_RESET);
        }

        return SETUP_WIZARD_RC_EXIT;
    }

    return -1;
}

static int load_genesis_path_flow(didactyl_config_t* cfg) {
    char path[OW_MAX_URL_LEN] = {0};
    render_wizard_page_header("Load Genesis File", "Bootstrap from File");
    if (read_line_prompt("Path [./genesis.jsonc]: ", path, sizeof(path)) != 0) {
        return -1;
    }
    if (line_is_quit(path)) return -1;
    if (path[0] == '\0') {
        snprintf(path, sizeof(path), "%s", "./genesis.jsonc");
    }

    config_free(cfg);
    if (config_load(path, cfg) != 0) {
        fprintf(stderr, "%sFailed to load %s: %s%s\n", ANSI_RED, path, config_last_error(), ANSI_RESET);
        return -1;
    }

    if (cfg->keys.nsec[0] == '\0') {
        char nsec_in[OW_MAX_KEY_LEN] = {0};
        if (read_secret_prompt("Config has no nsec. Enter nsec now: ", nsec_in, sizeof(nsec_in)) != 0) return -1;
        if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) {
            fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET);
            return -1;
        }
    }

    return 0;
}

int setup_wizard_run(didactyl_config_t* config, char* genesis_path_out, size_t genesis_path_out_size) {
    if (!config) return -1;

    config_set_defaults(config);
    if (set_default_relays(config) != 0) {
        return -1;
    }

    for (;;) {
        render_wizard_page_header("Main Menu", "Choose Setup Mode");
        print_option('n', "ew agent -- create a fresh identity");
        print_option('e', "xisting -- start an already set up agent");
        print_option('l', "oad -- boot from genesis.jsonc");
        print_option('q', "uit");

        wizard_option_t opts[] = {{'n', ""}, {'e', ""}, {'l', ""}, {'q', ""}};
        char c = read_menu_choice(opts, 4);
        if (c == 'q' || c == '\0') {
            return -1;
        }

        if (c == 'l') {
            int rc = load_genesis_path_flow(config);
            if (rc == 0) {
                return SETUP_WIZARD_RC_BOOTSTRAP;
            }
            return rc;
        }

        if (c == 'e') {
            int rc = existing_agent_flow(config);
            if (rc == SETUP_WIZARD_RC_BOOTSTRAP ||
                rc == SETUP_WIZARD_RC_EXISTING ||
                rc == SETUP_WIZARD_RC_EXIT) {
                return rc;
            }
            return rc;
        }

        if (c == 'n') {
            int rc = new_agent_flow(config, genesis_path_out, genesis_path_out_size);
            if (rc == 2) {
                config_free(config);
                config_set_defaults(config);
                if (set_default_relays(config) != 0) {
                    return -1;
                }
                continue;
            }
            if (rc == 0) {
                return SETUP_WIZARD_RC_BOOTSTRAP;
            }
            if (rc == 1) {
                return SETUP_WIZARD_RC_EXIT;
            }
            return rc;
        }
    }
}
