/* An example module for attaching metadata to keys.
 *
 * This example lets tests create metadata-key classes and then SET and GET metadata
 * to keys. The 8-byte slot stores a handle to a module-managed allocation; here
 * we use to attach a string per-key.
 *
 * The module pre-registers several metadata classes during initialization and exposes
 * the following commands (via RedisModule_CreateCommand):
 *
 * 1) KEYMETA.REGISTER <4-char-id> <version> [FLAGS]
 *    Register a new metadata-key class during module load.
 *    Returns the <keymeta-class-id> index (Returned from RedisModule_CreateKeyMetaClass)
 *    On failure, returns nil
 *    In a real module it should be registered "automatically" via OnLoad.
 *
 *    FLAGS (colon-separated):
 *      KEEPONCOPY     - Keep metadata on COPY operation
 *      KEEPONRENAME   - Keep metadata on RENAME operation
 *      KEEPONMOVE     - Keep metadata on MOVE operation
 *      UNLINKFREE     - Use unlink callback for async free
 *      RDBLOAD        - Enable rdb_load callback (metadata can be loaded from RDB)
 *      RDBSAVE        - Enable rdb_save callback (metadata can be saved to RDB)
 *      ALLOWIGNORE    - Enable ALLOW_IGNORE flag (graceful discard on load if
 *                       class not registered or no rdb_load callback)
 *
 *    Example: > keymeta.register KMT1 1 KEEPONCOPY:KEEPONRENAME:ALLOWIGNORE:RDBLOAD:RDBSAVE
 *    Example: > keymeta.register KMT2 1 ALLOWIGNORE
 *
 * 2) KEYMETA.SET <4-char-id> <key> <string-value>
 *    Set the string value as metadata to given key.
 *    Note:
 *    - If already set earlier, then it is expected that it will released before setting a
 *      new string. That is why this command should start with trying to get first
 *      metadata for given key.
 *
 * 3) KEYMETA.GET <4-char-id> <key>
 *    Get the metadata attached to the key for the given class.
 *    Returns a string attached to the given key. Or nil if nothing is attached.
 *
 * 4) KEYMETA.UNREGISTER <4-char-id>
 *    This will mark the key metadata class as released. It can later be reused again
 *    by the same class (consider comment above).
 *    Return REDISMODULE_OK/REDISMODULE_ERR.
 *
 * 5) KEYMETA.ACTIVE
 *    Return total number of active metadata at the moment.
 */

#include "redismodule.h"
#include <string.h>
#include <stdlib.h>
#include <assert.h>

/* Virtualize class IDs for testing. Values: 0 unused, 1..7 used, -1 released */
RedisModuleKeyMetaClassId class_ids[8] = { 0 };

/* Mapping from 4-char-id to class-id */
typedef struct {
    char name[5];  /* 4 chars + null terminator */
    RedisModuleKeyMetaClassId class_id;
} ClassMapping;

#define MAX_CLASS_MAPPINGS 8
static ClassMapping class_mappings[MAX_CLASS_MAPPINGS];
static int num_class_mappings = 0;

/* Reverse lookup: given a class_id, find the 4-char-id name */
static const char* lookupClassName(RedisModuleKeyMetaClassId class_id) {
    for (int i = 0; i < num_class_mappings; i++) {
        if (class_mappings[i].class_id == class_id) {
            return class_mappings[i].name;
        }
    }
    return NULL;
}

/* Track active metadata instances (not yet freed) */
static long long active_metadata_count = 0;

/* Helper functions for class mapping */

/* Add a mapping from 4-char-id to class-id */
static int addClassMapping(const char *name, RedisModuleKeyMetaClassId class_id) {
    if (num_class_mappings >= MAX_CLASS_MAPPINGS) {
        return 0; /* No space */
    }
    strncpy(class_mappings[num_class_mappings].name, name, 4);
    class_mappings[num_class_mappings].name[4] = '\0';
    class_mappings[num_class_mappings].class_id = class_id;
    num_class_mappings++;
    return 1;
}

