#define _POSIX_C_SOURCE 200809L

#include "tools_internal.h"

#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "cjson/cJSON.h"
#include "../nostr_handler.h"

static char* json_error_local(const char* msg) {
    cJSON* root = cJSON_CreateObject();
    if (!root) return NULL;
    cJSON_AddBoolToObject(root, "success", 0);
    cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
    char* out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    return out;
}

static const char* skip_ws_local(const char* p) {
    while (p && *p && isspace((unsigned char)*p)) p++;
    return p;
}

static char* sanitize_json_string_controls_local(const char* in) {
    if (!in) return NULL;

    size_t len = strlen(in);
    size_t cap = (len * 2U) + 1U;
    char* out = (char*)malloc(cap);
    if (!out) return NULL;

    int in_string = 0;
    int escaping = 0;
    size_t j = 0;

    for (size_t i = 0; i < len; i++) {
        char c = in[i];

        if (escaping) {
            if (j + 1U >= cap) {
                free(out);
                return NULL;
            }
            out[j++] = c;
            escaping = 0;
            continue;
        }

        if (c == '\\') {
            if (j + 1U >= cap) {
                free(out);
                return NULL;
            }
            out[j++] = c;
            if (in_string) escaping = 1;
            continue;
        }

        if (c == '"') {
            if (j + 1U >= cap) {
                free(out);
                return NULL;
            }
            out[j++] = c;
            in_string = !in_string;
            continue;
        }

        if (in_string && (c == '\n' || c == '\r' || c == '\t')) {
            if (j + 2U >= cap) {
                free(out);
                return NULL;
            }
            out[j++] = '\\';
            out[j++] = (c == '\n') ? 'n' : (c == '\r') ? 'r' : 't';
            continue;
        }

        if (j + 1U >= cap) {
            free(out);
            return NULL;
        }
        out[j++] = c;
    }

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

static cJSON* parse_tool_args_json_local(const char* args_json) {
    const char* raw_args_json = args_json ? args_json : "{}";
    raw_args_json = skip_ws_local(raw_args_json);
    if (!raw_args_json || raw_args_json[0] == '\0') {
        raw_args_json = "{}";
    }

    cJSON* args = cJSON_Parse(raw_args_json);
    char* repaired_args_json = NULL;

    if (args && cJSON_IsString(args) && args->valuestring) {
        cJSON* nested = cJSON_Parse(args->valuestring);
        if (nested) {
            cJSON_Delete(args);
            args = nested;
        }
    }

    if (!args) {
        repaired_args_json = sanitize_json_string_controls_local(raw_args_json);
        if (repaired_args_json) {
            args = cJSON_Parse(repaired_args_json);
            if (args && cJSON_IsString(args) && args->valuestring) {
                cJSON* nested = cJSON_Parse(args->valuestring);
                if (nested) {
                    cJSON_Delete(args);
                    args = nested;
                }
            }
        }
    }

    free(repaired_args_json);
    return args;
}

static const char* find_key_start_local(const char* in, const char* key) {
    if (!in || !key) return NULL;

    char pattern[64];
    int n = snprintf(pattern, sizeof(pattern), "\"%s\"", key);
    if (n <= 0 || (size_t)n >= sizeof(pattern)) return NULL;

    return strstr(in, pattern);
}

static int parse_loose_kind_local(const char* in, int* out_kind) {
    if (!in || !out_kind) return -1;

    const char* p = find_key_start_local(in, "kind");
    if (!p) return -1;

    p = strchr(p, ':');
    if (!p) return -1;
    p = skip_ws_local(p + 1);
    if (!p || !*p) return -1;

    char* end = NULL;
    long v = strtol(p, &end, 10);
    if (end == p) return -1;

    *out_kind = (int)v;
    return 0;
}

static char* parse_loose_json_string_value_local(const char* in, const char* key) {
    if (!in || !key) return NULL;

    const char* p = find_key_start_local(in, key);
    if (!p) return NULL;

    p = strchr(p, ':');
    if (!p) return NULL;
    p = skip_ws_local(p + 1);
    if (!p || *p != '"') return NULL;
    p++;

    size_t max_len = strlen(p);
    char* out = (char*)malloc(max_len + 1U);
    if (!out) return NULL;

    size_t j = 0;
    int escaping = 0;
    for (size_t i = 0; p[i] != '\0'; i++) {
        char c = p[i];

        if (escaping) {
            switch (c) {
                case 'n': out[j++] = '\n'; break;
                case 'r': out[j++] = '\r'; break;
                case 't': out[j++] = '\t'; break;
                case '"': out[j++] = '"'; break;
                case '\\': out[j++] = '\\'; break;
                default: out[j++] = c; break;
            }
            escaping = 0;
            continue;
        }

        if (c == '\\') {
            escaping = 1;
            continue;
        }

        if (c == '"') {
            out[j] = '\0';
            return out;
        }

        out[j++] = c;
    }

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

static cJSON* parse_loose_nostr_post_args_local(const char* in) {
    if (!in) return NULL;

    int kind = 0;
    if (parse_loose_kind_local(in, &kind) != 0) {
        return NULL;
    }

    char* content = parse_loose_json_string_value_local(in, "content");
    if (!content) {
        return NULL;
    }

    cJSON* args = cJSON_CreateObject();
    if (!args) {
        free(content);
        return NULL;
    }

    cJSON_AddNumberToObject(args, "kind", kind);
    cJSON_AddStringToObject(args, "content", content);
    free(content);
    return args;
}

static cJSON* ensure_tags_array_local(cJSON** tags_inout) {
    if (!tags_inout) return NULL;

    if (*tags_inout) {
        if (cJSON_IsArray(*tags_inout)) {
            return *tags_inout;
        }
        cJSON_Delete(*tags_inout);
        *tags_inout = NULL;
    }

    *tags_inout = cJSON_CreateArray();
    return *tags_inout;
}

static int has_tag_key_local(cJSON* tags, const char* key) {
    if (!tags || !cJSON_IsArray(tags) || !key) 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)) continue;

        cJSON* k = cJSON_GetArrayItem(tag, 0);
        if (k && cJSON_IsString(k) && k->valuestring && strcmp(k->valuestring, key) == 0) {
            return 1;
        }
    }
    return 0;
}

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

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

    cJSON_AddItemToArray(tag, cJSON_CreateString(key));
    cJSON_AddItemToArray(tag, cJSON_CreateString(value));
    cJSON_AddItemToArray(tags, tag);
    return 0;
}

