#define _GNU_SOURCE

#include "auth_envelope.h"

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

#include <cJSON.h>
#include <nostr_core/nip001.h>
#include <nostr_core/utils.h>

static int set_error(int *out_code,
                     const char **out_message,
                     int code,
                     const char *message) {
    if (out_code != NULL) {
        *out_code = code;
    }
    if (out_message != NULL) {
        *out_message = message;
    }
    return -1;
}

void auth_nonce_cache_init(auth_nonce_cache_t *cache) {
    if (cache == NULL) {
        return;
    }
    memset(cache, 0, sizeof(*cache));
}

static int auth_nonce_cache_contains(const auth_nonce_cache_t *cache, const uint8_t id[32]) {
    int i;

    if (cache == NULL || id == NULL) {
        return 0;
    }

    for (i = 0; i < cache->count; ++i) {
        if (memcmp(cache->ids[i], id, 32) == 0) {
            return 1;
        }
    }

    return 0;
}

static void auth_nonce_cache_insert(auth_nonce_cache_t *cache, const uint8_t id[32]) {
    if (cache == NULL || id == NULL) {
        return;
    }

    if (cache->count < AUTH_NONCE_CACHE_SIZE) {
        memcpy(cache->ids[cache->count], id, 32);
        cache->count++;
        return;
    }

    memcpy(cache->ids[cache->next], id, 32);
    cache->next = (cache->next + 1) % AUTH_NONCE_CACHE_SIZE;
}

static int json_item_to_compact_string(const cJSON *item, char *out, size_t out_sz) {
    char *printed;

    if (out == NULL || out_sz == 0 || item == NULL) {
        return -1;
    }

    if (cJSON_IsString(item) && item->valuestring != NULL) {
        strncpy(out, item->valuestring, out_sz - 1);
        out[out_sz - 1] = '\0';
        return 0;
    }

    printed = cJSON_PrintUnformatted((cJSON *)item);
    if (printed == NULL) {
        return -1;
    }

    strncpy(out, printed, out_sz - 1);
    out[out_sz - 1] = '\0';
    free(printed);
    return 0;
}

static int compute_hash_hex_for_json_item(const cJSON *item, char *out_hex, size_t out_hex_sz) {
    unsigned char hash[32];
    char *compact = NULL;

    if (out_hex == NULL || out_hex_sz < 65) {
        return -1;
    }

    if (item == NULL) {
        compact = strdup("null");
    } else {
        compact = cJSON_PrintUnformatted((cJSON *)item);
    }

    if (compact == NULL) {
        return -1;
    }

    if (nostr_sha256((const unsigned char *)compact, strlen(compact), hash) != 0) {
        free(compact);
        return -1;
    }

    nostr_bytes_to_hex(hash, sizeof(hash), out_hex);
    out_hex[64] = '\0';
    free(compact);
    return 0;
}

static int compute_params_hash_hex(cJSON *root, char *out_hex, size_t out_hex_sz) {
    cJSON *params = cJSON_GetObjectItemCaseSensitive(root, "params");
    return compute_hash_hex_for_json_item(params, out_hex, out_hex_sz);
}

static cJSON *find_tag_value(cJSON *tags, const char *name) {
    int i;

    if (!cJSON_IsArray(tags) || name == NULL) {
        return NULL;
    }

    for (i = 0; i < cJSON_GetArraySize(tags); ++i) {
        cJSON *tag = cJSON_GetArrayItem(tags, i);
        cJSON *tag_name;
        cJSON *tag_value;

        if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
            continue;
        }

        tag_name = cJSON_GetArrayItem(tag, 0);
        tag_value = cJSON_GetArrayItem(tag, 1);
        if (!cJSON_IsString(tag_name) || tag_name->valuestring == NULL) {
            continue;
        }
        if (strcmp(tag_name->valuestring, name) != 0) {
            continue;
        }

        return tag_value;
    }

    return NULL;
}

