// SPDX-FileCopyrightText: 2024 Redict Contributors
// SPDX-FileCopyrightText: 2024 Salvatore Sanfilippo <antirez at gmail dot com>
//
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-License-Identifier: LGPL-3.0-only

#include "redictmodule.h"

#include <string.h>
#include <strings.h>
#include <assert.h>
#include <unistd.h>
#include <errno.h>

/* Command which adds a stream entry with automatic ID, like XADD *.
 *
 * Syntax: STREAM.ADD key field1 value1 [ field2 value2 ... ]
 *
 * The response is the ID of the added stream entry or an error message.
 */
int stream_add(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    if (argc < 2 || argc % 2 != 0) {
        RedictModule_WrongArity(ctx);
        return REDICTMODULE_OK;
    }

    RedictModuleKey *key = RedictModule_OpenKey(ctx, argv[1], REDICTMODULE_WRITE);
    RedictModuleStreamID id;
    if (RedictModule_StreamAdd(key, REDICTMODULE_STREAM_ADD_AUTOID, &id,
                              &argv[2], (argc-2)/2) == REDICTMODULE_OK) {
        RedictModuleString *id_str = RedictModule_CreateStringFromStreamID(ctx, &id);
        RedictModule_ReplyWithString(ctx, id_str);
        RedictModule_FreeString(ctx, id_str);
    } else {
        RedictModule_ReplyWithError(ctx, "ERR StreamAdd failed");
    }
    RedictModule_CloseKey(key);
    return REDICTMODULE_OK;
}

/* Command which adds a stream entry N times.
 *
 * Syntax: STREAM.ADD key N field1 value1 [ field2 value2 ... ]
 *
 * Returns the number of successfully added entries.
 */
int stream_addn(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    if (argc < 3 || argc % 2 == 0) {
        RedictModule_WrongArity(ctx);
        return REDICTMODULE_OK;
    }

    long long n, i;
    if (RedictModule_StringToLongLong(argv[2], &n) == REDICTMODULE_ERR) {
        RedictModule_ReplyWithError(ctx, "N must be a number");
        return REDICTMODULE_OK;
    }

    RedictModuleKey *key = RedictModule_OpenKey(ctx, argv[1], REDICTMODULE_WRITE);
    for (i = 0; i < n; i++) {
        if (RedictModule_StreamAdd(key, REDICTMODULE_STREAM_ADD_AUTOID, NULL,
                                  &argv[3], (argc-3)/2) == REDICTMODULE_ERR)
            break;
    }
    RedictModule_ReplyWithLongLong(ctx, i);
    RedictModule_CloseKey(key);
    return REDICTMODULE_OK;
}

/* STREAM.DELETE key stream-id */
int stream_delete(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    if (argc != 3) return RedictModule_WrongArity(ctx);
    RedictModuleStreamID id;
    if (RedictModule_StringToStreamID(argv[2], &id) != REDICTMODULE_OK) {
        return RedictModule_ReplyWithError(ctx, "Invalid stream ID");
    }
    RedictModuleKey *key = RedictModule_OpenKey(ctx, argv[1], REDICTMODULE_WRITE);
    if (RedictModule_StreamDelete(key, &id) == REDICTMODULE_OK) {
        RedictModule_ReplyWithSimpleString(ctx, "OK");
    } else {
        RedictModule_ReplyWithError(ctx, "ERR StreamDelete failed");
    }
    RedictModule_CloseKey(key);
    return REDICTMODULE_OK;
}

/* STREAM.RANGE key start-id end-id
 *
 * Returns an array of stream items. Each item is an array on the form
 * [stream-id, [field1, value1, field2, value2, ...]].
 *
 * A funny side-effect used for testing RM_StreamIteratorDelete() is that if any
 * entry has a field named "selfdestruct", the stream entry is deleted. It is
 * however included in the results of this command.
 */
int stream_range(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    if (argc != 4) {
        RedictModule_WrongArity(ctx);
        return REDICTMODULE_OK;
    }

    RedictModuleStreamID startid, endid;
    if (RedictModule_StringToStreamID(argv[2], &startid) != REDICTMODULE_OK ||
        RedictModule_StringToStreamID(argv[3], &endid) != REDICTMODULE_OK) {
        RedictModule_ReplyWithError(ctx, "Invalid stream ID");
        return REDICTMODULE_OK;
    }

    /* If startid > endid, we swap and set the reverse flag. */
    int flags = 0;
    if (startid.ms > endid.ms ||
        (startid.ms == endid.ms && startid.seq > endid.seq)) {
        RedictModuleStreamID tmp = startid;
        startid = endid;
        endid = tmp;
        flags |= REDICTMODULE_STREAM_ITERATOR_REVERSE;
    }

    /* Open key and start iterator. */
    int openflags = REDICTMODULE_READ | REDICTMODULE_WRITE;
    RedictModuleKey *key = RedictModule_OpenKey(ctx, argv[1], openflags);
    if (RedictModule_StreamIteratorStart(key, flags,
                                        &startid, &endid) != REDICTMODULE_OK) {
        /* Key is not a stream, etc. */
        RedictModule_ReplyWithError(ctx, "ERR StreamIteratorStart failed");
        RedictModule_CloseKey(key);
        return REDICTMODULE_OK;
    }

    /* Check error handling: Delete current entry when no current entry. */
    assert(RedictModule_StreamIteratorDelete(key) ==
           REDICTMODULE_ERR);
    assert(errno == ENOENT);

    /* Check error handling: Fetch fields when no current entry. */
    assert(RedictModule_StreamIteratorNextField(key, NULL, NULL) ==
           REDICTMODULE_ERR);
    assert(errno == ENOENT);

    /* Return array. */
    RedictModule_ReplyWithArray(ctx, REDICTMODULE_POSTPONED_LEN);
    RedictModule_AutoMemory(ctx);
    RedictModuleStreamID id;
    long numfields;
    long len = 0;
    while (RedictModule_StreamIteratorNextID(key, &id,
                                            &numfields) == REDICTMODULE_OK) {
        RedictModule_ReplyWithArray(ctx, 2);
        RedictModuleString *id_str = RedictModule_CreateStringFromStreamID(ctx, &id);
        RedictModule_ReplyWithString(ctx, id_str);
        RedictModule_ReplyWithArray(ctx, numfields * 2);
        int delete = 0;
        RedictModuleString *field, *value;
        for (long i = 0; i < numfields; i++) {
            assert(RedictModule_StreamIteratorNextField(key, &field, &value) ==
                   REDICTMODULE_OK);
            RedictModule_ReplyWithString(ctx, field);
            RedictModule_ReplyWithString(ctx, value);
            /* check if this is a "selfdestruct" field */
            size_t field_len;
            const char *field_str = RedictModule_StringPtrLen(field, &field_len);
            if (!strncmp(field_str, "selfdestruct", field_len)) delete = 1;
        }
        if (delete) {
            assert(RedictModule_StreamIteratorDelete(key) == REDICTMODULE_OK);
        }
        /* check error handling: no more fields to fetch */
        assert(RedictModule_StreamIteratorNextField(key, &field, &value) ==
               REDICTMODULE_ERR);
        assert(errno == ENOENT);
        len++;
    }
    RedictModule_ReplySetArrayLength(ctx, len);
    RedictModule_StreamIteratorStop(key);
    RedictModule_CloseKey(key);
    return REDICTMODULE_OK;
}

