#define _POSIX_C_SOURCE 200809L

#include "nostr_block_list.h"

#include <pthread.h>
#include <stdlib.h>
#include <string.h>

#include "nostr_handler.h"
#include "debug.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"

typedef struct {
    didactyl_config_t* cfg;
    int initialized;
    int loaded;
    pthread_mutex_t mutex;

    char** pub_pubkeys;
    int pub_pubkey_count;
    char** pub_event_ids;
    int pub_event_id_count;
    char** pub_hashtags;
    int pub_hashtag_count;

    char** priv_pubkeys;
    int priv_pubkey_count;
    char** priv_event_ids;
    int priv_event_id_count;
    char** priv_hashtags;
    int priv_hashtag_count;
} block_list_state_t;

static block_list_state_t g_block = {
    .mutex = PTHREAD_MUTEX_INITIALIZER
};

static int is_hex_len_local(const char* s, size_t len) {
    if (!s || strlen(s) != len) return 0;
    for (size_t i = 0; i < len; i++) {
        char c = s[i];
        if (!((c >= '0' && c <= '9') ||
              (c >= 'a' && c <= 'f') ||
              (c >= 'A' && c <= 'F'))) {
            return 0;
        }
    }
    return 1;
}

static int str_array_contains(char** arr, int count, const char* s) {
    if (!arr || count <= 0 || !s) return 0;
    for (int i = 0; i < count; i++) {
        if (arr[i] && strcmp(arr[i], s) == 0) return 1;
    }
    return 0;
}

static int str_array_add_unique(char*** arr, int* count, const char* s) {
    if (!arr || !count || !s || s[0] == '\0') return -1;
    if (str_array_contains(*arr, *count, s)) return 0;

    char** grown = (char**)realloc(*arr, (size_t)(*count + 1) * sizeof(char*));
    if (!grown) return -1;

    char* dup = strdup(s);
    if (!dup) return -1;

    grown[*count] = dup;
    *arr = grown;
    (*count)++;
    return 1;
}

static void str_array_free(char** arr, int count) {
    if (!arr) return;
    for (int i = 0; i < count; i++) {
        free(arr[i]);
    }
    free(arr);
}

static void block_clear_sets_locked(void) {
    str_array_free(g_block.pub_pubkeys, g_block.pub_pubkey_count);
    str_array_free(g_block.pub_event_ids, g_block.pub_event_id_count);
    str_array_free(g_block.pub_hashtags, g_block.pub_hashtag_count);
    str_array_free(g_block.priv_pubkeys, g_block.priv_pubkey_count);
    str_array_free(g_block.priv_event_ids, g_block.priv_event_id_count);
    str_array_free(g_block.priv_hashtags, g_block.priv_hashtag_count);

    g_block.pub_pubkeys = NULL;
    g_block.pub_pubkey_count = 0;
    g_block.pub_event_ids = NULL;
    g_block.pub_event_id_count = 0;
    g_block.pub_hashtags = NULL;
    g_block.pub_hashtag_count = 0;
    g_block.priv_pubkeys = NULL;
    g_block.priv_pubkey_count = 0;
    g_block.priv_event_ids = NULL;
    g_block.priv_event_id_count = 0;
    g_block.priv_hashtags = NULL;
    g_block.priv_hashtag_count = 0;
}

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

    if (strcmp(key, "p") == 0) {
        if (!is_hex_len_local(value, 64U)) return -1;
        return is_public
            ? str_array_add_unique(&g_block.pub_pubkeys, &g_block.pub_pubkey_count, value)
            : str_array_add_unique(&g_block.priv_pubkeys, &g_block.priv_pubkey_count, value);
    }

    if (strcmp(key, "e") == 0) {
        if (!is_hex_len_local(value, 64U)) return -1;
        return is_public
            ? str_array_add_unique(&g_block.pub_event_ids, &g_block.pub_event_id_count, value)
            : str_array_add_unique(&g_block.priv_event_ids, &g_block.priv_event_id_count, value);
    }

    if (strcmp(key, "t") == 0) {
        return is_public
            ? str_array_add_unique(&g_block.pub_hashtags, &g_block.pub_hashtag_count, value)
            : str_array_add_unique(&g_block.priv_hashtags, &g_block.priv_hashtag_count, value);
    }

    return 0;
}

