#define _POSIX_C_SOURCE 200809L

#include "config.h"

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

#include "cjson/cJSON.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"

/* ---------------------------------------------------------------------------
 * jsonc_strip_comments  –  strip JSONC comments from a buffer
 *
 * Returns a newly malloc'd string containing valid JSON (caller must free).
 * The function is careful to skip over JSON string literals so that comment
 * characters inside strings are preserved.  Handles:
 *   - single-line comments   //  (to end of line)
 *   - block comments         (slash-star ... star-slash, may span lines)
 *   - escaped quotes inside strings  \"
 *   - trailing commas before ] or } are NOT removed (cJSON tolerates them)
 *
 * Returns NULL on allocation failure.
 * ------------------------------------------------------------------------ */
char* jsonc_strip_comments(const char* src, size_t src_len) {
    if (!src || src_len == 0) {
        char* empty = (char*)malloc(1);
        if (empty) empty[0] = '\0';
        return empty;
    }

    char* out = (char*)malloc(src_len + 1);
    if (!out) return NULL;

    size_t o = 0;
    size_t i = 0;

    while (i < src_len) {
        /* Inside a JSON string literal — copy verbatim until closing quote */
        if (src[i] == '"') {
            out[o++] = src[i++];                       /* opening quote */
            while (i < src_len) {
                if (src[i] == '\\' && i + 1 < src_len) {
                    out[o++] = src[i++];               /* backslash */
                    out[o++] = src[i++];               /* escaped char */
                } else if (src[i] == '"') {
                    out[o++] = src[i++];               /* closing quote */
                    break;
                } else {
                    out[o++] = src[i++];
                }
            }
            continue;
        }

        /* Single-line comment  //  */
        if (src[i] == '/' && i + 1 < src_len && src[i + 1] == '/') {
            /* skip to end of line */
            i += 2;
            while (i < src_len && src[i] != '\n') {
                i++;
            }
            /* preserve the newline so line numbers stay meaningful */
            if (i < src_len) {
                out[o++] = src[i++];                   /* the \n */
            }
            continue;
        }

        /* Block comment (slash-star ... star-slash) */
        if (src[i] == '/' && i + 1 < src_len && src[i + 1] == '*') {
            i += 2;
            while (i + 1 < src_len && !(src[i] == '*' && src[i + 1] == '/')) {
                /* preserve newlines inside block comments */
                if (src[i] == '\n') {
                    out[o++] = '\n';
                }
                i++;
            }
            if (i + 1 < src_len) {
                i += 2;                                /* skip closing */
            }
            continue;
        }

        /* Normal character — copy through */
        out[o++] = src[i++];
    }

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

static char g_config_last_error[512] = {0};

static void config_set_error(const char* fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(g_config_last_error, sizeof(g_config_last_error), fmt ? fmt : "unknown configuration error", ap);
    va_end(ap);
}

const char* config_last_error(void) {
    return g_config_last_error[0] != '\0' ? g_config_last_error : "unknown configuration error";
}

static int read_file_to_buffer(const char* path, char** out_buf, size_t* out_len) {
    FILE* fp = fopen(path, "rb");
    if (!fp) {
        return -1;
    }

    if (fseek(fp, 0, SEEK_END) != 0) {
        fclose(fp);
        return -1;
    }

    long len = ftell(fp);
    if (len < 0) {
        fclose(fp);
        return -1;
    }

    if (fseek(fp, 0, SEEK_SET) != 0) {
        fclose(fp);
        return -1;
    }

    char* buf = (char*)malloc((size_t)len + 1U);
    if (!buf) {
        fclose(fp);
        return -1;
    }

    size_t read_len = fread(buf, 1, (size_t)len, fp);
    fclose(fp);

    if (read_len != (size_t)len) {
        free(buf);
        return -1;
    }

    buf[len] = '\0';
    *out_buf = buf;
    *out_len = (size_t)len;
    return 0;
}

static int copy_json_string(cJSON* obj, const char* key, char* dst, size_t dst_size, int required) {
    cJSON* item = cJSON_GetObjectItemCaseSensitive(obj, key);
    if (!item || !cJSON_IsString(item) || !item->valuestring) {
        return required ? -1 : 0;
    }

    size_t n = strlen(item->valuestring);
    if (n >= dst_size) {
        return -1;
    }

    memcpy(dst, item->valuestring, n + 1U);
    return 0;
}

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_private_key(const char* nsec_or_hex, unsigned char out_private_key[32]) {
    if (!nsec_or_hex || !out_private_key) {
        return -1;
    }

    if (strncmp(nsec_or_hex, "nsec1", 5) == 0) {
        return nostr_decode_nsec(nsec_or_hex, out_private_key) == 0 ? 0 : -1;
    }

    if (is_hex64(nsec_or_hex)) {
        return nostr_hex_to_bytes(nsec_or_hex, out_private_key, 32) == 0 ? 0 : -1;
    }

    return -1;
}

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

    if (is_hex64(in)) {
        memcpy(out_hex, in, 65);
        return 0;
    }

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

    return -1;
}

