#include "redismodule.h"
#include <errno.h>

int InternalAuth_GetInternalSecret(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    REDISMODULE_NOT_USED(argv);
    REDISMODULE_NOT_USED(argc);

    /* NOTE: The internal secret SHOULD NOT be exposed by any module. This is
    done for testing purposes only. */
    size_t len;
    const char *secret = RedisModule_GetInternalSecret(ctx, &len);
    if(secret) {
        RedisModule_ReplyWithStringBuffer(ctx, secret, len);
    } else {
        RedisModule_ReplyWithError(ctx, "ERR no internal secret available");
    }
    return REDISMODULE_OK;
}

int InternalAuth_InternalCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    REDISMODULE_NOT_USED(argv);
    REDISMODULE_NOT_USED(argc);

    RedisModule_ReplyWithSimpleString(ctx, "OK");
    return REDISMODULE_OK;
}

typedef enum {
    RM_CALL_REGULAR = 0,
    RM_CALL_WITHUSER = 1,
    RM_CALL_WITHDETACHEDCLIENT = 2,
    RM_CALL_REPLICATED = 3
} RMCallMode;

int call_rm_call(RedisModuleCtx *ctx, RedisModuleString **argv, int argc, RMCallMode mode) {
    if(argc < 2){
        return RedisModule_WrongArity(ctx);
    }
    RedisModuleCallReply *rep = NULL;
    RedisModuleCtx *detached_ctx = NULL;
    const char* cmd = RedisModule_StringPtrLen(argv[1], NULL);

    switch (mode) {
        case RM_CALL_REGULAR:
            // Regular call, with the unrestricted user.
            rep = RedisModule_Call(ctx, cmd, "vE", argv + 2, argc - 2);
            break;
        case RM_CALL_WITHUSER:
            // Simply call the command with the current client.
            rep = RedisModule_Call(ctx, cmd, "vCE", argv + 2, argc - 2);
            break;
        case RM_CALL_WITHDETACHEDCLIENT:
            // Use a context created with the thread-safe-context API
            detached_ctx = RedisModule_GetThreadSafeContext(NULL);
            if(!detached_ctx){
                RedisModule_ReplyWithError(ctx, "ERR failed to create detached context");
                return REDISMODULE_ERR;
            }
            // Dispatch the command with the detached context
            rep = RedisModule_Call(detached_ctx, cmd, "vCE", argv + 2, argc - 2);
            break;
        case RM_CALL_REPLICATED:
            rep = RedisModule_Call(ctx, cmd, "vE", argv + 2, argc - 2);
    }

    if(!rep) {
        char err[100];
        switch (errno) {
            case EACCES:
                RedisModule_ReplyWithError(ctx, "ERR NOPERM");
                break;
            case ENOENT:
                RedisModule_ReplyWithError(ctx, "ERR unknown command");
                break;
            default:
                snprintf(err, sizeof(err) - 1, "ERR errno=%d", errno);
                RedisModule_ReplyWithError(ctx, err);
                break;
        }
    } else {
        RedisModule_ReplyWithCallReply(ctx, rep);
        RedisModule_FreeCallReply(rep);
        if (mode == RM_CALL_REPLICATED)
            RedisModule_ReplicateVerbatim(ctx);
    }

    if (mode == RM_CALL_WITHDETACHEDCLIENT) {
        RedisModule_FreeThreadSafeContext(detached_ctx);
    }

    return REDISMODULE_OK;
}

int internal_rmcall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    return call_rm_call(ctx, argv, argc, RM_CALL_REGULAR);
}

int noninternal_rmcall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    return call_rm_call(ctx, argv, argc, RM_CALL_REGULAR);
}

int noninternal_rmcall_withuser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    return call_rm_call(ctx, argv, argc, RM_CALL_WITHUSER);
}

int noninternal_rmcall_detachedcontext_withuser(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    return call_rm_call(ctx, argv, argc, RM_CALL_WITHDETACHEDCLIENT);
}

int internal_rmcall_replicated(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    return call_rm_call(ctx, argv, argc, RM_CALL_REPLICATED);
}

/* This function must be present on each Redis module. It is used in order to
 * register the commands into the Redis server. */
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    REDISMODULE_NOT_USED(argv);
    REDISMODULE_NOT_USED(argc);

    if (RedisModule_Init(ctx,"testinternalsecret",1,REDISMODULE_APIVER_1)
        == REDISMODULE_ERR) return REDISMODULE_ERR;

    /* WARNING: A module should NEVER expose the internal secret - this is for
     * testing purposes only. */
    if (RedisModule_CreateCommand(ctx,"internalauth.getinternalsecret",
        InternalAuth_GetInternalSecret,"",0,0,0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx,"internalauth.internalcommand",
        InternalAuth_InternalCommand,"internal",0,0,0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx,"internalauth.internal_rmcall",
        internal_rmcall,"write internal",0,0,0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall",
        noninternal_rmcall,"write",0,0,0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall_withuser",
        noninternal_rmcall_withuser,"write",0,0,0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx,"internalauth.noninternal_rmcall_detachedcontext_withuser",
        noninternal_rmcall_detachedcontext_withuser,"write",0,0,0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx,"internalauth.internal_rmcall_replicated",
        internal_rmcall_replicated,"write internal",0,0,0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    return REDISMODULE_OK;
}