static void parse_tags_into_sets_locked(cJSON* tags, int is_public) {
    if (!tags || !cJSON_IsArray(tags)) return;

    cJSON* tag = NULL;
    cJSON_ArrayForEach(tag, tags) {
        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;
        (void)add_tuple_to_sets_locked(k->valuestring, v->valuestring, is_public);
    }
}

static cJSON* fetch_latest_kind10000_event(void) {
    if (!g_block.cfg) return NULL;

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

    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10000));
    cJSON_AddItemToObject(filter, "kinds", kinds);
    cJSON_AddItemToArray(authors, cJSON_CreateString(g_block.cfg->keys.public_key_hex));
    cJSON_AddItemToObject(filter, "authors", authors);
    cJSON_AddNumberToObject(filter, "limit", 1);

    char* events_json = nostr_handler_query_json(filter, 8000);
    cJSON_Delete(filter);
    if (!events_json) return NULL;

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

    cJSON* ev0 = cJSON_GetArrayItem(arr, 0);
    cJSON* out = ev0 ? cJSON_Duplicate(ev0, 1) : NULL;
    cJSON_Delete(arr);
    return out;
}

static int decrypt_private_tags_array(const char* encrypted_content, cJSON** out_array) {
    if (!out_array) return -1;
    *out_array = NULL;

    if (!encrypted_content || encrypted_content[0] == '\0') {
        *out_array = cJSON_CreateArray();
        return *out_array ? 0 : -1;
    }

    size_t cap = strlen(encrypted_content) + 4096;
    char* plaintext = (char*)calloc(1, cap);
    if (!plaintext) return -1;

    int rc = nostr_nip44_decrypt(g_block.cfg->keys.private_key,
                                 g_block.cfg->keys.public_key,
                                 encrypted_content,
                                 plaintext,
                                 cap);
    if (rc != 0) {
        free(plaintext);
        return -1;
    }

    cJSON* arr = cJSON_Parse(plaintext);
    free(plaintext);
    if (!arr || !cJSON_IsArray(arr)) {
        cJSON_Delete(arr);
        return -1;
    }

    *out_array = arr;
    return 0;
}

static int encrypt_private_tags_array(cJSON* private_tags_array, char** out_content) {
    if (!private_tags_array || !out_content || !cJSON_IsArray(private_tags_array)) return -1;
    *out_content = NULL;

    char* plain = cJSON_PrintUnformatted(private_tags_array);
    if (!plain) return -1;

    size_t cap = strlen(plain) * 4 + 4096;
    char* cipher = (char*)calloc(1, cap);
    if (!cipher) {
        free(plain);
        return -1;
    }

    int rc = nostr_nip44_encrypt(g_block.cfg->keys.private_key,
                                 g_block.cfg->keys.public_key,
                                 plain,
                                 cipher,
                                 cap);
    free(plain);
    if (rc != 0) {
        free(cipher);
        return -1;
    }

    *out_content = cipher;
    return 0;
}

static int normalize_to_tuple(cJSON* tuple, char** out_k, char** out_v) {
    if (!tuple || !out_k || !out_v || !cJSON_IsArray(tuple) || cJSON_GetArraySize(tuple) < 2) return -1;
    cJSON* k = cJSON_GetArrayItem(tuple, 0);
    cJSON* v = cJSON_GetArrayItem(tuple, 1);
    if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) return -1;
    *out_k = k->valuestring;
    *out_v = v->valuestring;
    return 0;
}