static char* trim_copy_local(const char* start, size_t len) {
    while (len > 0 && isspace((unsigned char)start[0])) {
        start++;
        len--;
    }
    while (len > 0 && isspace((unsigned char)start[len - 1])) {
        len--;
    }

    char* out = (char*)malloc(len + 1U);
    if (!out) return NULL;
    memcpy(out, start, len);
    out[len] = '\0';
    return out;
}

static char* first_markdown_h1_local(const char* content) {
    if (!content) return NULL;

    const char* p = content;
    while (*p) {
        const char* line = p;
        const char* nl = strchr(line, '\n');
        size_t len = nl ? (size_t)(nl - line) : strlen(line);
        while (len > 0 && line[len - 1] == '\r') len--;

        size_t i = 0;
        while (i < len && isspace((unsigned char)line[i])) i++;
        if (i < len && line[i] == '#') {
            size_t j = i;
            while (j < len && line[j] == '#') j++;
            if (j < len && isspace((unsigned char)line[j])) {
                while (j < len && isspace((unsigned char)line[j])) j++;
                if (j < len) return trim_copy_local(line + j, len - j);
            }
        }

        if (!nl) break;
        p = nl + 1;
    }
    return NULL;
}

static int is_paragraph_line_local(const char* line, size_t len) {
    size_t i = 0;
    while (i < len && isspace((unsigned char)line[i])) i++;
    if (i >= len) return 0;

    char c = line[i];
    if (c == '#' || c == '>' || c == '-' || c == '*' || c == '`' || c == '|') return 0;
    if (isdigit((unsigned char)c)) {
        size_t j = i;
        while (j < len && isdigit((unsigned char)line[j])) j++;
        if (j < len && line[j] == '.') return 0;
    }
    return 1;
}