/* Lookup class-id by 4-char-id. Returns -1 if not found. */
static RedisModuleKeyMetaClassId lookupClassId(const char *name) {
    for (int i = 0; i < num_class_mappings; i++) {
        if (strncmp(class_mappings[i].name, name, 4) == 0) {
            return class_mappings[i].class_id;
        }
    }
    return -1;
}

/* Remove a mapping by 4-char-id */
static int removeClassMapping(const char *name) {
    for (int i = 0; i < num_class_mappings; i++) {
        if (strncmp(class_mappings[i].name, name, 4) == 0) {
            /* Shift remaining entries down */
            for (int j = i; j < num_class_mappings - 1; j++) {
                class_mappings[j] = class_mappings[j + 1];
            }
            num_class_mappings--;
            return 1;
        }
    }
    return 0;
}

/* Callback functions for metadata lifecycle */

/* Copy callback - called when a key is copied */
static int KeyMetaCopyCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) {
    REDISMODULE_NOT_USED(ctx);
    char *str = (char *)*meta;
    /* Note, condition is redundant since cb only invoked when meta != reset_value */
    if (str) {
        char *new_str = strdup(str);
        *meta = (uint64_t)new_str;
        active_metadata_count++; /* New metadata instance created */
    }
    return 1; /* Keep metadata */
}

/* Rename callback - called when a key is renamed. */
static int KeyMetaRenameDiscardCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) {
    REDISMODULE_NOT_USED(ctx);
    REDISMODULE_NOT_USED(meta);
    return 0;
}

/* Unlink callback - called when a key is unlinked */
static void KeyMetaUnlinkCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) {
    /* Let's challenge and free early on before free callback */
    /* Note, condition is redundant since cb only invoked when meta != reset_value */
    if (*meta != 0) {
        char *str = (char *)*meta;
        free(str);
        *meta = 0;  /* Set to reset_value !!! */
        active_metadata_count--; /* Metadata instance freed */
    }
    REDISMODULE_NOT_USED(ctx);
}

/* Free callback - called when metadata needs to be freed */
static void KeyMetaFreeCallback(const char *keyname, uint64_t meta) {
    REDISMODULE_NOT_USED(keyname);
    /* Note, condition is redundant since cb only invoked when meta != reset_value */
    if (meta != 0) {
        char *str = (char *)meta;
        free(str);
        active_metadata_count--; /* Metadata instance freed */
    }
}

static int KeyMetaMoveDiscardCallback(RedisModuleKeyOptCtx *ctx, uint64_t *meta) {
    REDISMODULE_NOT_USED(ctx);
    REDISMODULE_NOT_USED(meta);
    return 0; /* discard metadata */
}

/* RDB Save Callback - Serialize metadata to RDB
 * This callback is invoked during RDB save to write the metadata value.
 *
 * Parameters:
 *   - rdb: RedisModuleIO context for writing to RDB
 *   - reserved: Reserved for future use
 *   - meta: Pointer to the 8-byte metadata value (pointer to our string)
 */
static void KeyMetaRDBSaveCallback(RedisModuleIO *rdb, void *reserved, uint64_t *meta) {
    REDISMODULE_NOT_USED(reserved);

    /* If metadata is NULL (reset_value), don't save anything */
    if (*meta == 0) return;

    /* Extract the string from the metadata pointer */
    char *metadata_string = (char *)*meta;

    /* Save the string to RDB using SaveStringBuffer */
    RedisModule_SaveStringBuffer(rdb, metadata_string, strlen(metadata_string));
    /* Save more silly data */
    RedisModule_SaveSigned(rdb, 1);
    RedisModule_SaveFloat(rdb, 1.5);
    RedisModule_SaveLongDouble(rdb, 0.333333333333333333L);
}

