#include "sentry_envelope.h"
#include "sentry_alloc.h"
#include "sentry_core.h"
#include "sentry_json.h"
#include "sentry_options.h"
#include "sentry_path.h"
#include "sentry_ratelimiter.h"
#include "sentry_string.h"
#include "sentry_transport.h"
#include "sentry_value.h"
#include <string.h>

struct sentry_envelope_item_s {
    sentry_value_t headers;
    sentry_value_t event;
    char *payload;
    size_t payload_len;
};

struct sentry_envelope_s {
    bool is_raw;
    union {
        struct {
            sentry_value_t headers;
            sentry_envelope_item_t items[SENTRY_MAX_ENVELOPE_ITEMS];
            size_t item_count;
        } items;
        struct {
            char *payload;
            size_t payload_len;
        } raw;
    } contents;
};

static sentry_envelope_item_t *
envelope_add_item(sentry_envelope_t *envelope)
{
    if (envelope->is_raw) {
        return NULL;
    }
    if (envelope->contents.items.item_count >= SENTRY_MAX_ENVELOPE_ITEMS) {
        return NULL;
    }
    // TODO: Envelopes may have at most one event item or one transaction item,
    // and not one of both. Some checking should be done here or in
    // `sentry__envelope_add_[transaction|event]` to ensure this can't happen.

    sentry_envelope_item_t *rv
        = &envelope->contents.items
               .items[envelope->contents.items.item_count++];
    rv->headers = sentry_value_new_object();
    rv->event = sentry_value_new_null();
    rv->payload = NULL;
    rv->payload_len = 0;
    return rv;
}

static void
envelope_item_cleanup(sentry_envelope_item_t *item)
{
    sentry_value_decref(item->headers);
    sentry_value_decref(item->event);
    sentry_free(item->payload);
}

void
sentry__envelope_item_set_header(
    sentry_envelope_item_t *item, const char *key, sentry_value_t value)
{
    sentry_value_set_by_key(item->headers, key, value);
}

static int
envelope_item_get_ratelimiter_category(const sentry_envelope_item_t *item)
{
    const char *ty = sentry_value_as_string(
        sentry_value_get_by_key(item->headers, "type"));
    if (sentry__string_eq(ty, "session")) {
        return SENTRY_RL_CATEGORY_SESSION;
    } else if (sentry__string_eq(ty, "transaction")) {
        return SENTRY_RL_CATEGORY_TRANSACTION;
    }
    // NOTE: the `type` here can be `event` or `attachment`.
    // Ideally, attachments should have their own RL_CATEGORY.
    return SENTRY_RL_CATEGORY_ERROR;
}

static sentry_envelope_item_t *
envelope_add_from_owned_buffer(
    sentry_envelope_t *envelope, char *buf, size_t buf_len, const char *type)
{
    if (!buf) {
        return NULL;
    }
    sentry_envelope_item_t *item = envelope_add_item(envelope);
    if (!item) {
        sentry_free(buf);
        return NULL;
    }

    item->payload = buf;
    item->payload_len = buf_len;
    sentry_value_t length = sentry_value_new_int32((int32_t)buf_len);
    sentry__envelope_item_set_header(
        item, "type", sentry_value_new_string(type));
    sentry__envelope_item_set_header(item, "length", length);

    return item;
}

void
sentry_envelope_free(sentry_envelope_t *envelope)
{
    if (!envelope) {
        return;
    }
    if (envelope->is_raw) {
        sentry_free(envelope->contents.raw.payload);
        sentry_free(envelope);
        return;
    }
    sentry_value_decref(envelope->contents.items.headers);
    for (size_t i = 0; i < envelope->contents.items.item_count; i++) {
        envelope_item_cleanup(&envelope->contents.items.items[i]);
    }
    sentry_free(envelope);
}

static void
sentry__envelope_set_header(
    sentry_envelope_t *envelope, const char *key, sentry_value_t value)
{
    if (envelope->is_raw) {
        return;
    }
    sentry_value_set_by_key(envelope->contents.items.headers, key, value);
}