static char* first_markdown_paragraph_local(const char* content) {
    if (!content) return NULL;

    const char* p = content;
    while (*p) {
        const char* line = p;
        const char* nl = strchr(line, '\n');
        size_t len = nl ? (size_t)(nl - line) : strlen(line);
        while (len > 0 && line[len - 1] == '\r') len--;

        if (is_paragraph_line_local(line, len)) {
            size_t cap = 1024;
            size_t used = 0;
            char* out = (char*)malloc(cap);
            if (!out) return NULL;

            const char* q = line;
            const char* qnl = nl;
            size_t qlen = len;

            while (1) {
                if (!is_paragraph_line_local(q, qlen)) break;

                size_t start = 0;
                while (start < qlen && isspace((unsigned char)q[start])) start++;
                size_t end = qlen;
                while (end > start && isspace((unsigned char)q[end - 1])) end--;
                size_t part_len = end - start;

                if (used + part_len + 2 >= cap) {
                    cap = (cap * 2U) + part_len + 16U;
                    char* bigger = (char*)realloc(out, cap);
                    if (!bigger) {
                        free(out);
                        return NULL;
                    }
                    out = bigger;
                }

                if (part_len > 0) {
                    if (used > 0) out[used++] = ' ';
                    memcpy(out + used, q + start, part_len);
                    used += part_len;
                }

                if (!qnl) break;
                q = qnl + 1;
                qnl = strchr(q, '\n');
                qlen = qnl ? (size_t)(qnl - q) : strlen(q);
                while (qlen > 0 && q[qlen - 1] == '\r') qlen--;
            }

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

        if (!nl) break;
        p = nl + 1;
    }

    return NULL;
}

static char* first_markdown_image_url_local(const char* content) {
    if (!content) return NULL;

    const char* p = content;
    while ((p = strstr(p, "![")) != NULL) {
        const char* close_bracket = strchr(p + 2, ']');
        if (!close_bracket || close_bracket[1] != '(') {
            p += 2;
            continue;
        }

        const char* url_start = close_bracket + 2;
        const char* url_end = strchr(url_start, ')');
        if (!url_end || url_end <= url_start) {
            p += 2;
            continue;
        }

        return trim_copy_local(url_start, (size_t)(url_end - url_start));
    }

    return NULL;
}

static char* d_tagify_string_local(const char* in, const char* fallback) {
    if (!in || in[0] == '\0') {
        return fallback ? strdup(fallback) : NULL;
    }

    size_t len = strlen(in);
    char* out = (char*)malloc(len + 1U);
    if (!out) return NULL;

    size_t j = 0;
    int prev_dash = 0;
    for (size_t i = 0; i < len; i++) {
        unsigned char c = (unsigned char)in[i];
        if (isalnum(c)) {
            out[j++] = (char)tolower(c);
            prev_dash = 0;
        } else if (!prev_dash && j > 0) {
            out[j++] = '-';
            prev_dash = 1;
        }
    }

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

    if (j == 0) {
        free(out);
        return fallback ? strdup(fallback) : NULL;
    }

    return out;
}

static void ensure_nip23_metadata_tags_local(int kind, const char* content, cJSON** tags_inout) {
    if (!content || (kind != 30023 && kind != 30024) || !tags_inout) {
        return;
    }

    cJSON* tags = ensure_tags_array_local(tags_inout);
    if (!tags) return;

    char* title = has_tag_key_local(tags, "title") ? NULL : first_markdown_h1_local(content);
    char* summary = has_tag_key_local(tags, "summary") ? NULL : first_markdown_paragraph_local(content);
    char* image = has_tag_key_local(tags, "image") ? NULL : first_markdown_image_url_local(content);

    if (title) (void)add_string_tag_local(tags, "title", title);
    if (summary) (void)add_string_tag_local(tags, "summary", summary);
    if (image) (void)add_string_tag_local(tags, "image", image);

    if (!has_tag_key_local(tags, "published_at")) {
        char published_at[32];
        snprintf(published_at, sizeof(published_at), "%lld", (long long)time(NULL));
        (void)add_string_tag_local(tags, "published_at", published_at);
    }

    if (!has_tag_key_local(tags, "d")) {
        char* d_tag = NULL;
        if (title) {
            d_tag = d_tagify_string_local(title, "long-form-note");
        } else {
            char* derived_title = first_markdown_h1_local(content);
            d_tag = d_tagify_string_local(derived_title, "long-form-note");
            free(derived_title);
        }
        if (d_tag) {
            (void)add_string_tag_local(tags, "d", d_tag);
            free(d_tag);
        }
    }

    free(title);
    free(summary);
    free(image);
}

static int is_safe_relative_path_local(const char* path) {
    if (!path || path[0] == '\0') return 0;
    if (path[0] == '/') return 0;
    if (strstr(path, "..") != NULL) return 0;
    if (strchr(path, '\\') != NULL) return 0;
    return 1;
}

static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
    if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
    if (!is_safe_relative_path_local(rel_path)) return -1;

    const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
                          ? ctx->cfg->tools.shell.working_directory
                          : ".";

    int n = 0;
    if (strcmp(cwd, ".") == 0) {
        n = snprintf(out, out_size, "%s", rel_path);
    } else {
        n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
    }

    if (n < 0 || (size_t)n >= out_size) return -1;
    return 0;
}