/* RDB Load Callback - Deserialize metadata from RDB
 * This callback is invoked during RDB load to read the metadata value.
 *
 * Parameters:
 *   - rdb: RedisModuleIO context for reading from RDB
 *   - meta: Pointer to store the loaded 8-byte metadata value
 *   - encver: Encoding version (class version from RDB)
 *
 * Returns:
 *   - 1: Attach metadata to key (success)
 *   - 0: Ignore/skip metadata (not an error)
 *   - -1: Error - abort RDB load
 */
static int KeyMetaRDBLoadCallback(RedisModuleIO *rdb, uint64_t *meta, int encver) {
    REDISMODULE_NOT_USED(encver);

    /* Load the string from RDB using LoadStringBuffer */
    size_t len;
    char *loaded_string = RedisModule_LoadStringBuffer(rdb, &len);

    if (loaded_string == NULL) {
        /* Error loading string */
        return -1;
    }

    /* Allocate and copy the string (LoadStringBuffer returns a buffer that must be freed) */
    char *metadata_string = malloc(len + 1);
    if (metadata_string == NULL) {
        RedisModule_Free(loaded_string);
        return -1;
    }

    memcpy(metadata_string, loaded_string, len);
    metadata_string[len] = '\0';
    RedisModule_Free(loaded_string);

    /* Load the additional data that was saved (must match rdb_save) */
    int64_t signed_val = RedisModule_LoadSigned(rdb);
    float float_val = RedisModule_LoadFloat(rdb);
    long double ldouble_val = RedisModule_LoadLongDouble(rdb);
    /* We don't use these values, just need to consume them from the stream */
    (void)signed_val;
    (void)float_val;
    (void)ldouble_val;

    /* Store the pointer in metadata */
    *meta = (uint64_t)metadata_string;
    active_metadata_count++; /* New metadata instance created */

    /* Return 1 to attach metadata to the key */
    return 1;
}

/* AOF Rewrite Callback - Common implementation for all classes
 * This callback is invoked during AOF rewrite to emit commands that will
 * recreate the metadata when the AOF is loaded.
 *
 * Parameters:
 *   - aof: RedisModuleIO context for writing to AOF
 *   - reserved: Reserved for future use
 *   - meta: The 8-byte metadata value (pointer to our string)
 *   - class_id: The class ID for this metadata
 */
static void KeyMetaAOFRewriteCallback_Class(RedisModuleIO *aof, void *reserved, uint64_t meta, RedisModuleKeyMetaClassId class_id) {
    REDISMODULE_NOT_USED(reserved);

    /* If metadata is NULL (reset_value), don't emit anything */
    if (meta == 0) return;

    /* Extract the string from the metadata pointer */
    char *metadata_string = (char *)meta;

    /* Lookup the 9-byte-id name for this class */
    const char *class_name = lookupClassName(class_id);
    if (!class_name) {
        /* This shouldn't happen, but handle gracefully */
        return;
    }

    /* Get the key name from the AOF IO context */
    const RedisModuleString *key = RedisModule_GetKeyNameFromIO(aof);
    if (!key) {
        /* Key name not available - shouldn't happen during AOF rewrite */
        return;
    }

    /* Emit the KEYMETA.SET command to recreate this metadata
     * Format: KEYMETA.SET <9-byte-id> <key> <string-value> */
    RedisModule_EmitAOF(aof, "KEYMETA.SET", "csc",
                        class_name,           /* c: 9-byte-id (C string) */
                        key,                  /* s: key name (RedisModuleString) */
                        metadata_string);     /* c: metadata value (C string) */
}

/* Individual AOF rewrite callbacks for each class (1-7)
 * Each callback wraps the common implementation with its specific class ID */
static void KeyMetaAOFRewriteCb1(RedisModuleIO *aof, void *reserved, uint64_t meta) {
    KeyMetaAOFRewriteCallback_Class(aof, reserved, meta, 1);
}