static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
    cJSON* tools = cJSON_GetObjectItemCaseSensitive(root, "tools");
    if (!tools || !cJSON_IsObject(tools)) {
        return 0;
    }

    cJSON* enabled = cJSON_GetObjectItemCaseSensitive(tools, "enabled");
    cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(tools, "max_turns");
    cJSON* trigger_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "trigger_max_turns");
    cJSON* api_default_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "api_default_max_turns");
    cJSON* api_max_turns_ceiling = cJSON_GetObjectItemCaseSensitive(tools, "api_max_turns_ceiling");
    cJSON* stall_repeat_threshold = cJSON_GetObjectItemCaseSensitive(tools, "stall_repeat_threshold");
    cJSON* local_http_fetch_default_timeout_seconds =
        cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_default_timeout_seconds");
    cJSON* local_http_fetch_max_timeout_seconds =
        cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_max_timeout_seconds");
    cJSON* blossom_max_upload_bytes =
        cJSON_GetObjectItemCaseSensitive(tools, "blossom_max_upload_bytes");
    cJSON* blossom_max_download_bytes =
        cJSON_GetObjectItemCaseSensitive(tools, "blossom_max_download_bytes");
    if (enabled && cJSON_IsBool(enabled)) {
        config->tools.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
    }
    if (max_turns && cJSON_IsNumber(max_turns)) {
        config->tools.max_turns = (int)max_turns->valuedouble;
    }
    if (trigger_max_turns && cJSON_IsNumber(trigger_max_turns)) {
        config->tools.trigger_max_turns = (int)trigger_max_turns->valuedouble;
    }
    if (api_default_max_turns && cJSON_IsNumber(api_default_max_turns)) {
        config->tools.api_default_max_turns = (int)api_default_max_turns->valuedouble;
    }
    if (api_max_turns_ceiling && cJSON_IsNumber(api_max_turns_ceiling)) {
        config->tools.api_max_turns_ceiling = (int)api_max_turns_ceiling->valuedouble;
    }
    if (stall_repeat_threshold && cJSON_IsNumber(stall_repeat_threshold)) {
        config->tools.stall_repeat_threshold = (int)stall_repeat_threshold->valuedouble;
    }
    if (local_http_fetch_default_timeout_seconds && cJSON_IsNumber(local_http_fetch_default_timeout_seconds)) {
        config->tools.local_http_fetch_default_timeout_seconds =
            (int)local_http_fetch_default_timeout_seconds->valuedouble;
    }
    if (local_http_fetch_max_timeout_seconds && cJSON_IsNumber(local_http_fetch_max_timeout_seconds)) {
        config->tools.local_http_fetch_max_timeout_seconds =
            (int)local_http_fetch_max_timeout_seconds->valuedouble;
    }
    if (blossom_max_upload_bytes && cJSON_IsNumber(blossom_max_upload_bytes)) {
        config->tools.blossom_max_upload_bytes = (int)blossom_max_upload_bytes->valuedouble;
    }
    if (blossom_max_download_bytes && cJSON_IsNumber(blossom_max_download_bytes)) {
        config->tools.blossom_max_download_bytes = (int)blossom_max_download_bytes->valuedouble;
    }

    cJSON* shell = cJSON_GetObjectItemCaseSensitive(tools, "shell");
    if (!shell || !cJSON_IsObject(shell)) {
        return 0;
    }

    cJSON* shell_enabled = cJSON_GetObjectItemCaseSensitive(shell, "enabled");
    cJSON* timeout_seconds = cJSON_GetObjectItemCaseSensitive(shell, "timeout_seconds");
    cJSON* max_output_bytes = cJSON_GetObjectItemCaseSensitive(shell, "max_output_bytes");
    if (shell_enabled && cJSON_IsBool(shell_enabled)) {
        config->tools.shell.enabled = cJSON_IsTrue(shell_enabled) ? 1 : 0;
    }
    if (timeout_seconds && cJSON_IsNumber(timeout_seconds)) {
        config->tools.shell.timeout_seconds = (int)timeout_seconds->valuedouble;
    }
    if (max_output_bytes && cJSON_IsNumber(max_output_bytes)) {
        config->tools.shell.max_output_bytes = (int)max_output_bytes->valuedouble;
    }

    if (copy_json_string(shell,
                         "working_directory",
                         config->tools.shell.working_directory,
                         sizeof(config->tools.shell.working_directory),
                         0) != 0) {
        return -1;
    }

    if (config->tools.max_turns < 1) {
        config->tools.max_turns = 8;
    }
    if (config->tools.trigger_max_turns < 1) {
        config->tools.trigger_max_turns = config->tools.max_turns;
    }
    if (config->tools.api_default_max_turns < 1) {
        config->tools.api_default_max_turns = config->tools.max_turns;
    }
    if (config->tools.api_max_turns_ceiling < 1) {
        config->tools.api_max_turns_ceiling = 16;
    }
    if (config->tools.api_default_max_turns > config->tools.api_max_turns_ceiling) {
        config->tools.api_default_max_turns = config->tools.api_max_turns_ceiling;
    }
    if (config->tools.stall_repeat_threshold < 2) {
        config->tools.stall_repeat_threshold = 3;
    }
    if (config->tools.local_http_fetch_default_timeout_seconds < 1) {
        config->tools.local_http_fetch_default_timeout_seconds = 20;
    }
    if (config->tools.local_http_fetch_max_timeout_seconds < 1) {
        config->tools.local_http_fetch_max_timeout_seconds = 120;
    }
    if (config->tools.local_http_fetch_default_timeout_seconds > config->tools.local_http_fetch_max_timeout_seconds) {
        config->tools.local_http_fetch_default_timeout_seconds = config->tools.local_http_fetch_max_timeout_seconds;
    }
    if (config->tools.blossom_max_upload_bytes < 1024) {
        config->tools.blossom_max_upload_bytes = 16 * 1024 * 1024;
    }
    if (config->tools.blossom_max_download_bytes < 1024) {
        config->tools.blossom_max_download_bytes = 16 * 1024 * 1024;
    }

    return 0;
}