static cJSON *build_tag_pair(const char *name, const char *value) {
    cJSON *pair;

    if (name == NULL || value == NULL) {
        return NULL;
    }

    pair = cJSON_CreateArray();
    if (pair == NULL) {
        return NULL;
    }

    cJSON_AddItemToArray(pair, cJSON_CreateString(name));
    cJSON_AddItemToArray(pair, cJSON_CreateString(value));
    return pair;
}

int auth_envelope_build_for_request(const char *request_id,
                                    const char *method,
                                    const cJSON *params,
                                    const unsigned char privkey[32],
                                    const char *label,
                                    time_t created_at,
                                    cJSON **out_auth) {
    cJSON *tags = NULL;
    cJSON *auth = NULL;
    char body_hash_hex[65];

    if (out_auth == NULL || request_id == NULL || method == NULL ||
        privkey == NULL || request_id[0] == '\0' || method[0] == '\0') {
        return -1;
    }

    *out_auth = NULL;

    if (compute_hash_hex_for_json_item(params, body_hash_hex, sizeof(body_hash_hex)) != 0) {
        return -1;
    }

    tags = cJSON_CreateArray();
    if (tags == NULL) {
        return -1;
    }

    cJSON_AddItemToArray(tags, build_tag_pair("nsigner_rpc", request_id));
    cJSON_AddItemToArray(tags, build_tag_pair("nsigner_method", method));
    cJSON_AddItemToArray(tags, build_tag_pair("nsigner_body_hash", body_hash_hex));

    if (created_at <= 0) {
        created_at = time(NULL);
    }

    auth = nostr_create_and_sign_event(AUTH_EVENT_KIND,
                                       (label != NULL) ? label : "",
                                       tags,
                                       privkey,
                                       created_at);

    if (auth == NULL) {
        cJSON_Delete(tags);
        return -1;
    }

    *out_auth = auth;
    return 0;
}