sentry_envelope_t *
sentry__envelope_new(void)
{
    sentry_envelope_t *rv = SENTRY_MAKE(sentry_envelope_t);
    if (!rv) {
        return NULL;
    }

    rv->is_raw = false;
    rv->contents.items.item_count = 0;
    rv->contents.items.headers = sentry_value_new_object();

    SENTRY_WITH_OPTIONS (options) {
        if (options->dsn && options->dsn->is_valid) {
            sentry__envelope_set_header(rv, "dsn",
                sentry_value_new_string(sentry_options_get_dsn(options)));
        }
    }

    return rv;
}

sentry_envelope_t *
sentry__envelope_from_path(const sentry_path_t *path)
{
    size_t buf_len;
    char *buf = sentry__path_read_to_buffer(path, &buf_len);
    if (!buf) {
        SENTRY_WARNF("failed to read raw envelope from \"%" SENTRY_PATH_PRI
                     "\"",
            path->path);
        return NULL;
    }

    sentry_envelope_t *envelope = SENTRY_MAKE(sentry_envelope_t);
    if (!envelope) {
        sentry_free(buf);
        return NULL;
    }

    envelope->is_raw = true;
    envelope->contents.raw.payload = buf;
    envelope->contents.raw.payload_len = buf_len;

    return envelope;
}

sentry_uuid_t
sentry__envelope_get_event_id(const sentry_envelope_t *envelope)
{
    if (envelope->is_raw) {
        return sentry_uuid_nil();
    }
    return sentry_uuid_from_string(sentry_value_as_string(
        sentry_value_get_by_key(envelope->contents.items.headers, "event_id")));
}

sentry_value_t
sentry_envelope_get_event(const sentry_envelope_t *envelope)
{
    if (envelope->is_raw) {
        return sentry_value_new_null();
    }
    for (size_t i = 0; i < envelope->contents.items.item_count; i++) {

        if (!sentry_value_is_null(envelope->contents.items.items[i].event)
            && !sentry__event_is_transaction(
                envelope->contents.items.items[i].event)) {
            return envelope->contents.items.items[i].event;
        }
    }
    return sentry_value_new_null();
}

sentry_value_t
sentry_envelope_get_transaction(const sentry_envelope_t *envelope)
{
    if (envelope->is_raw) {
        return sentry_value_new_null();
    }
    for (size_t i = 0; i < envelope->contents.items.item_count; i++) {
        if (!sentry_value_is_null(envelope->contents.items.items[i].event)
            && sentry__event_is_transaction(
                envelope->contents.items.items[i].event)) {
            return envelope->contents.items.items[i].event;
        }
    }
    return sentry_value_new_null();
}

sentry_envelope_item_t *
sentry__envelope_add_event(sentry_envelope_t *envelope, sentry_value_t event)
{
    sentry_envelope_item_t *item = envelope_add_item(envelope);
    if (!item) {
        return NULL;
    }

    sentry_jsonwriter_t *jw = sentry__jsonwriter_new(NULL);
    if (!jw) {
        return NULL;
    }

    sentry_value_t event_id = sentry__ensure_event_id(event, NULL);

    item->event = event;
    sentry__jsonwriter_write_value(jw, event);
    item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len);

    sentry__envelope_item_set_header(
        item, "type", sentry_value_new_string("event"));
    sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len);
    sentry__envelope_item_set_header(item, "length", length);

    sentry_value_incref(event_id);
    sentry__envelope_set_header(envelope, "event_id", event_id);

    return item;
}