/*
 * STREAM.TRIM key (MAXLEN (=|~) length | MINID (=|~) id)
 */
int stream_trim(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    if (argc != 5) {
        RedictModule_WrongArity(ctx);
        return REDICTMODULE_OK;
    }

    /* Parse args */
    int trim_by_id = 0; /* 0 = maxlen, 1 = minid */
    long long maxlen;
    RedictModuleStreamID minid;
    size_t arg_len;
    const char *arg = RedictModule_StringPtrLen(argv[2], &arg_len);
    if (!strcasecmp(arg, "minid")) {
        trim_by_id = 1;
        if (RedictModule_StringToStreamID(argv[4], &minid) != REDICTMODULE_OK) {
            RedictModule_ReplyWithError(ctx, "ERR Invalid stream ID");
            return REDICTMODULE_OK;
        }
    } else if (!strcasecmp(arg, "maxlen")) {
        if (RedictModule_StringToLongLong(argv[4], &maxlen) == REDICTMODULE_ERR) {
            RedictModule_ReplyWithError(ctx, "ERR Maxlen must be a number");
            return REDICTMODULE_OK;
        }
    } else {
        RedictModule_ReplyWithError(ctx, "ERR Invalid arguments");
        return REDICTMODULE_OK;
    }

    /* Approx or exact */
    int flags;
    arg = RedictModule_StringPtrLen(argv[3], &arg_len);
    if (arg_len == 1 && arg[0] == '~') {
        flags = REDICTMODULE_STREAM_TRIM_APPROX;
    } else if (arg_len == 1 && arg[0] == '=') {
        flags = 0;
    } else {
        RedictModule_ReplyWithError(ctx, "ERR Invalid approx-or-exact mark");
        return REDICTMODULE_OK;
    }

    /* Trim */
    RedictModuleKey *key = RedictModule_OpenKey(ctx, argv[1], REDICTMODULE_WRITE);
    long long trimmed;
    if (trim_by_id) {
        trimmed = RedictModule_StreamTrimByID(key, flags, &minid);
    } else {
        trimmed = RedictModule_StreamTrimByLength(key, flags, maxlen);
    }

    /* Return result */
    if (trimmed < 0) {
        RedictModule_ReplyWithError(ctx, "ERR Trimming failed");
    } else {
        RedictModule_ReplyWithLongLong(ctx, trimmed);
    }
    RedictModule_CloseKey(key);
    return REDICTMODULE_OK;
}

int RedictModule_OnLoad(RedictModuleCtx *ctx, RedictModuleString **argv, int argc) {
    REDICTMODULE_NOT_USED(argv);
    REDICTMODULE_NOT_USED(argc);
    if (RedictModule_Init(ctx, "stream", 1, REDICTMODULE_APIVER_1) == REDICTMODULE_ERR)
        return REDICTMODULE_ERR;

    if (RedictModule_CreateCommand(ctx, "stream.add", stream_add, "write",
                                  1, 1, 1) == REDICTMODULE_ERR)
        return REDICTMODULE_ERR;
    if (RedictModule_CreateCommand(ctx, "stream.addn", stream_addn, "write",
                                  1, 1, 1) == REDICTMODULE_ERR)
        return REDICTMODULE_ERR;
    if (RedictModule_CreateCommand(ctx, "stream.delete", stream_delete, "write",
                                  1, 1, 1) == REDICTMODULE_ERR)
        return REDICTMODULE_ERR;
    if (RedictModule_CreateCommand(ctx, "stream.range", stream_range, "write",
                                  1, 1, 1) == REDICTMODULE_ERR)
        return REDICTMODULE_ERR;
    if (RedictModule_CreateCommand(ctx, "stream.trim", stream_trim, "write",
                                  1, 1, 1) == REDICTMODULE_ERR)
        return REDICTMODULE_ERR;

    return REDICTMODULE_OK;
}