static int parse_security_config(cJSON* root, didactyl_config_t* config) {
    cJSON* security = cJSON_GetObjectItemCaseSensitive(root, "security");
    if (!security || !cJSON_IsObject(security)) {
        return 0;
    }

    cJSON* verify_signatures = cJSON_GetObjectItemCaseSensitive(security, "verify_signatures");
    if (verify_signatures && cJSON_IsBool(verify_signatures)) {
        config->security.verify_signatures = cJSON_IsTrue(verify_signatures) ? 1 : 0;
    }

    if (copy_json_string(security,
                         "stranger_response",
                         config->security.stranger_response,
                         sizeof(config->security.stranger_response),
                         0) != 0) {
        return -1;
    }

    cJSON* tiers = cJSON_GetObjectItemCaseSensitive(security, "tiers");
    if (!tiers || !cJSON_IsObject(tiers)) {
        return 0;
    }

    cJSON* admin_tier = cJSON_GetObjectItemCaseSensitive(tiers, "admin");
    if (admin_tier && cJSON_IsObject(admin_tier)) {
        cJSON* tools_enabled = cJSON_GetObjectItemCaseSensitive(admin_tier, "tools_enabled");
        if (tools_enabled && cJSON_IsBool(tools_enabled)) {
            config->security.admin.tools_enabled = cJSON_IsTrue(tools_enabled) ? 1 : 0;
        }
    }

    cJSON* wot_tier = cJSON_GetObjectItemCaseSensitive(tiers, "wot");
    if (wot_tier && cJSON_IsObject(wot_tier)) {
        cJSON* enabled = cJSON_GetObjectItemCaseSensitive(wot_tier, "enabled");
        cJSON* tools_enabled = cJSON_GetObjectItemCaseSensitive(wot_tier, "tools_enabled");
        if (enabled && cJSON_IsBool(enabled)) {
            config->security.wot.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
        }
        if (tools_enabled && cJSON_IsBool(tools_enabled)) {
            config->security.wot.tools_enabled = cJSON_IsTrue(tools_enabled) ? 1 : 0;
        }
    }

    cJSON* stranger_tier = cJSON_GetObjectItemCaseSensitive(tiers, "stranger");
    if (stranger_tier && cJSON_IsObject(stranger_tier)) {
        cJSON* enabled = cJSON_GetObjectItemCaseSensitive(stranger_tier, "enabled");
        if (enabled && cJSON_IsBool(enabled)) {
            config->security.stranger.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
        }
    }

    return 0;
}

static int parse_admin_context_config(cJSON* root, didactyl_config_t* config) {
    cJSON* admin_context = cJSON_GetObjectItemCaseSensitive(root, "admin_context");
    if (!admin_context || !cJSON_IsObject(admin_context)) {
        return 0;
    }

    cJSON* enabled = cJSON_GetObjectItemCaseSensitive(admin_context, "enabled");
    if (enabled && cJSON_IsBool(enabled)) {
        config->admin_context.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
    }

    cJSON* kind_1_limit = cJSON_GetObjectItemCaseSensitive(admin_context, "kind_1_limit");
    if (kind_1_limit && cJSON_IsNumber(kind_1_limit)) {
        config->admin_context.kind_1_limit = (int)kind_1_limit->valuedouble;
    }

    cJSON* subscribe_kinds = cJSON_GetObjectItemCaseSensitive(admin_context, "subscribe_kinds");
    if (subscribe_kinds && cJSON_IsArray(subscribe_kinds)) {
        config->admin_context.track_kind_0 = 0;
        config->admin_context.track_kind_3 = 0;
        config->admin_context.track_kind_10002 = 0;
        config->admin_context.track_kind_1 = 0;

        int n = cJSON_GetArraySize(subscribe_kinds);
        for (int i = 0; i < n; i++) {
            cJSON* k = cJSON_GetArrayItem(subscribe_kinds, i);
            if (!k || !cJSON_IsNumber(k)) {
                continue;
            }
            int kind = (int)k->valuedouble;
            if (kind == 0) config->admin_context.track_kind_0 = 1;
            else if (kind == 3) config->admin_context.track_kind_3 = 1;
            else if (kind == 10002) config->admin_context.track_kind_10002 = 1;
            else if (kind == 1) config->admin_context.track_kind_1 = 1;
        }
    }

    return 0;
}