static char* read_entire_file_local(const char* file_path, size_t* out_bytes) {
    if (!file_path) return NULL;

    FILE* fp = fopen(file_path, "rb");
    if (!fp) return NULL;

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

    long size = ftell(fp);
    if (size < 0) {
        fclose(fp);
        return NULL;
    }

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

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

    size_t n = fread(buf, 1, (size_t)size, fp);
    fclose(fp);
    buf[n] = '\0';

    if (out_bytes) *out_bytes = n;
    return buf;
}

static char* basename_lowercase_dup_local(const char* path) {
    if (!path || path[0] == '\0') return NULL;

    const char* base = strrchr(path, '/');
    base = base ? (base + 1) : path;
    if (base[0] == '\0') return NULL;

    char* out = strdup(base);
    if (!out) return NULL;

    for (size_t i = 0; out[i] != '\0'; i++) {
        out[i] = (char)tolower((unsigned char)out[i]);
    }

    return out;
}

char* execute_nostr_post(const char* args_json) {
    const char* raw_args_json = args_json ? args_json : "{}";
    raw_args_json = skip_ws_local(raw_args_json);
    if (!raw_args_json || raw_args_json[0] == '\0') {
        raw_args_json = "{}";
    }

    cJSON* args = cJSON_Parse(raw_args_json);
    char* repaired_args_json = NULL;

    if (args && cJSON_IsString(args) && args->valuestring) {
        cJSON* nested = cJSON_Parse(args->valuestring);
        if (nested) {
            cJSON_Delete(args);
            args = nested;
        }
    }

    if (!args && raw_args_json) {
        repaired_args_json = sanitize_json_string_controls_local(raw_args_json);
        if (repaired_args_json) {
            args = cJSON_Parse(repaired_args_json);
            if (args && cJSON_IsString(args) && args->valuestring) {
                cJSON* nested = cJSON_Parse(args->valuestring);
                if (nested) {
                    cJSON_Delete(args);
                    args = nested;
                }
            }
        }
    }

    if (!args && raw_args_json) {
        args = parse_loose_nostr_post_args_local(raw_args_json);
    }
    if (!args) {
        free(repaired_args_json);
        return json_error_local("invalid arguments JSON");
    }

    cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
    cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
    cJSON* tags = cJSON_GetObjectItemCaseSensitive(args, "tags");
    if (!kind || !cJSON_IsNumber(kind) || !content || !cJSON_IsString(content) || !content->valuestring) {
        cJSON_Delete(args);
        free(repaired_args_json);
        return json_error_local("nostr_post requires integer kind and string content");
    }
    if (tags && !cJSON_IsArray(tags)) {
        cJSON_Delete(args);
        free(repaired_args_json);
        return json_error_local("nostr_post tags must be an array when provided");
    }

    cJSON* tags_dup = NULL;
    if (tags) {
        tags_dup = cJSON_Duplicate(tags, 1);
        if (!tags_dup) {
            cJSON_Delete(args);
            free(repaired_args_json);
            return json_error_local("nostr_post failed to duplicate tags");
        }
    }

    ensure_nip23_metadata_tags_local((int)kind->valuedouble, content->valuestring, &tags_dup);

    nostr_publish_result_t publish_result;
    memset(&publish_result, 0, sizeof(publish_result));

    int rc = nostr_handler_publish_kind_event((int)kind->valuedouble,
                                              content->valuestring,
                                              tags_dup,
                                              &publish_result);
    cJSON_Delete(tags_dup);
    cJSON_Delete(args);
    free(repaired_args_json);

    if (rc != 0) {
        nostr_handler_publish_result_free(&publish_result);
        return json_error_local("nostr_post failed");
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        nostr_handler_publish_result_free(&publish_result);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
    cJSON_AddStringToObject(out, "message", "nostr_post published");
    cJSON_AddNumberToObject(out, "kind", publish_result.kind);
    cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
    cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
    cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);

    if (publish_result.note_uri[0] != '\0') cJSON_AddStringToObject(out, "note_uri", publish_result.note_uri);
    if (publish_result.naddr_uri[0] != '\0') cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
    if (publish_result.d_tag[0] != '\0') cJSON_AddStringToObject(out, "d_tag", publish_result.d_tag);

    cJSON* relays = cJSON_CreateArray();
    if (!relays) {
        nostr_handler_publish_result_free(&publish_result);
        cJSON_Delete(out);
        return NULL;
    }
    for (int i = 0; i < publish_result.relay_count; i++) {
        cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
    }
    cJSON_AddItemToObject(out, "relays", relays);

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    nostr_handler_publish_result_free(&publish_result);
    return json;
}

