// calogKv.c -- calog key-value library (see calogKv.h). A process-wide, mutex-guarded array
// of {key bytes, key length, value}. kvSet/kvGet/kvHas/kvDelete/kvKeys are natives over that
// shared store; every stored value is a deep copy, so the store owns pure data.
//
// The registry state is STATIC (not heap) and its bookkeeping is never freed: the five
// natives stay reachable (via any context) right up until calogDestroy tears the broker
// registry down, so freeing the state in calogKvShutdown -- which the contract requires
// BEFORE calogDestroy -- would leave those natives dereferencing freed memory. Shutdown
// instead just frees the stored keys + values and empties the array; the small static
// bookkeeping persists for the process (no per-cycle leak), and a post-shutdown native call
// simply sees an empty store.
//
// Function values are refused (calogErrUnsupportedE): storing a CalogFnT would drag script
// callable lifecycle into a data store, so kv sidesteps it entirely and holds data only.

#define _POSIX_C_SOURCE 200809L

#include "calogKv.h"

#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "calogInternal.h"

typedef struct KvEntryT {
    char        *keyBytes;
    int64_t      keyLen;
    CalogValueT  value;
} KvEntryT;

// The store (heap array), guarded by gMapMutex. refCount (guarded by gInitMutex) counts
// registered runtimes so the LAST calogKvShutdown frees the store's contents.
static pthread_mutex_t gMapMutex  = PTHREAD_MUTEX_INITIALIZER;
static KvEntryT       *gEntries   = NULL;
static int32_t         gCount     = 0;
static int32_t         gCap       = 0;
static pthread_mutex_t gInitMutex = PTHREAD_MUTEX_INITIALIZER;
static int32_t         gRefCount  = 0;

static bool    kvContainsFn(const CalogValueT *value, int32_t depth);
static int32_t kvDelete(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t kvFindLocked(const char *bytes, int64_t length);
static void    kvFreeAll(void);
static int32_t kvGet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t kvHas(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t kvKeys(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t kvSet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);


int32_t calogKvRegister(CalogT *calog) {
    calogRegistryRetain(&gInitMutex, &gRefCount);
    calogRegisterInline(calog, "kvSet", kvSet, NULL);
    calogRegisterInline(calog, "kvGet", kvGet, NULL);
    calogRegisterInline(calog, "kvHas", kvHas, NULL);
    calogRegisterInline(calog, "kvDelete", kvDelete, NULL);
    calogRegisterInline(calog, "kvKeys", kvKeys, NULL);
    return calogOkE;
}


void calogKvShutdown(void) {
    // calogRegistryRelease holds gInitMutex for the whole call, including kvFreeAll below.
    calogRegistryRelease(&gInitMutex, &gRefCount, kvFreeAll);
}


static bool kvContainsFn(const CalogValueT *value, int32_t depth) {
    int64_t index;

    // kv stores only data (copied by value); a function value -- at the top level OR nested in
    // an aggregate -- would retain a CalogFnT into the process-wide store past its owner's life,
    // so reject any value that carries one. Too-deep is rejected as unverifiable.
    if (depth >= CALOG_MAX_DEPTH) {
        return true;
    }
    if (value->type == calogFnE) {
        return true;
    }
    if (value->type == calogAggE) {
        for (index = 0; index < value->as.agg->arrayCount; index++) {
            if (kvContainsFn(&value->as.agg->array[index], depth + 1)) {
                return true;
            }
        }
        for (index = 0; index < value->as.agg->pairCount; index++) {
            if (kvContainsFn(&value->as.agg->pairs[index].key, depth + 1)) {
                return true;
            }
            if (kvContainsFn(&value->as.agg->pairs[index].value, depth + 1)) {
                return true;
            }
        }
    }
    return false;
}


static int32_t kvDelete(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    int32_t index;

    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "kvDelete expects (key)");
    }
    pthread_mutex_lock(&gMapMutex);
    index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
    if (index >= 0) {
        free(gEntries[index].keyBytes);
        calogValueFree(&gEntries[index].value);
        gEntries[index] = gEntries[gCount - 1];
        gCount--;
    }
    pthread_mutex_unlock(&gMapMutex);
    return calogOkE;
}


static int32_t kvFindLocked(const char *bytes, int64_t length) {
    int32_t index;

    for (index = 0; index < gCount; index++) {
        if (gEntries[index].keyLen == length && memcmp(gEntries[index].keyBytes, bytes, (size_t)length) == 0) {
            return index;
        }
    }
    return -1;
}