static int parse_triggers_config(cJSON* root, didactyl_config_t* config) {
    cJSON* triggers = cJSON_GetObjectItemCaseSensitive(root, "triggers");
    if (!triggers || !cJSON_IsObject(triggers)) {
        return 0;
    }

    cJSON* enabled = cJSON_GetObjectItemCaseSensitive(triggers, "enabled");
    cJSON* max_active = cJSON_GetObjectItemCaseSensitive(triggers, "max_active");
    cJSON* cooldown_seconds = cJSON_GetObjectItemCaseSensitive(triggers, "cooldown_seconds");
    cJSON* llm_rate_limit = cJSON_GetObjectItemCaseSensitive(triggers, "llm_rate_limit_per_minute");
    cJSON* template_rate_limit = cJSON_GetObjectItemCaseSensitive(triggers, "template_rate_limit_per_minute");

    if (enabled && cJSON_IsBool(enabled)) {
        config->triggers.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
    }
    if (max_active && cJSON_IsNumber(max_active)) {
        config->triggers.max_active = (int)max_active->valuedouble;
    }
    if (cooldown_seconds && cJSON_IsNumber(cooldown_seconds)) {
        config->triggers.cooldown_seconds = (int)cooldown_seconds->valuedouble;
    }
    if (llm_rate_limit && cJSON_IsNumber(llm_rate_limit)) {
        config->triggers.llm_rate_limit_per_minute = (int)llm_rate_limit->valuedouble;
    }
    if (template_rate_limit && cJSON_IsNumber(template_rate_limit)) {
        config->triggers.template_rate_limit_per_minute = (int)template_rate_limit->valuedouble;
    }

    if (config->triggers.max_active < 1) {
        config->triggers.max_active = 1;
    }
    if (config->triggers.cooldown_seconds < 0) {
        config->triggers.cooldown_seconds = 0;
    }
    if (config->triggers.llm_rate_limit_per_minute < 1) {
        config->triggers.llm_rate_limit_per_minute = 1;
    }
    if (config->triggers.template_rate_limit_per_minute < 1) {
        config->triggers.template_rate_limit_per_minute = 1;
    }

    return 0;
}

static int parse_dm_protocol_config(cJSON* root, didactyl_config_t* config) {
    if (!root || !config) {
        return -1;
    }

    cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol");
    if (!dm_protocol) {
        return 0;
    }

    if (!cJSON_IsString(dm_protocol) || !dm_protocol->valuestring) {
        return -1;
    }

    if (strcmp(dm_protocol->valuestring, "nip04") == 0) {
        config->dm_protocol = DM_PROTOCOL_NIP04;
    } else if (strcmp(dm_protocol->valuestring, "nip17") == 0) {
        config->dm_protocol = DM_PROTOCOL_NIP17;
    } else if (strcmp(dm_protocol->valuestring, "both") == 0) {
        config->dm_protocol = DM_PROTOCOL_BOTH;
    } else {
        return -1;
    }

    return 0;
}

static int parse_api_config(cJSON* root, didactyl_config_t* config) {
    cJSON* api = cJSON_GetObjectItemCaseSensitive(root, "api");
    if (!api || !cJSON_IsObject(api)) {
        return 0;
    }

    cJSON* enabled = cJSON_GetObjectItemCaseSensitive(api, "enabled");
    cJSON* port = cJSON_GetObjectItemCaseSensitive(api, "port");

    if (enabled && cJSON_IsBool(enabled)) {
        config->api.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
    }
    if (port && cJSON_IsNumber(port)) {
        config->api.port = (int)port->valuedouble;
    }

    if (copy_json_string(api,
                         "bind_address",
                         config->api.bind_address,
                         sizeof(config->api.bind_address),
                         0) != 0) {
        return -1;
    }

    if (config->api.port < 1 || config->api.port > 65535) {
        config->api.port = 8484;
    }
    if (config->api.bind_address[0] == '\0') {
        snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1");
    }

    return 0;
}

static void free_cashu_wallet_mints(cashu_wallet_config_t* wallet) {
    if (!wallet || !wallet->mint_urls) {
        return;
    }

    for (int i = 0; i < wallet->mint_count; i++) {
        free(wallet->mint_urls[i]);
    }
    free(wallet->mint_urls);
    wallet->mint_urls = NULL;
    wallet->mint_count = 0;
}

static int parse_cashu_wallet_config(cJSON* root, didactyl_config_t* config) {
    cJSON* wallet = cJSON_GetObjectItemCaseSensitive(root, "cashu_wallet");
    if (!wallet || !cJSON_IsObject(wallet)) {
        return 0;
    }

    cJSON* enabled = cJSON_GetObjectItemCaseSensitive(wallet, "enabled");
    if (enabled && cJSON_IsBool(enabled)) {
        config->cashu_wallet.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
    }

    cJSON* auto_load = cJSON_GetObjectItemCaseSensitive(wallet, "auto_load");
    if (auto_load && cJSON_IsBool(auto_load)) {
        config->cashu_wallet.auto_load = cJSON_IsTrue(auto_load) ? 1 : 0;
    }

    cJSON* timeout = cJSON_GetObjectItemCaseSensitive(wallet, "mint_timeout_seconds");
    if (timeout && cJSON_IsNumber(timeout)) {
        config->cashu_wallet.mint_timeout_seconds = (int)timeout->valuedouble;
    }

    if (copy_json_string(wallet,
                         "unit",
                         config->cashu_wallet.unit,
                         sizeof(config->cashu_wallet.unit),
                         0) != 0) {
        return -1;
    }

    cJSON* mints = cJSON_GetObjectItemCaseSensitive(wallet, "mints");
    if (mints) {
        if (!cJSON_IsArray(mints)) {
            return -1;
        }

        int count = cJSON_GetArraySize(mints);
        char** urls = NULL;
        if (count > 0) {
            urls = (char**)calloc((size_t)count, sizeof(char*));
            if (!urls) {
                return -1;
            }
        }

        for (int i = 0; i < count; i++) {
            cJSON* item = cJSON_GetArrayItem(mints, i);
            if (!item || !cJSON_IsString(item) || !item->valuestring || item->valuestring[0] == '\0') {
                for (int j = 0; j < i; j++) {
                    free(urls[j]);
                }
                free(urls);
                return -1;
            }
            urls[i] = strdup(item->valuestring);
            if (!urls[i]) {
                for (int j = 0; j < i; j++) {
                    free(urls[j]);
                }
                free(urls);
                return -1;
            }
        }

        free_cashu_wallet_mints(&config->cashu_wallet);
        config->cashu_wallet.mint_urls = urls;
        config->cashu_wallet.mint_count = count;
    }

    if (config->cashu_wallet.unit[0] == '\0') {
        snprintf(config->cashu_wallet.unit, sizeof(config->cashu_wallet.unit), "%s", "sat");
    }
    if (config->cashu_wallet.mint_timeout_seconds < 1) {
        config->cashu_wallet.mint_timeout_seconds = 30;
    }

    return 0;
}