static int tuple_equal(cJSON* a, cJSON* b) {
    char *ak = NULL, *av = NULL, *bk = NULL, *bv = NULL;
    if (normalize_to_tuple(a, &ak, &av) != 0) return 0;
    if (normalize_to_tuple(b, &bk, &bv) != 0) return 0;
    return strcmp(ak, bk) == 0 && strcmp(av, bv) == 0;
}

static int tags_contains_tuple(cJSON* tags, cJSON* tuple) {
    if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
    int n = cJSON_GetArraySize(tags);
    for (int i = 0; i < n; i++) {
        if (tuple_equal(cJSON_GetArrayItem(tags, i), tuple)) return 1;
    }
    return 0;
}

static int remove_tuple_all(cJSON* tags, cJSON* tuple) {
    if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
    int removed = 0;
    for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) {
        if (tuple_equal(cJSON_GetArrayItem(tags, i), tuple)) {
            cJSON_DeleteItemFromArray(tags, i);
            removed++;
        }
    }
    return removed;
}

static cJSON* build_tuple(const char* tag_key, const char* tag_value) {
    cJSON* tuple = cJSON_CreateArray();
    if (!tuple) return NULL;
    cJSON_AddItemToArray(tuple, cJSON_CreateString(tag_key ? tag_key : ""));
    cJSON_AddItemToArray(tuple, cJSON_CreateString(tag_value ? tag_value : ""));
    return tuple;
}

static int publish_kind10000(cJSON* public_tags, cJSON* private_tags, char* out_event_id64) {
    if (!public_tags || !private_tags || !cJSON_IsArray(public_tags) || !cJSON_IsArray(private_tags)) return -1;

    char* encrypted_content = NULL;
    if (encrypt_private_tags_array(private_tags, &encrypted_content) != 0) {
        return -1;
    }

    nostr_publish_result_t result;
    memset(&result, 0, sizeof(result));
    int rc = nostr_handler_publish_kind_event(10000, encrypted_content, public_tags, &result);
    free(encrypted_content);
    if (rc != 0) {
        nostr_handler_publish_result_free(&result);
        return -1;
    }

    if (out_event_id64 && result.event_id[0] != '\0') {
        snprintf(out_event_id64, 65, "%s", result.event_id);
    }

    nostr_handler_publish_result_free(&result);
    return 0;
}

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

    pthread_mutex_lock(&g_block.mutex);
    g_block.cfg = cfg;
    g_block.initialized = 1;
    pthread_mutex_unlock(&g_block.mutex);
    return 0;
}

int nostr_block_list_load_from_relays(void) {
    if (!g_block.initialized || !g_block.cfg) return -1;

    cJSON* ev = fetch_latest_kind10000_event();

    pthread_mutex_lock(&g_block.mutex);
    block_clear_sets_locked();

    if (!ev) {
        g_block.loaded = 1;
        pthread_mutex_unlock(&g_block.mutex);
        return 0;
    }

    cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
    parse_tags_into_sets_locked(tags, 1);

    cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content");
    if (content && cJSON_IsString(content) && content->valuestring && content->valuestring[0] != '\0') {
        cJSON* private_tags = NULL;
        if (decrypt_private_tags_array(content->valuestring, &private_tags) == 0) {
            parse_tags_into_sets_locked(private_tags, 0);
            cJSON_Delete(private_tags);
        } else {
            DEBUG_WARN("[didactyl] nostr_block_list: failed to decrypt kind 10000 private content");
        }
    }

    g_block.loaded = 1;
    pthread_mutex_unlock(&g_block.mutex);
    cJSON_Delete(ev);
    return 0;
}

int nostr_block_list_refresh(void) {
    return nostr_block_list_load_from_relays();
}

void nostr_block_list_cleanup(void) {
    pthread_mutex_lock(&g_block.mutex);
    block_clear_sets_locked();
    g_block.cfg = NULL;
    g_block.loaded = 0;
    g_block.initialized = 0;
    pthread_mutex_unlock(&g_block.mutex);
}