// Invoked by calogRegistryRelease while gInitMutex is held, exactly once, when the last
// reference drops.
static void kvFreeAll(void) {
    int32_t index;

    pthread_mutex_lock(&gMapMutex);
    for (index = 0; index < gCount; index++) {
        free(gEntries[index].keyBytes);
        calogValueFree(&gEntries[index].value);
    }
    free(gEntries);
    gEntries = NULL;
    gCount   = 0;
    gCap     = 0;
    pthread_mutex_unlock(&gMapMutex);
}


static int32_t kvGet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    int32_t index;
    int32_t status;

    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "kvGet expects (key)");
    }
    status = calogOkE;
    pthread_mutex_lock(&gMapMutex);
    index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
    if (index >= 0) {
        status = calogValueCopy(result, &gEntries[index].value);
    }
    pthread_mutex_unlock(&gMapMutex);
    if (status != calogOkE) {
        return calogFail(result, status, "kvGet: out of memory");
    }
    return calogOkE;
}


static int32_t kvHas(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    int32_t index;

    (void)userData;
    calogValueNil(result);
    if (argCount != 1 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "kvHas expects (key)");
    }
    pthread_mutex_lock(&gMapMutex);
    index = kvFindLocked(args[0].as.s.bytes, args[0].as.s.length);
    pthread_mutex_unlock(&gMapMutex);
    calogValueBool(result, index >= 0);
    return calogOkE;
}


static int32_t kvKeys(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    CalogAggT *agg;
    int32_t    index;
    int32_t    status;

    (void)args;
    (void)argCount;
    (void)userData;
    calogValueNil(result);
    status = calogAggCreate(&agg, calogListE);
    if (status != calogOkE) {
        return calogFail(result, status, "kvKeys: out of memory");
    }
    pthread_mutex_lock(&gMapMutex);
    for (index = 0; index < gCount; index++) {
        CalogValueT key;
        status = calogValueString(&key, gEntries[index].keyBytes, gEntries[index].keyLen);
        if (status != calogOkE) {
            break;
        }
        status = calogAggPush(agg, &key);
        if (status != calogOkE) {
            calogValueFree(&key);
            break;
        }
    }
    pthread_mutex_unlock(&gMapMutex);
    if (status != calogOkE) {
        calogAggFree(agg);
        return calogFail(result, status, "kvKeys: out of memory");
    }
    calogValueAgg(result, agg);
    return calogOkE;
}


static int32_t kvSet(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    void        *buffer;
    int64_t      capacity;
    CalogValueT  copy;
    char        *keyBytes;
    int64_t      keyLen;
    int32_t      index;
    int32_t      status;

    (void)userData;
    calogValueNil(result);
    if (argCount != 2 || args[0].type != calogStringE) {
        return calogFail(result, calogErrArgE, "kvSet expects (key, value)");
    }
    if (kvContainsFn(&args[1], 0)) {
        return calogFail(result, calogErrUnsupportedE, "kv stores data, not functions");
    }
    status = calogValueCopy(&copy, &args[1]);
    if (status != calogOkE) {
        return calogFail(result, status, "kvSet: out of memory");
    }
    keyLen = args[0].as.s.length;
    pthread_mutex_lock(&gMapMutex);
    index = kvFindLocked(args[0].as.s.bytes, keyLen);
    if (index >= 0) {
        // Replace: drop the old value, keep the existing key bytes.
        calogValueFree(&gEntries[index].value);
        gEntries[index].value = copy;
        pthread_mutex_unlock(&gMapMutex);
        return calogOkE;
    }
    buffer   = gEntries;
    capacity = gCap;
    status   = calogGrow(&buffer, &capacity, (int64_t)gCount + 1, sizeof(KvEntryT));
    if (status != calogOkE) {
        pthread_mutex_unlock(&gMapMutex);
        calogValueFree(&copy);
        return calogFail(result, status, "kvSet: out of memory");
    }
    gEntries = (KvEntryT *)buffer;
    gCap     = (int32_t)capacity;
    keyBytes = (char *)malloc((size_t)keyLen + 1);
    if (keyBytes == NULL) {
        pthread_mutex_unlock(&gMapMutex);
        calogValueFree(&copy);
        return calogFail(result, calogErrOomE, "kvSet: out of memory");
    }
    if (keyLen > 0) {
        memcpy(keyBytes, args[0].as.s.bytes, (size_t)keyLen);
    }
    keyBytes[keyLen] = '\0';
    gEntries[gCount].keyBytes = keyBytes;
    gEntries[gCount].keyLen   = keyLen;
    gEntries[gCount].value    = copy;
    gCount++;
    pthread_mutex_unlock(&gMapMutex);
    return calogOkE;
}