static void KeyMetaAOFRewriteCb2(RedisModuleIO *aof, void *reserved, uint64_t meta) {
    KeyMetaAOFRewriteCallback_Class(aof, reserved, meta, 2);
}

static void KeyMetaAOFRewriteCb3(RedisModuleIO *aof, void *reserved, uint64_t meta) {
    KeyMetaAOFRewriteCallback_Class(aof, reserved, meta, 3);
}

static void KeyMetaAOFRewriteCb4(RedisModuleIO *aof, void *reserved, uint64_t meta) {
    KeyMetaAOFRewriteCallback_Class(aof, reserved, meta, 4);
}

static void KeyMetaAOFRewriteCb5(RedisModuleIO *aof, void *reserved, uint64_t meta) {
    KeyMetaAOFRewriteCallback_Class(aof, reserved, meta, 5);
}

static void KeyMetaAOFRewriteCb6(RedisModuleIO *aof, void *reserved, uint64_t meta) {
    KeyMetaAOFRewriteCallback_Class(aof, reserved, meta, 6);
}

static void KeyMetaAOFRewriteCb7(RedisModuleIO *aof, void *reserved, uint64_t meta) {
    KeyMetaAOFRewriteCallback_Class(aof, reserved, meta, 7);
}

/* KEYMETA.REGISTER <4-char-id> <version> [KEEPONCOPY:KEEPONRENAME:UNLINKFREE:ALLOWIGNORE:NORDBLOAD:NORDBSAVE] */
static int KeyMetaRegister_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (argc < 3 || argc > 4) {
        return RedisModule_WrongArity(ctx);
    }

    /* argv[1]: key metadata class name */
    size_t namelen;
    const char *metaname = RedisModule_StringPtrLen(argv[1], &namelen);

    /* argv[2]: key metadata class version */
    long long metaver;
    if (RedisModule_StringToLongLong(argv[2], &metaver) != REDISMODULE_OK) {
        RedisModule_ReplyWithError(ctx, "ERR invalid version number");
        return REDISMODULE_OK;
    }

    /* Parse optional callback flags */
    int keep_on_copy = 0, keep_on_rename = 0, unlink_free = 0, keep_on_move = 0;
    int allow_ignore = 0;  /* Default: ALLOW_IGNORE disabled */
    int rdb_load = 0;      /* Default: rdb_load disabled */
    int rdb_save = 0;      /* Default: rdb_save disabled */

    if (argc == 4) {
        const char *flags = RedisModule_StringPtrLen(argv[3], NULL);
        if (strstr(flags, "KEEPONCOPY")) keep_on_copy = 1;
        if (strstr(flags, "KEEPONRENAME")) keep_on_rename = 1;
        if (strstr(flags, "UNLINKFREE")) unlink_free = 1;
        if (strstr(flags, "KEEPONMOVE")) keep_on_move = 1;
        if (strstr(flags, "ALLOWIGNORE")) allow_ignore = 1;   /* Enable ALLOW_IGNORE */
        if (strstr(flags, "RDBLOAD")) rdb_load = 1;           /* Enable rdb_load */
        if (strstr(flags, "RDBSAVE")) rdb_save = 1;           /* Enable rdb_save */
    }

    /* Setup configuration */
    RedisModuleKeyMetaClassConfig config = {0};
    config.version = REDISMODULE_KEY_META_VERSION;
    config.flags = allow_ignore ? (1 << REDISMODULE_META_ALLOW_IGNORE) : 0;
    config.reset_value = (uint64_t)NULL;  /* NULL pointer means no resource to free */
    config.rdb_load = rdb_load ? KeyMetaRDBLoadCallback : NULL;
    config.rdb_save = rdb_save ? KeyMetaRDBSaveCallback : NULL;
    switch (num_class_mappings + 1) { /* distinct cb per class */
        case 1: config.aof_rewrite = KeyMetaAOFRewriteCb1; break;
        case 2: config.aof_rewrite = KeyMetaAOFRewriteCb2; break;
        case 3: config.aof_rewrite = KeyMetaAOFRewriteCb3; break;
        case 4: config.aof_rewrite = KeyMetaAOFRewriteCb4; break;
        case 5: config.aof_rewrite = KeyMetaAOFRewriteCb5; break;
        case 6: config.aof_rewrite = KeyMetaAOFRewriteCb6; break;
        case 7: config.aof_rewrite = KeyMetaAOFRewriteCb7; break;
        default: config.aof_rewrite = NULL; break;
    }
    config.free = KeyMetaFreeCallback;
    config.copy = keep_on_copy ? KeyMetaCopyCallback : NULL;
    config.rename = keep_on_rename ? NULL : KeyMetaRenameDiscardCallback;
    config.move = keep_on_move ? NULL : KeyMetaMoveDiscardCallback;
    config.defrag = NULL;
    config.unlink = unlink_free ? KeyMetaUnlinkCallback : NULL;
    config.mem_usage = NULL;
    config.free_effort = NULL;

    /* Create the metadata class */
    RedisModuleKeyMetaClassId class_id = RedisModule_CreateKeyMetaClass(ctx, metaname, (int)metaver, &config);

    if (class_id < 0) {
        RedisModule_ReplyWithError(ctx, "ERR failed to create metadata class");
        return REDISMODULE_OK;
    } else {
        /* Store the mapping from 9-byte-id to class-id */
        if (!addClassMapping(metaname, class_id)) {
            RedisModule_ReplyWithError(ctx, "ERR failed to store class mapping");
            return REDISMODULE_OK;
        }
        RedisModule_ReplyWithLongLong(ctx, class_id);
    }

    return REDISMODULE_OK;
}