int nostr_block_list_is_pubkey_blocked(const char* pubkey_hex) {
    if (!pubkey_hex) return 0;
    pthread_mutex_lock(&g_block.mutex);
    int blocked = str_array_contains(g_block.pub_pubkeys, g_block.pub_pubkey_count, pubkey_hex) ||
                  str_array_contains(g_block.priv_pubkeys, g_block.priv_pubkey_count, pubkey_hex);
    pthread_mutex_unlock(&g_block.mutex);
    return blocked ? 1 : 0;
}

int nostr_block_list_is_event_blocked(const char* event_id_hex) {
    if (!event_id_hex) return 0;
    pthread_mutex_lock(&g_block.mutex);
    int blocked = str_array_contains(g_block.pub_event_ids, g_block.pub_event_id_count, event_id_hex) ||
                  str_array_contains(g_block.priv_event_ids, g_block.priv_event_id_count, event_id_hex);
    pthread_mutex_unlock(&g_block.mutex);
    return blocked ? 1 : 0;
}

int nostr_block_list_is_hashtag_blocked(const char* hashtag) {
    if (!hashtag) return 0;
    pthread_mutex_lock(&g_block.mutex);
    int blocked = str_array_contains(g_block.pub_hashtags, g_block.pub_hashtag_count, hashtag) ||
                  str_array_contains(g_block.priv_hashtags, g_block.priv_hashtag_count, hashtag);
    pthread_mutex_unlock(&g_block.mutex);
    return blocked ? 1 : 0;
}

int nostr_block_list_is_event_filtered(cJSON* event) {
    if (!event || !cJSON_IsObject(event)) return 0;

    cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id");
    if (id && cJSON_IsString(id) && id->valuestring && nostr_block_list_is_event_blocked(id->valuestring)) {
        return 1;
    }

    cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
    if (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring && nostr_block_list_is_pubkey_blocked(pubkey->valuestring)) {
        return 1;
    }

    cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
    if (tags && cJSON_IsArray(tags)) {
        cJSON* tag = NULL;
        cJSON_ArrayForEach(tag, tags) {
            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, "t") == 0 && nostr_block_list_is_hashtag_blocked(v->valuestring)) {
                return 1;
            }
        }
    }

    return 0;
}

static int mutate_block_list(const char* tag_key, const char* tag_value, int add_op, int is_public) {
    if (!g_block.initialized || !g_block.cfg || !tag_key || !tag_value) return -1;
    if (!(strcmp(tag_key, "p") == 0 || strcmp(tag_key, "e") == 0 || strcmp(tag_key, "t") == 0)) return -1;

    cJSON* ev = fetch_latest_kind10000_event();
    cJSON* public_tags = cJSON_CreateArray();
    cJSON* private_tags = cJSON_CreateArray();
    if (!public_tags || !private_tags) {
        cJSON_Delete(ev);
        cJSON_Delete(public_tags);
        cJSON_Delete(private_tags);
        return -1;
    }

    if (ev && cJSON_IsObject(ev)) {
        cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
        if (tags && cJSON_IsArray(tags)) {
            cJSON_Delete(public_tags);
            public_tags = cJSON_Duplicate(tags, 1);
            if (!public_tags) {
                cJSON_Delete(ev);
                cJSON_Delete(private_tags);
                return -1;
            }
        }

        cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content");
        if (content && cJSON_IsString(content) && content->valuestring && content->valuestring[0] != '\0') {
            cJSON_Delete(private_tags);
            if (decrypt_private_tags_array(content->valuestring, &private_tags) != 0) {
                private_tags = cJSON_CreateArray();
            }
            if (!private_tags) {
                cJSON_Delete(ev);
                cJSON_Delete(public_tags);
                return -1;
            }
        }
    }

    cJSON* tuple = build_tuple(tag_key, tag_value);
    if (!tuple) {
        cJSON_Delete(ev);
        cJSON_Delete(public_tags);
        cJSON_Delete(private_tags);
        return -1;
    }

    cJSON* target = is_public ? public_tags : private_tags;
    cJSON* other = is_public ? private_tags : public_tags;

    if (add_op) {
        if (!tags_contains_tuple(target, tuple)) {
            cJSON* dup = cJSON_Duplicate(tuple, 1);
            if (!dup) {
                cJSON_Delete(tuple);
                cJSON_Delete(ev);
                cJSON_Delete(public_tags);
                cJSON_Delete(private_tags);
                return -1;
            }
            cJSON_AddItemToArray(target, dup);
        }
    } else {
        (void)remove_tuple_all(target, tuple);
        (void)remove_tuple_all(other, tuple);
    }

    cJSON_Delete(tuple);

    int rc = publish_kind10000(public_tags, private_tags, NULL);
    cJSON_Delete(ev);
    cJSON_Delete(public_tags);
    cJSON_Delete(private_tags);
    if (rc != 0) return -1;

    return nostr_block_list_refresh();
}