sentry_envelope_item_t *
sentry__envelope_add_transaction(
    sentry_envelope_t *envelope, sentry_value_t transaction)
{
    sentry_envelope_item_t *item = envelope_add_item(envelope);
    if (!item) {
        return NULL;
    }

    sentry_jsonwriter_t *jw = sentry__jsonwriter_new(NULL);
    if (!jw) {
        return NULL;
    }

    sentry_value_t event_id = sentry__ensure_event_id(transaction, NULL);

    item->event = transaction;
    sentry__jsonwriter_write_value(jw, transaction);
    item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len);

    sentry__envelope_item_set_header(
        item, "type", sentry_value_new_string("transaction"));
    sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len);
    sentry__envelope_item_set_header(item, "length", length);

    sentry_value_incref(event_id);
    sentry__envelope_set_header(envelope, "event_id", event_id);

#ifdef SENTRY_UNITTEST
    sentry_value_t now = sentry_value_new_string("2021-12-16T05:53:59.343Z");
#else
    sentry_value_t now = sentry__value_new_string_owned(
        sentry__msec_time_to_iso8601(sentry__msec_time()));
#endif
    sentry__envelope_set_header(envelope, "sent_at", now);

    return item;
}

sentry_envelope_item_t *
sentry__envelope_add_session(
    sentry_envelope_t *envelope, const sentry_session_t *session)
{
    if (!envelope || !session) {
        return NULL;
    }
    sentry_jsonwriter_t *jw = sentry__jsonwriter_new(NULL);
    if (!jw) {
        return NULL;
    }
    sentry__session_to_json(session, jw);
    size_t payload_len = 0;
    char *payload = sentry__jsonwriter_into_string(jw, &payload_len);

    // NOTE: function will check for `payload` internally and free it on error
    return envelope_add_from_owned_buffer(
        envelope, payload, payload_len, "session");
}

sentry_envelope_item_t *
sentry__envelope_add_from_buffer(sentry_envelope_t *envelope, const char *buf,
    size_t buf_len, const char *type)
{
    // NOTE: function will check for the clone of `buf` internally and free it
    // on error
    return envelope_add_from_owned_buffer(
        envelope, sentry__string_clone_n(buf, buf_len), buf_len, type);
}

sentry_envelope_item_t *
sentry__envelope_add_from_path(
    sentry_envelope_t *envelope, const sentry_path_t *path, const char *type)
{
    if (!envelope) {
        return NULL;
    }
    size_t buf_len;
    char *buf = sentry__path_read_to_buffer(path, &buf_len);
    if (!buf) {
        SENTRY_WARNF("failed to read envelope item from \"%" SENTRY_PATH_PRI
                     "\"",
            path->path);
        return NULL;
    }
    // NOTE: function will free `buf` on error
    return envelope_add_from_owned_buffer(envelope, buf, buf_len, type);
}

static void
sentry__envelope_serialize_headers_into_stringbuilder(
    const sentry_envelope_t *envelope, sentry_stringbuilder_t *sb)
{
    sentry_jsonwriter_t *jw = sentry__jsonwriter_new(sb);
    if (jw) {
        sentry__jsonwriter_write_value(jw, envelope->contents.items.headers);
        sentry__jsonwriter_free(jw);
    }
}

static void
sentry__envelope_serialize_item_into_stringbuilder(
    const sentry_envelope_item_t *item, sentry_stringbuilder_t *sb)
{
    sentry_jsonwriter_t *jw = sentry__jsonwriter_new(sb);
    if (!jw) {
        return;
    }
    sentry__stringbuilder_append_char(sb, '\n');

    sentry__jsonwriter_write_value(jw, item->headers);
    sentry__jsonwriter_free(jw);

    sentry__stringbuilder_append_char(sb, '\n');

    sentry__stringbuilder_append_buf(sb, item->payload, item->payload_len);
}

void
sentry__envelope_serialize_into_stringbuilder(
    const sentry_envelope_t *envelope, sentry_stringbuilder_t *sb)
{
    if (envelope->is_raw) {
        sentry__stringbuilder_append_buf(sb, envelope->contents.raw.payload,
            envelope->contents.raw.payload_len);
        return;
    }

    SENTRY_TRACE("serializing envelope into buffer");
    sentry__envelope_serialize_headers_into_stringbuilder(envelope, sb);

    for (size_t i = 0; i < envelope->contents.items.item_count; i++) {
        const sentry_envelope_item_t *item = &envelope->contents.items.items[i];
        sentry__envelope_serialize_item_into_stringbuilder(item, sb);
    }
}