/* KEYMETA.SET <9-byte-id> <key> <string-value> */
static int KeyMetaSet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (argc != 4) {
        return RedisModule_WrongArity(ctx);
    }

    /* Parse arguments */
    const char *metaname = RedisModule_StringPtrLen(argv[1], NULL);
    RedisModuleString *keyname = argv[2];
    const char *value = RedisModule_StringPtrLen(argv[3], NULL);

    /* Lookup the metadata class by name */
    RedisModuleKeyMetaClassId class_id = lookupClassId(metaname);
    if (class_id < 0) {
        RedisModule_ReplyWithError(ctx, "ERR metadata class not found");
        return REDISMODULE_OK;
    }

    /* Open the key for writing */
    RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ | REDISMODULE_WRITE);
    
    if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
        RedisModule_ReplyWithNull(ctx);
        RedisModule_CloseKey(key);
        return REDISMODULE_OK;
    }    

    /* Check if metadata already exists and free it first. 
     * 
     * Note: The caller is responsible for retrieving and freeing any existing 
     *       pointer-based metadata before RM_SetKeyMeta() to a new value 
     */
    uint64_t meta = 0;
    if (RedisModule_GetKeyMeta(class_id, key, &meta) == REDISMODULE_OK) {
        if (meta != 0) {
            free((char *)meta);
            active_metadata_count--; /* Old metadata freed */
        }
    }

    char *new_str = strdup(value);
    int res = RedisModule_SetKeyMeta(class_id, key, (uint64_t)new_str);

    if (res == REDISMODULE_OK) {
        active_metadata_count++; /* New metadata instance created */
    }

    RedisModule_CloseKey(key);

    if (res == REDISMODULE_OK) {
        RedisModule_ReplyWithSimpleString(ctx, "OK");
    } else {
        free(new_str);
        RedisModule_ReplyWithError(ctx, "ERR failed to set metadata");
    }
    return REDISMODULE_OK;
}