static cJSON* find_tag_value_string(cJSON* tags, const char* tag_key) {
    if (!tags || !cJSON_IsArray(tags) || !tag_key) {
        return NULL;
    }

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

        cJSON* 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, tag_key) == 0) {
            return val;
        }
    }

    return NULL;
}

static int set_tag_value_string(cJSON* tags, const char* tag_key, const char* tag_value) {
    if (!tags || !cJSON_IsArray(tags) || !tag_key || !tag_value || tag_value[0] == '\0') {
        return -1;
    }

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

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

        if (strcmp(key->valuestring, tag_key) == 0) {
            if (cJSON_IsString(val)) {
                if (!cJSON_SetValuestring(val, tag_value)) {
                    return -1;
                }
                return 0;
            }

            cJSON* new_val = cJSON_CreateString(tag_value);
            if (!new_val) {
                return -1;
            }
            cJSON_ReplaceItemInArray(tag, 1, new_val);
            return 0;
        }
    }

    cJSON* new_tag = cJSON_CreateArray();
    if (!new_tag) {
        return -1;
    }
    cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_key));
    cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_value));
    cJSON_AddItemToArray(tags, new_tag);
    return 0;
}

static int startup_event_has_d_tag(const startup_event_t* se, const char* d_tag);
static int append_startup_event(didactyl_config_t* config, int kind, const char* content, const char* tags_json);

static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) {
    if (!item || !tags || !cJSON_IsArray(tags)) {
        return 0;
    }
    if (event_kind != 31123 && event_kind != 31124) {
        return 0;
    }

    cJSON* d_val = find_tag_value_string(tags, "d");
    if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) {
        return 0;
    }

    int needs_normalize =
        (strcmp(d_val->valuestring, "skill") == 0 || strcmp(d_val->valuestring, "private_skill") == 0);
    if (!needs_normalize) {
        return 0;
    }

    const char* d_tag = NULL;

    if (!d_tag) {
        cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields");
        if (content_fields && cJSON_IsObject(content_fields)) {
            cJSON* name = cJSON_GetObjectItemCaseSensitive(content_fields, "name");
            if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
                d_tag = name->valuestring;
            }
        }
    }

    if (!d_tag) {
        return 0;
    }

    return set_tag_value_string(tags, "d", d_tag);
}

static int parse_legacy_default_skill_to_startup_events(cJSON* root, didactyl_config_t* config) {
    if (!root || !config) {
        return -1;
    }

    cJSON* def = cJSON_GetObjectItemCaseSensitive(root, "default_skill");
    if (!def || !cJSON_IsObject(def)) {
        return 0;
    }

    cJSON* kind = cJSON_GetObjectItemCaseSensitive(def, "kind");
    cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(def, "d_tag");
    cJSON* content = cJSON_GetObjectItemCaseSensitive(def, "content");
    cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(def, "content_fields");
    cJSON* tags = cJSON_GetObjectItemCaseSensitive(def, "tags");

    if (!kind || !cJSON_IsNumber(kind) || !d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0') {
        return -1;
    }

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

    for (int i = 0; i < config->startup_event_count; i++) {
        startup_event_t* se = &config->startup_events[i];
        if (se->kind == kind_i && startup_event_has_d_tag(se, d_tag->valuestring)) {
            return 0;
        }
    }

    char* content_text = NULL;
    if (content_fields) {
        if (!cJSON_IsObject(content_fields)) {
            return -1;
        }
        content_text = cJSON_PrintUnformatted(content_fields);
    } else {
        if (!content || !cJSON_IsString(content) || !content->valuestring) {
            return -1;
        }
        content_text = strdup(content->valuestring);
    }
    if (!content_text) {
        return -1;
    }

    cJSON* tags_work = NULL;
    if (tags) {
        if (!cJSON_IsArray(tags)) {
            free(content_text);
            return -1;
        }
        tags_work = cJSON_Duplicate(tags, 1);
    } else {
        tags_work = cJSON_CreateArray();
    }
    if (!tags_work || !cJSON_IsArray(tags_work)) {
        cJSON_Delete(tags_work);
        free(content_text);
        return -1;
    }

    if (set_tag_value_string(tags_work, "d", d_tag->valuestring) != 0) {
        cJSON_Delete(tags_work);
        free(content_text);
        return -1;
    }

    char* tags_json = cJSON_PrintUnformatted(tags_work);
    cJSON_Delete(tags_work);
    if (!tags_json) {
        free(content_text);
        return -1;
    }

    int rc = append_startup_event(config, kind_i, content_text, tags_json);
    free(content_text);
    free(tags_json);
    return rc;
}