char *
sentry_envelope_serialize_ratelimited(const sentry_envelope_t *envelope,
    const sentry_rate_limiter_t *rl, size_t *size_out, bool *owned_out)
{
    if (envelope->is_raw) {
        *size_out = envelope->contents.raw.payload_len;
        *owned_out = false;
        return envelope->contents.raw.payload;
    }
    *owned_out = true;

    sentry_stringbuilder_t sb;
    sentry__stringbuilder_init(&sb);
    sentry__envelope_serialize_headers_into_stringbuilder(envelope, &sb);

    size_t serialized_items = 0;
    for (size_t i = 0; i < envelope->contents.items.item_count; i++) {
        const sentry_envelope_item_t *item = &envelope->contents.items.items[i];
        if (rl) {
            int category = envelope_item_get_ratelimiter_category(item);
            if (sentry__rate_limiter_is_disabled(rl, category)) {
                continue;
            }
        }
        sentry__envelope_serialize_item_into_stringbuilder(item, &sb);
        serialized_items += 1;
    }

    if (!serialized_items) {
        sentry__stringbuilder_cleanup(&sb);
        *size_out = 0;
        return NULL;
    }

    *size_out = sentry__stringbuilder_len(&sb);
    return sentry__stringbuilder_into_string(&sb);
}

char *
sentry_envelope_serialize(const sentry_envelope_t *envelope, size_t *size_out)
{
    sentry_stringbuilder_t sb;
    sentry__stringbuilder_init(&sb);

    sentry__envelope_serialize_into_stringbuilder(envelope, &sb);

    *size_out = sentry__stringbuilder_len(&sb);
    return sentry__stringbuilder_into_string(&sb);
}

MUST_USE int
sentry_envelope_write_to_path(
    const sentry_envelope_t *envelope, const sentry_path_t *path)
{
    // TODO: This currently builds the whole buffer in-memory.
    // It would be nice to actually stream this to a file.
    size_t buf_len = 0;
    char *buf = sentry_envelope_serialize(envelope, &buf_len);

    int rv = sentry__path_write_buffer(path, buf, buf_len);

    sentry_free(buf);

    return rv;
}

int
sentry_envelope_write_to_file_n(
    const sentry_envelope_t *envelope, const char *path, size_t path_len)
{
    if (!envelope || !path) {
        return 1;
    }
    sentry_path_t *path_obj = sentry__path_from_str_n(path, path_len);

    int rv = sentry_envelope_write_to_path(envelope, path_obj);

    sentry__path_free(path_obj);

    return rv;
}

int
sentry_envelope_write_to_file(
    const sentry_envelope_t *envelope, const char *path)
{
    if (!envelope || !path) {
        return 1;
    }

    return sentry_envelope_write_to_file_n(envelope, path, strlen(path));
}

#ifdef SENTRY_UNITTEST
size_t
sentry__envelope_get_item_count(const sentry_envelope_t *envelope)
{
    return envelope->is_raw ? 0 : envelope->contents.items.item_count;
}

const sentry_envelope_item_t *
sentry__envelope_get_item(const sentry_envelope_t *envelope, size_t idx)
{
    return !envelope->is_raw && idx < envelope->contents.items.item_count
        ? &envelope->contents.items.items[idx]
        : NULL;
}

sentry_value_t
sentry__envelope_item_get_header(
    const sentry_envelope_item_t *item, const char *key)
{
    return sentry_value_get_by_key(item->headers, key);
}

const char *
sentry__envelope_item_get_payload(
    const sentry_envelope_item_t *item, size_t *payload_len_out)
{
    if (payload_len_out) {
        *payload_len_out = item->payload_len;
    }
    return item->payload;
}
#endif