char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_tool_args_json_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");
    cJSON_Delete(args);

    char readme_path[PATH_MAX];
    if (build_tool_path_local(ctx, "README.md", readme_path, sizeof(readme_path)) != 0) {
        return json_error_local("failed to resolve README path");
    }

    size_t bytes_read = 0;
    char* content = read_entire_file_local(readme_path, &bytes_read);
    if (!content) {
        return json_error_local("failed to read README.md");
    }

    cJSON* tags = cJSON_CreateArray();
    if (!tags) {
        free(content);
        return json_error_local("failed to create tags");
    }

    if (add_string_tag_local(tags, "d", "readme.md") != 0 ||
        add_string_tag_local(tags,
                             "image",
                             "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg") != 0) {
        cJSON_Delete(tags);
        free(content);
        return json_error_local("failed to set required tags");
    }

    ensure_nip23_metadata_tags_local(30023, content, &tags);

    nostr_publish_result_t publish_result;
    memset(&publish_result, 0, sizeof(publish_result));

    int rc = nostr_handler_publish_kind_event(30023, content, tags, &publish_result);
    cJSON_Delete(tags);
    free(content);

    if (rc != 0) {
        nostr_handler_publish_result_free(&publish_result);
        return json_error_local("nostr_post_readme failed");
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        nostr_handler_publish_result_free(&publish_result);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
    cJSON_AddStringToObject(out, "message", "nostr_post_readme published");
    cJSON_AddStringToObject(out, "path", readme_path);
    cJSON_AddNumberToObject(out, "bytes_read", (double)bytes_read);
    cJSON_AddNumberToObject(out, "kind", publish_result.kind);
    cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
    cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
    cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);

    if (publish_result.note_uri[0] != '\0') cJSON_AddStringToObject(out, "note_uri", publish_result.note_uri);
    if (publish_result.naddr_uri[0] != '\0') cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
    if (publish_result.d_tag[0] != '\0') cJSON_AddStringToObject(out, "d_tag", publish_result.d_tag);

    cJSON* relays = cJSON_CreateArray();
    if (!relays) {
        nostr_handler_publish_result_free(&publish_result);
        cJSON_Delete(out);
        return NULL;
    }
    for (int i = 0; i < publish_result.relay_count; i++) {
        cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
    }
    cJSON_AddItemToObject(out, "relays", relays);

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    nostr_handler_publish_result_free(&publish_result);
    return json;
}