int nostr_block_list_add(const char* tag_key, const char* tag_value, int is_public) {
    return mutate_block_list(tag_key, tag_value, 1, is_public ? 1 : 0);
}

int nostr_block_list_remove(const char* tag_key, const char* tag_value) {
    return mutate_block_list(tag_key, tag_value, 0, 0);
}

char* nostr_block_list_json(void) {
    cJSON* root = cJSON_CreateObject();
    cJSON* blocked_pubkeys = cJSON_CreateArray();
    cJSON* blocked_events = cJSON_CreateArray();
    cJSON* blocked_hashtags = cJSON_CreateArray();
    if (!root || !blocked_pubkeys || !blocked_events || !blocked_hashtags) {
        cJSON_Delete(root);
        cJSON_Delete(blocked_pubkeys);
        cJSON_Delete(blocked_events);
        cJSON_Delete(blocked_hashtags);
        return NULL;
    }

    pthread_mutex_lock(&g_block.mutex);

    for (int i = 0; i < g_block.pub_pubkey_count; i++) {
        cJSON_AddItemToArray(blocked_pubkeys, cJSON_CreateString(g_block.pub_pubkeys[i]));
    }
    for (int i = 0; i < g_block.priv_pubkey_count; i++) {
        cJSON_AddItemToArray(blocked_pubkeys, cJSON_CreateString(g_block.priv_pubkeys[i]));
    }

    for (int i = 0; i < g_block.pub_event_id_count; i++) {
        cJSON_AddItemToArray(blocked_events, cJSON_CreateString(g_block.pub_event_ids[i]));
    }
    for (int i = 0; i < g_block.priv_event_id_count; i++) {
        cJSON_AddItemToArray(blocked_events, cJSON_CreateString(g_block.priv_event_ids[i]));
    }

    for (int i = 0; i < g_block.pub_hashtag_count; i++) {
        cJSON_AddItemToArray(blocked_hashtags, cJSON_CreateString(g_block.pub_hashtags[i]));
    }
    for (int i = 0; i < g_block.priv_hashtag_count; i++) {
        cJSON_AddItemToArray(blocked_hashtags, cJSON_CreateString(g_block.priv_hashtags[i]));
    }

    int total = g_block.pub_pubkey_count + g_block.priv_pubkey_count +
                g_block.pub_event_id_count + g_block.priv_event_id_count +
                g_block.pub_hashtag_count + g_block.priv_hashtag_count;

    cJSON_AddBoolToObject(root, "success", 1);
    cJSON_AddBoolToObject(root, "loaded", g_block.loaded ? 1 : 0);
    cJSON_AddItemToObject(root, "blocked_pubkeys", blocked_pubkeys);
    cJSON_AddItemToObject(root, "blocked_events", blocked_events);
    cJSON_AddItemToObject(root, "blocked_hashtags", blocked_hashtags);
    cJSON_AddNumberToObject(root, "total_entries", total);

    pthread_mutex_unlock(&g_block.mutex);

    char* out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    return out;
}