int auth_envelope_verify_request(const char *request_json,
                                 auth_nonce_cache_t *cache,
                                 int skew_seconds,
                                 char *out_pubkey_hex,
                                 size_t out_pubkey_hex_sz,
                                 char *out_label,
                                 size_t out_label_sz,
                                 int *out_error_code,
                                 const char **out_error_message) {
    cJSON *root = NULL;
    cJSON *auth = NULL;
    cJSON *method_item;
    cJSON *id_item;
    cJSON *kind_item;
    cJSON *created_item;
    cJSON *pubkey_item;
    cJSON *content_item;
    cJSON *id_hex_item;
    cJSON *tags_item;
    cJSON *tag_rpc;
    cJSON *tag_method;
    cJSON *tag_body_hash;
    char request_id[128];
    char body_hash_hex[65];
    uint8_t nonce_bytes[32];
    time_t now;
    long long created;

    if (out_error_code != NULL) {
        *out_error_code = 0;
    }
    if (out_error_message != NULL) {
        *out_error_message = NULL;
    }

    if (out_pubkey_hex == NULL || out_pubkey_hex_sz < 65 ||
        out_label == NULL || out_label_sz == 0 ||
        request_json == NULL || cache == NULL) {
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MALFORMED,
                         "auth_envelope_malformed");
    }

    out_pubkey_hex[0] = '\0';
    out_label[0] = '\0';

    root = cJSON_Parse(request_json);
    if (root == NULL) {
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MALFORMED,
                         "auth_envelope_malformed");
    }

    method_item = cJSON_GetObjectItemCaseSensitive(root, "method");
    id_item = cJSON_GetObjectItemCaseSensitive(root, "id");
    if (!cJSON_IsString(method_item) || method_item->valuestring == NULL ||
        json_item_to_compact_string(id_item, request_id, sizeof(request_id)) != 0) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MALFORMED,
                         "auth_envelope_malformed");
    }

    auth = cJSON_GetObjectItemCaseSensitive(root, "auth");
    if (!cJSON_IsObject(auth)) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_REQUIRED,
                         "auth_envelope_required");
    }

    if (nostr_validate_event_structure(auth) != 0) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MALFORMED,
                         "auth_envelope_malformed");
    }

    if (nostr_verify_event_signature(auth) != 0) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_SIGNATURE_INVALID,
                         "auth_signature_invalid");
    }

    kind_item = cJSON_GetObjectItemCaseSensitive(auth, "kind");
    if (!cJSON_IsNumber(kind_item) || kind_item->valueint != AUTH_EVENT_KIND) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_KIND_INVALID,
                         "auth_kind_invalid");
    }

    tags_item = cJSON_GetObjectItemCaseSensitive(auth, "tags");
    tag_rpc = find_tag_value(tags_item, "nsigner_rpc");
    tag_method = find_tag_value(tags_item, "nsigner_method");
    tag_body_hash = find_tag_value(tags_item, "nsigner_body_hash");
    if (!cJSON_IsString(tag_rpc) || tag_rpc->valuestring == NULL ||
        !cJSON_IsString(tag_method) || tag_method->valuestring == NULL ||
        !cJSON_IsString(tag_body_hash) || tag_body_hash->valuestring == NULL) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MALFORMED,
                         "auth_envelope_malformed");
    }

    if (strcmp(tag_rpc->valuestring, request_id) != 0 ||
        strcmp(tag_method->valuestring, method_item->valuestring) != 0) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MISMATCH,
                         "auth_envelope_mismatch");
    }

    if (compute_params_hash_hex(root, body_hash_hex, sizeof(body_hash_hex)) != 0 ||
        strcasecmp(tag_body_hash->valuestring, body_hash_hex) != 0) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_BODY_MISMATCH,
                         "auth_body_mismatch");
    }

    created_item = cJSON_GetObjectItemCaseSensitive(auth, "created_at");
    if (!cJSON_IsNumber(created_item)) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MALFORMED,
                         "auth_envelope_malformed");
    }

    if (skew_seconds <= 0) {
        skew_seconds = AUTH_DEFAULT_SKEW_SECONDS;
    }

    now = time(NULL);
    created = (long long)created_item->valuedouble;
    if (llabs((long long)now - created) > (long long)skew_seconds) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_STALE,
                         "auth_envelope_stale");
    }

    id_hex_item = cJSON_GetObjectItemCaseSensitive(auth, "id");
    if (!cJSON_IsString(id_hex_item) || id_hex_item->valuestring == NULL ||
        strlen(id_hex_item->valuestring) != 64 ||
        nostr_hex_to_bytes(id_hex_item->valuestring, nonce_bytes, sizeof(nonce_bytes)) != 0) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MALFORMED,
                         "auth_envelope_malformed");
    }

    if (auth_nonce_cache_contains(cache, nonce_bytes)) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_REPLAY_DETECTED,
                         "auth_replay_detected");
    }
    auth_nonce_cache_insert(cache, nonce_bytes);

    pubkey_item = cJSON_GetObjectItemCaseSensitive(auth, "pubkey");
    if (!cJSON_IsString(pubkey_item) || pubkey_item->valuestring == NULL ||
        strlen(pubkey_item->valuestring) != 64) {
        cJSON_Delete(root);
        return set_error(out_error_code, out_error_message,
                         AUTH_ERR_ENVELOPE_MALFORMED,
                         "auth_envelope_malformed");
    }

    strncpy(out_pubkey_hex, pubkey_item->valuestring, out_pubkey_hex_sz - 1);
    out_pubkey_hex[out_pubkey_hex_sz - 1] = '\0';

    content_item = cJSON_GetObjectItemCaseSensitive(auth, "content");
    if (cJSON_IsString(content_item) && content_item->valuestring != NULL) {
        size_t i;
        for (i = 0; content_item->valuestring[i] != '\0' && i + 1 < out_label_sz; ++i) {
            unsigned char ch = (unsigned char)content_item->valuestring[i];
            out_label[i] = (char)((isprint(ch) && ch != '\n' && ch != '\r' && ch != '\t') ? ch : ' ');
        }
        out_label[i] = '\0';
    } else {
        out_label[0] = '\0';
    }

    cJSON_Delete(root);
    return 0;
}