/* KEYMETA.GET <9-byte-id> <key> */
static int KeyMetaGet_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (argc != 3) {
        return RedisModule_WrongArity(ctx);
    }

    /* Parse arguments */
    const char *metaname = RedisModule_StringPtrLen(argv[1], NULL);
    RedisModuleString *keyname = argv[2];

    /* Lookup the metadata class by name */
    RedisModuleKeyMetaClassId class_id = lookupClassId(metaname);
    if (class_id < 0) {
        RedisModule_ReplyWithError(ctx, "ERR metadata class not found");
        return REDISMODULE_OK;
    }

    /* Open the key for reading */
    RedisModuleKey *key = RedisModule_OpenKey(ctx, keyname, REDISMODULE_READ);
    if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
        RedisModule_ReplyWithNull(ctx);
        RedisModule_CloseKey(key);
        return REDISMODULE_OK;
    }

    /* Get the metadata */
    uint64_t meta = 0;
    int result = RedisModule_GetKeyMeta(class_id, key, &meta);

    RedisModule_CloseKey(key);

    if (result == REDISMODULE_OK && meta != 0) {
        char *str = (char *)meta;
        RedisModule_ReplyWithCString(ctx, str);
    } else {
        RedisModule_ReplyWithNull(ctx);
    }

    return REDISMODULE_OK;
}

/* KEYMETA.UNREGISTER <9-byte-id> */
static int KeyMetaUnregister_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (argc != 2) {
        return RedisModule_WrongArity(ctx);
    }

    /* Parse arguments */
    const char *metaname = RedisModule_StringPtrLen(argv[1], NULL);

    /* Lookup the metadata class by name */
    RedisModuleKeyMetaClassId class_id = lookupClassId(metaname);
    if (class_id < 0) {
        RedisModule_ReplyWithError(ctx, "ERR metadata class not found");
        return REDISMODULE_OK;
    }

    /* Release the metadata class */
    int result = RedisModule_ReleaseKeyMetaClass(class_id);

    if (result == REDISMODULE_OK) {
        /* Remove the mapping */
        removeClassMapping(metaname);
        RedisModule_ReplyWithSimpleString(ctx, "OK");
    } else {
        RedisModule_ReplyWithError(ctx, "ERR failed to unregister class");
    }
    return REDISMODULE_OK;
}

/* KEYMETA.ACTIVE
 * Returns the total number of active metadata instances that haven't been freed yet.
 * This is useful for testing to verify that metadata is properly cleaned up. */
static int KeyMetaActive_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (argc != 1) {
        return RedisModule_WrongArity(ctx);
    }
    REDISMODULE_NOT_USED(argv);

    RedisModule_ReplyWithLongLong(ctx, active_metadata_count);
    return REDISMODULE_OK;
}

/* Module initialization */
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    REDISMODULE_NOT_USED(argv);
    REDISMODULE_NOT_USED(argc);

    if (RedisModule_Init(ctx, "test_keymeta", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
    }

    /* Register commands */
    if (RedisModule_CreateCommand(ctx, "keymeta.register",
        KeyMetaRegister_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
    }

    if (RedisModule_CreateCommand(ctx, "keymeta.set",
        KeyMetaSet_RedisCommand, "write deny-oom", 1, 1, 1) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
    }

    if (RedisModule_CreateCommand(ctx, "keymeta.get",
        KeyMetaGet_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
    }

    if (RedisModule_CreateCommand(ctx, "keymeta.unregister",
        KeyMetaUnregister_RedisCommand, "write", 0, 0, 0) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
    }

    if (RedisModule_CreateCommand(ctx, "keymeta.active",
        KeyMetaActive_RedisCommand, "readonly fast", 0, 0, 0) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
    }

    return REDISMODULE_OK;
}

int RedisModule_OnUnload(RedisModuleCtx *ctx) {
    REDISMODULE_NOT_USED(ctx);
    long unsigned int i;
    for (i = 0 ; i < sizeof(class_ids) / sizeof(class_ids[0]); i++) {
        if (class_ids[i] > 0)
            RedisModule_ReleaseKeyMetaClass(class_ids[i]);
    }
    return REDISMODULE_OK;
}