char* execute_nostr_file_md_to_longform_post(tools_context_t* ctx, const char* args_json) {
    if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");

    cJSON* args = parse_tool_args_json_local(args_json);
    if (!args) return json_error_local("invalid arguments JSON");

    cJSON* file = cJSON_GetObjectItemCaseSensitive(args, "file");
    cJSON* title = cJSON_GetObjectItemCaseSensitive(args, "title");
    cJSON* image = cJSON_GetObjectItemCaseSensitive(args, "image");
    cJSON* summary = cJSON_GetObjectItemCaseSensitive(args, "summary");

    if (!file || !cJSON_IsString(file) || !file->valuestring || file->valuestring[0] == '\0') {
        cJSON_Delete(args);
        return json_error_local("nostr_file_md_to_longform_post requires string file");
    }
    if (title && !cJSON_IsString(title)) {
        cJSON_Delete(args);
        return json_error_local("nostr_file_md_to_longform_post title must be string when provided");
    }
    if (image && !cJSON_IsString(image)) {
        cJSON_Delete(args);
        return json_error_local("nostr_file_md_to_longform_post image must be string when provided");
    }
    if (summary && !cJSON_IsString(summary)) {
        cJSON_Delete(args);
        return json_error_local("nostr_file_md_to_longform_post summary must be string when provided");
    }

    char file_path[PATH_MAX];
    if (build_tool_path_local(ctx, file->valuestring, file_path, sizeof(file_path)) != 0) {
        cJSON_Delete(args);
        return json_error_local("failed to resolve markdown file path");
    }

    size_t bytes_read = 0;
    char* content = read_entire_file_local(file_path, &bytes_read);
    if (!content) {
        cJSON_Delete(args);
        return json_error_local("failed to read markdown file");
    }

    char* d_tag = basename_lowercase_dup_local(file->valuestring);
    if (!d_tag || d_tag[0] == '\0') {
        free(d_tag);
        free(content);
        cJSON_Delete(args);
        return json_error_local("failed to derive d tag from filename");
    }

    cJSON* tags = cJSON_CreateArray();
    if (!tags) {
        free(d_tag);
        free(content);
        cJSON_Delete(args);
        return json_error_local("failed to create tags");
    }

    if (add_string_tag_local(tags, "d", d_tag) != 0) {
        free(d_tag);
        cJSON_Delete(tags);
        free(content);
        cJSON_Delete(args);
        return json_error_local("failed to set d tag");
    }

    if (title && title->valuestring && title->valuestring[0] != '\0') {
        if (add_string_tag_local(tags, "title", title->valuestring) != 0) {
            free(d_tag);
            cJSON_Delete(tags);
            free(content);
            cJSON_Delete(args);
            return json_error_local("failed to set title tag");
        }
    }

    if (image && image->valuestring && image->valuestring[0] != '\0') {
        if (add_string_tag_local(tags, "image", image->valuestring) != 0) {
            free(d_tag);
            cJSON_Delete(tags);
            free(content);
            cJSON_Delete(args);
            return json_error_local("failed to set image tag");
        }
    }

    if (summary && summary->valuestring && summary->valuestring[0] != '\0') {
        if (add_string_tag_local(tags, "summary", summary->valuestring) != 0) {
            free(d_tag);
            cJSON_Delete(tags);
            free(content);
            cJSON_Delete(args);
            return json_error_local("failed to set summary tag");
        }
    }

    ensure_nip23_metadata_tags_local(30023, content, &tags);

    nostr_publish_result_t publish_result;
    memset(&publish_result, 0, sizeof(publish_result));

    int rc = nostr_handler_publish_kind_event(30023, content, tags, &publish_result);
    cJSON_Delete(tags);
    free(content);
    cJSON_Delete(args);

    if (rc != 0) {
        free(d_tag);
        nostr_handler_publish_result_free(&publish_result);
        return json_error_local("nostr_file_md_to_longform_post failed");
    }

    cJSON* out = cJSON_CreateObject();
    if (!out) {
        free(d_tag);
        nostr_handler_publish_result_free(&publish_result);
        return NULL;
    }

    cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
    cJSON_AddStringToObject(out, "message", "nostr_file_md_to_longform_post published");
    cJSON_AddStringToObject(out, "path", file_path);
    cJSON_AddStringToObject(out, "d_tag", d_tag);
    cJSON_AddNumberToObject(out, "bytes_read", (double)bytes_read);
    cJSON_AddNumberToObject(out, "kind", publish_result.kind);
    cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
    cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
    cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);

    if (publish_result.note_uri[0] != '\0') cJSON_AddStringToObject(out, "note_uri", publish_result.note_uri);
    if (publish_result.naddr_uri[0] != '\0') cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);

    cJSON* relays = cJSON_CreateArray();
    if (!relays) {
        free(d_tag);
        nostr_handler_publish_result_free(&publish_result);
        cJSON_Delete(out);
        return NULL;
    }
    for (int i = 0; i < publish_result.relay_count; i++) {
        cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
    }
    cJSON_AddItemToObject(out, "relays", relays);

    char* json = cJSON_PrintUnformatted(out);
    cJSON_Delete(out);
    free(d_tag);
    nostr_handler_publish_result_free(&publish_result);
    return json;
}