static int parse_startup_events(cJSON* root, didactyl_config_t* config) {
    cJSON* arr = cJSON_GetObjectItemCaseSensitive(root, "startup_events");
    if (!arr || !cJSON_IsArray(arr)) {
        return 0;
    }

    int count = cJSON_GetArraySize(arr);
    if (count <= 0) return 0;

    config->startup_events = (startup_event_t*)calloc((size_t)count, sizeof(startup_event_t));
    if (!config->startup_events) return -1;
    config->startup_event_count = count;

    for (int i = 0; i < count; i++) {
        cJSON* item = cJSON_GetArrayItem(arr, i);
        if (!item || !cJSON_IsObject(item)) return -1;

        cJSON* kind = cJSON_GetObjectItemCaseSensitive(item, "kind");
        cJSON* content = cJSON_GetObjectItemCaseSensitive(item, "content");
        cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields");
        cJSON* tags = cJSON_GetObjectItemCaseSensitive(item, "tags");

        if (!kind || !cJSON_IsNumber(kind)) {
            return -1;
        }

        config->startup_events[i].kind = (int)kind->valuedouble;

        if (content_fields) {
            if (!cJSON_IsObject(content_fields)) {
                return -1;
            }
            config->startup_events[i].content = cJSON_PrintUnformatted(content_fields);
            if (!config->startup_events[i].content) {
                return -1;
            }
        } else {
            if (!content || !cJSON_IsString(content) || !content->valuestring) {
                return -1;
            }
            config->startup_events[i].content = strdup(content->valuestring);
            if (!config->startup_events[i].content) {
                return -1;
            }
        }

        if (tags) {
            if (!cJSON_IsArray(tags)) return -1;
            if (normalize_skill_d_tag(config->startup_events[i].kind, item, tags) != 0) {
                return -1;
            }
            config->startup_events[i].tags_json = cJSON_PrintUnformatted(tags);
            if (!config->startup_events[i].tags_json) return -1;
        }
    }

    return 0;
}

static int parse_relays_from_startup_kind10002(didactyl_config_t* config) {
    if (!config || !config->startup_events || config->startup_event_count <= 0) {
        return -1;
    }

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

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

        int tag_count = cJSON_GetArraySize(tags);
        int relay_count = 0;
        for (int j = 0; j < tag_count; j++) {
            cJSON* tag = cJSON_GetArrayItem(tags, j);
            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') {
                relay_count++;
            }
        }

        if (relay_count <= 0) {
            cJSON_Delete(tags);
            continue;
        }

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

        config->relay_count = relay_count;
        int out_i = 0;
        for (int j = 0; j < tag_count && out_i < relay_count; j++) {
            cJSON* tag = cJSON_GetArrayItem(tags, j);
            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') {
                config->relays[out_i] = strdup(val->valuestring);
                if (!config->relays[out_i]) {
                    cJSON_Delete(tags);
                    return -1;
                }
                out_i++;
            }
        }

        cJSON_Delete(tags);
        return 0;
    }

    return -1;
}

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

    static const char* defaults[] = {
        "wss://relay.damus.io",
        "wss://relay.primal.net",
        "wss://nos.lol"
    };

    const int relay_count = (int)(sizeof(defaults) / sizeof(defaults[0]));
    char** relays = (char**)calloc((size_t)relay_count, sizeof(char*));
    if (!relays) {
        return -1;
    }

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

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

    config->relays = relays;
    config->relay_count = relay_count;
    return 0;
}

static int parse_relays(cJSON* root, didactyl_config_t* config) {
    (void)root;
    if (parse_relays_from_startup_kind10002(config) == 0) {
        return 0;
    }
    return set_default_bootstrap_relays(config);
}

static int startup_event_has_d_tag(const startup_event_t* se, const char* d_tag) {
    if (!se || !se->tags_json || !d_tag || d_tag[0] == '\0') {
        return 0;
    }

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

    int found = 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* 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 && strcmp(v->valuestring, d_tag) == 0) {
            found = 1;
            break;
        }
    }

    cJSON_Delete(tags);
    return found;
}

static int adoption_tags_has_addr(cJSON* tags, const char* addr) {
    if (!tags || !cJSON_IsArray(tags) || !addr || addr[0] == '\0') {
        return 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* k = cJSON_GetArrayItem(tag, 0);
        cJSON* v = cJSON_GetArrayItem(tag, 1);
        if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue;
        if (strcmp(k->valuestring, "a") == 0 && strcmp(v->valuestring, addr) == 0) {
            return 1;
        }
    }

    return 0;
}

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

    cJSON* tag = cJSON_CreateArray();
    if (!tag) return -1;
    cJSON_AddItemToArray(tag, cJSON_CreateString("a"));
    cJSON_AddItemToArray(tag, cJSON_CreateString(addr));
    cJSON_AddItemToArray(tags, tag);
    return 0;
}

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

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

    config->startup_events = grown;
    memset(&config->startup_events[old_count], 0, sizeof(startup_event_t));
    config->startup_events[old_count].kind = kind;
    config->startup_events[old_count].content = strdup(content);
    if (!config->startup_events[old_count].content) {
        return -1;
    }

    if (tags_json && tags_json[0] != '\0') {
        config->startup_events[old_count].tags_json = strdup(tags_json);
        if (!config->startup_events[old_count].tags_json) {
            free(config->startup_events[old_count].content);
            config->startup_events[old_count].content = NULL;
            return -1;
        }
    }

    config->startup_event_count = old_count + 1;
    return 0;
}

int config_ensure_startup_skill_adoption(didactyl_config_t* config) {
    if (!config || config->keys.public_key_hex[0] == '\0') {
        return 0;
    }

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

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

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

        cJSON* d_val = find_tag_value_string(tags, "d");
        if (d_val && cJSON_IsString(d_val) && d_val->valuestring && d_val->valuestring[0] != '\0') {
            char addr[256];
            snprintf(addr,
                     sizeof(addr),
                     "%d:%s:%s",
                     se->kind,
                     config->keys.public_key_hex,
                     d_val->valuestring);
            cJSON_AddItemToArray(needed_addrs, cJSON_CreateString(addr));
        }

        cJSON_Delete(tags);
    }

    if (cJSON_GetArraySize(needed_addrs) == 0) {
        cJSON_Delete(needed_addrs);
        return 0;
    }

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

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

        int needed_n = cJSON_GetArraySize(needed_addrs);
        for (int j = 0; j < needed_n; j++) {
            cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j);
            if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') {
                continue;
            }
            if (!adoption_tags_has_addr(tags, addr_j->valuestring) && append_adoption_addr_tag(tags, addr_j->valuestring) != 0) {
                cJSON_Delete(tags);
                cJSON_Delete(needed_addrs);
                return -1;
            }
        }

        char* updated = cJSON_PrintUnformatted(tags);
        cJSON_Delete(tags);
        if (!updated) {
            cJSON_Delete(needed_addrs);
            return -1;
        }

        free(se->tags_json);
        se->tags_json = updated;
        cJSON_Delete(needed_addrs);
        return 0;
    }

    cJSON* tags = cJSON_CreateArray();
    if (!tags) {
        cJSON_Delete(needed_addrs);
        return -1;
    }

    int needed_n = cJSON_GetArraySize(needed_addrs);
    for (int j = 0; j < needed_n; j++) {
        cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j);
        if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') {
            continue;
        }
        if (append_adoption_addr_tag(tags, addr_j->valuestring) != 0) {
            cJSON_Delete(tags);
            cJSON_Delete(needed_addrs);
            return -1;
        }
    }

    cJSON* app = cJSON_CreateArray();
    cJSON* scope = cJSON_CreateArray();
    if (!app || !scope) {
        cJSON_Delete(app);
        cJSON_Delete(scope);
        cJSON_Delete(tags);
        cJSON_Delete(needed_addrs);
        return -1;
    }
    cJSON_AddItemToArray(app, cJSON_CreateString("app"));
    cJSON_AddItemToArray(app, cJSON_CreateString("didactyl"));
    cJSON_AddItemToArray(scope, cJSON_CreateString("scope"));
    cJSON_AddItemToArray(scope, cJSON_CreateString("private"));
    cJSON_AddItemToArray(tags, app);
    cJSON_AddItemToArray(tags, scope);

    char* tags_json = cJSON_PrintUnformatted(tags);
    cJSON_Delete(tags);
    cJSON_Delete(needed_addrs);
    if (!tags_json) {
        return -1;
    }

    int rc = append_startup_event(config, 10123, "", tags_json);
    free(tags_json);
    return rc;
}

void config_free(didactyl_config_t* config) {
    if (!config) {
        return;
    }

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

    if (config->startup_events) {
        for (int i = 0; i < config->startup_event_count; i++) {
            free(config->startup_events[i].content);
            free(config->startup_events[i].tags_json);
        }
        free(config->startup_events);
    }

    free_cashu_wallet_mints(&config->cashu_wallet);

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

int config_load(const char* path, didactyl_config_t* config) {
    if (!path || !config) {
        config_set_error("config_load called with invalid arguments");
        return -1;
    }

    config_set_error("unknown configuration error");

    memset(config, 0, sizeof(*config));
    snprintf(config->config_path, sizeof(config->config_path), "%s", path);
    config->dm_protocol = DM_PROTOCOL_NIP04;

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

    config->security.verify_signatures = 1;
    config->security.admin.enabled = 1;
    config->security.admin.tools_enabled = 1;
    config->security.wot.enabled = 1;
    config->security.wot.tools_enabled = 0;
    config->security.stranger.enabled = 1;
    config->security.stranger.tools_enabled = 0;
    config->security.stranger_response[0] = '\0';

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

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

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

    config->cashu_wallet.enabled = 0;
    config->cashu_wallet.mint_urls = NULL;
    config->cashu_wallet.mint_count = 0;
    snprintf(config->cashu_wallet.unit, sizeof(config->cashu_wallet.unit), "%s", "sat");
    config->cashu_wallet.auto_load = 1;
    config->cashu_wallet.mint_timeout_seconds = 30;

    char* raw_buf = NULL;
    size_t raw_len = 0;
    if (read_file_to_buffer(path, &raw_buf, &raw_len) != 0) {
        if (errno == ENOENT) {
            if (set_default_bootstrap_relays(config) != 0) {
                config_set_error("failed to initialize default bootstrap relays");
                return -1;
            }
            return 0;
        }
        config_set_error("failed to read config file '%s': %s", path, strerror(errno));
        return -1;
    }

    /* Strip JSONC comments before parsing */
    char* json_buf = jsonc_strip_comments(raw_buf, raw_len);
    free(raw_buf);
    if (!json_buf) {
        config_set_error("failed to allocate memory for JSONC comment stripping");
        return -1;
    }
    size_t json_len = strlen(json_buf);

    cJSON* root = cJSON_ParseWithLength(json_buf, json_len);
    free(json_buf);

    if (!root) {
        const char* err = cJSON_GetErrorPtr();
        config_set_error("invalid JSON in config '%s'%s%s",
                         path,
                         err ? " near: " : "",
                         err ? err : "");
        return -1;
    }

    int rc = -1;

    cJSON* keys = cJSON_GetObjectItemCaseSensitive(root, "keys");
    if (!keys || !cJSON_IsObject(keys)) {
        keys = cJSON_GetObjectItemCaseSensitive(root, "key");
    }
    cJSON* admin = cJSON_GetObjectItemCaseSensitive(root, "admin");
    cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm");

    if (!admin || !cJSON_IsObject(admin) ||
        !llm || !cJSON_IsObject(llm)) {
        config_set_error("config must include object sections: admin, llm");
        goto cleanup;
    }

    if (!keys || !cJSON_IsObject(keys)) {
        config->keys.nsec[0] = '\0';
    } else {
        if (copy_json_string(keys, "nsec", config->keys.nsec, sizeof(config->keys.nsec), 0) != 0) {
            config_set_error("keys.nsec/key.nsec must be a string when provided");
            goto cleanup;
        }
    }

    char admin_key_raw[OW_MAX_KEY_LEN] = {0};
    if (copy_json_string(admin, "pubkey", admin_key_raw, sizeof(admin_key_raw), 1) != 0) {
        config_set_error("admin.pubkey is required and must be a non-empty string");
        goto cleanup;
    }
    if (decode_pubkey_hex_or_npub(admin_key_raw, config->admin.pubkey) != 0) {
        config_set_error("admin.pubkey must be npub1... or 64-char hex");
        goto cleanup;
    }

    if (parse_startup_events(root, config) != 0) {
        config_set_error("invalid startup_events configuration");
        goto cleanup;
    }

    if (parse_legacy_default_skill_to_startup_events(root, config) != 0) {
        config_set_error("invalid legacy default_skill configuration");
        goto cleanup;
    }

    if (parse_relays(root, config) != 0) {
        config_set_error("relay configuration is invalid and bootstrap fallback failed");
        goto cleanup;
    }

    if (copy_json_string(llm, "provider", config->llm.provider, sizeof(config->llm.provider), 0) != 0) {
        config_set_error("llm.provider must be a string if provided");
        goto cleanup;
    }
    if (config->llm.provider[0] == '\0') {
        strcpy(config->llm.provider, "openai");
    }

    if (copy_json_string(llm, "api_key", config->llm.api_key, sizeof(config->llm.api_key), 1) != 0) {
        config_set_error("llm.api_key is required and must be a string");
        goto cleanup;
    }
    if (copy_json_string(llm, "model", config->llm.model, sizeof(config->llm.model), 1) != 0) {
        config_set_error("llm.model is required and must be a string");
        goto cleanup;
    }
    if (copy_json_string(llm, "base_url", config->llm.base_url, sizeof(config->llm.base_url), 1) != 0) {
        config_set_error("llm.base_url is required and must be a string");
        goto cleanup;
    }

    cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(llm, "max_tokens");
    cJSON* temperature = cJSON_GetObjectItemCaseSensitive(llm, "temperature");

    config->llm.max_tokens = (max_tokens && cJSON_IsNumber(max_tokens)) ? (int)max_tokens->valuedouble : 512;
    config->llm.temperature = (temperature && cJSON_IsNumber(temperature)) ? temperature->valuedouble : 0.7;

    if (parse_dm_protocol_config(root, config) != 0) {
        config_set_error("invalid dm_protocol configuration (expected 'nip04', 'nip17', or 'both')");
        goto cleanup;
    }

    if (parse_tools_config(root, config) != 0) {
        config_set_error("invalid tools configuration");
        goto cleanup;
    }

    if (parse_security_config(root, config) != 0) {
        config_set_error("invalid security configuration");
        goto cleanup;
    }

    if (parse_admin_context_config(root, config) != 0) {
        config_set_error("invalid admin_context configuration");
        goto cleanup;
    }

    if (parse_triggers_config(root, config) != 0) {
        config_set_error("invalid triggers configuration");
        goto cleanup;
    }

    if (parse_api_config(root, config) != 0) {
        config_set_error("invalid api configuration");
        goto cleanup;
    }

    if (parse_cashu_wallet_config(root, config) != 0) {
        config_set_error("invalid cashu_wallet configuration");
        goto cleanup;
    }

    if (config->keys.nsec[0] != '\0') {
        if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
            config_set_error("keys.nsec/key.nsec must be valid nsec1... or 64-char hex private key");
            goto cleanup;
        }

        if (nostr_ec_public_key_from_private_key(config->keys.private_key, config->keys.public_key) != 0) {
            config_set_error("failed to derive public key from keys.nsec/key.nsec");
            goto cleanup;
        }

        nostr_bytes_to_hex(config->keys.public_key, 32, config->keys.public_key_hex);
    }

    rc = 0;

cleanup:
    cJSON_Delete(root);
    if (rc != 0) {
        config_free(config);
    }
    return rc;
}
