/*
 * Copyright (C) 2022-2024 Jordan Bancino <@jordan:bancino.net> with
 * other valuable contributors. See CONTRIBUTORS.txt for the full list.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
#include <Cytoplasm/Db.h>
#include <User.h>
#include <Cytoplasm/Util.h>
#include <Cytoplasm/Memory.h>
#include <Cytoplasm/Str.h>
#include <Cytoplasm/Sha.h>
#include <Cytoplasm/Json.h>
#include <Cytoplasm/Log.h>

#include <Parser.h>
#include <Room.h>

#include <pthread.h>
#include <string.h>
#include <errno.h>

struct User
{
    Db *db;
    DbRef *ref;

    DbRef *inviteRef, *joinRef;

    char *name;
    char *deviceId;
};

static pthread_mutex_t pushLock;
static HashMap *pushTable = NULL;

bool
UserValidate(char *localpart, char *domain)
{
    size_t maxLen = 255 - strlen(domain) - 1;
    size_t i = 0;

    while (localpart[i])
    {
        char c = localpart[i];

        if (i > maxLen)
        {
            return false;
        }

        if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
              (c == '.') || (c == '_') || (c == '=') || (c == '-') ||
              (c == '/')))
        {
            return false;
        }

        i++;
    }

    return true;
}

bool
UserHistoricalValidate(char *localpart, char *domain)
{
    size_t maxLen = 255 - strlen(domain) - 1;
    size_t i = 0;

    while (localpart[i])
    {
        char c = localpart[i];

        if (i > maxLen)
        {
            return false;
        }

        if (!((c >= 0x21 && c <= 0x39) || (c >= 0x3B && c <= 0x7E)))
        {
            return false;
        }

        i++;
    }

    return true;
}

bool
UserExists(Db * db, char *name)
{
    return DbExists(db, 2, "users", name);
}

User *
UserLock(Db * db, char *name)
{
    User *user = NULL;
    DbRef *ref = NULL;

    if (!name || !UserExists(db, name))
    {
        return NULL;
    }

    ref = DbLock(db, 2, "users", name);
    user = Malloc(sizeof(User));
    user->db = db;
    user->ref = ref;
    user->name = StrDuplicate(name);
    user->deviceId = NULL;

    user->inviteRef = DbLock(db, 3, "users", user->name, "invites");
    user->joinRef = DbLock(db, 3, "users", user->name, "joins");

    return user;
}

User *
UserAuthenticate(Db * db, char *accessToken)
{
    User *user;
    DbRef *atRef;

    char *userName;
    char *deviceId;
    uint64_t expires;

    if (!db || !accessToken)
    {
        return NULL;
    }

    atRef = DbLock(db, 3, "tokens", "access", accessToken);
    if (!atRef)
    {
        return NULL;
    }

    userName = JsonValueAsString(HashMapGet(DbJson(atRef), "user"));
    deviceId = JsonValueAsString(HashMapGet(DbJson(atRef), "device"));
    expires =  JsonValueAsInteger(HashMapGet(DbJson(atRef), "expires"));

    user = UserLock(db, userName);
    if (!user)
    {
        DbUnlock(db, atRef);
        return NULL;
    }

    if (expires && UtilTsMillis() >= expires)
    {
        UserUnlock(user);
        DbUnlock(db, atRef);
        return NULL;
    }

    user->deviceId = StrDuplicate(deviceId);

    DbUnlock(db, atRef);
    return user;
}

bool
UserUnlock(User * user)
{
    bool ret;
    Db *db;
    DbRef *ref;

    if (!user)
    {
        return false;
    }
    db = user->db;
    ref = user->ref;

    Free(user->name);
    Free(user->deviceId);

    ret =   DbUnlock(db, ref) && 
            DbUnlock(db, user->joinRef) && 
            DbUnlock(db, user->inviteRef);
    user->db = NULL;
    user->ref = NULL;
    Free(user);

    return ret;
}

User *
UserCreate(Db * db, char *name, char *password)
{
    User *user = NULL;
    HashMap *json = NULL;

    uint64_t ts = UtilTsMillis();

    /* TODO: Put some sort of password policy(like for example at least
     * 8 chars, or maybe check it's entropy)? */
    if (!db                             || 
        (name && UserExists(db, name))  || 
        !password                       || 
        !strlen(password))
    {
        /* User exists or cannot be registered, therefore, do NOT
         * bother */
        return NULL;
    }

    user = Malloc(sizeof(User));
    user->db = db;

    if (!name)
    {
        user->name = StrRandom(12);
    }
    else
    {
        user->name = StrDuplicate(name);
    }

    user->ref = DbCreate(db, 2, "users", user->name);
    if (!user->ref)
    {
        /* The only scenario where I can see that occur is if for some
         * strange reason, Db fails to create a file(e.g fs is full) */
        Free(user->name);
        Free(user);
        return NULL;
    }

    UserSetPassword(user, password);

    json = DbJson(user->ref);
    HashMapSet(json, "createdOn", JsonValueInteger(ts));
    HashMapSet(json, "deactivated", JsonValueBoolean(false));

    user->inviteRef = DbCreate(db, 3, "users", user->name, "invites");
    user->joinRef = DbCreate(db, 3, "users", user->name, "joins");

    return user;
}

UserLoginInfo *
UserLogin(User * user, char *password, char *deviceId, char *deviceDisplayName,
          int withRefresh)
{
    DbRef *rtRef = NULL;

    HashMap *devices;
    HashMap *device;

    UserLoginInfo *result;

    if (!user || !password)
    {
        return NULL;
    }

    if (!UserCheckPassword(user, password) || UserDeactivated(user))
    {
        return NULL;
    }

    result = Malloc(sizeof(UserLoginInfo));
    if (!result)
    {
        return NULL;
    }

    result->refreshToken = NULL;

    if (!deviceId)
    {
        deviceId = StrRandom(10);
    }
    else
    {
        deviceId = StrDuplicate(deviceId);
    }

    /* Generate an access token */
    result->accessToken = UserAccessTokenGenerate(user, deviceId, withRefresh);
    UserAccessTokenSave(user->db, result->accessToken);

    if (withRefresh)
    {
        result->refreshToken = StrRandom(64);
        rtRef = DbCreate(user->db, 3, "tokens", "refresh", result->refreshToken);

        HashMapSet(DbJson(rtRef), "refreshes",
                   JsonValueString(result->accessToken->string));
        DbUnlock(user->db, rtRef);
    }

    devices = JsonValueAsObject(HashMapGet(DbJson(user->ref), "devices"));
    if (!devices)
    {
        devices = HashMapCreate();
        HashMapSet(DbJson(user->ref), "devices", JsonValueObject(devices));
    }

    device = JsonValueAsObject(HashMapGet(devices, deviceId));

    if (device)
    {
        JsonValue *val;

        val = HashMapDelete(device, "accessToken");
        if (val)
        {
            DbDelete(user->db, 3, "tokens", "access", JsonValueAsString(val));
            JsonValueFree(val);
        }

        val = HashMapDelete(device, "refreshToken");
        if (val)
        {
            DbDelete(user->db, 3, "tokens", "refresh", JsonValueAsString(val));
            JsonValueFree(val);
        }
    }
    else
    {
        device = HashMapCreate();
        HashMapSet(devices, deviceId, JsonValueObject(device));

        if (deviceDisplayName)
        {
            HashMapSet(device, "displayName",
                       JsonValueString(deviceDisplayName));
        }

    }

    Free(deviceId);

    if (result->refreshToken)
    {
        HashMapSet(device, "refreshToken",
                   JsonValueString(result->refreshToken));
    }

    HashMapSet(device, "accessToken",
               JsonValueString(result->accessToken->string));

    return result;
}

char *
UserGetName(User * user)
{
    return user ? user->name : NULL;
}

char *
UserGetDeviceId(User * user)
{
    return user ? user->deviceId : NULL;
}

bool
UserCheckPassword(User * user, char *password)
{
    HashMap *json;

    char *storedHash;
    char *salt;

    unsigned char *hashBytes;
    char *hashedPwd;
    char *tmp;

    bool result;

    if (!user || !password)
    {
        return false;
    }

    json = DbJson(user->ref);

    storedHash = JsonValueAsString(HashMapGet(json, "password"));
    salt = JsonValueAsString(HashMapGet(json, "salt"));

    if (!storedHash || !salt)
    {
        return false;
    }

    tmp = StrConcat(2, password, salt);
    hashBytes = Sha256(tmp);
    hashedPwd = ShaToHex(hashBytes);
    Free(tmp);
    Free(hashBytes);

    result = StrEquals(hashedPwd, storedHash);

    Free(hashedPwd);

    return result;
}

bool
UserSetPassword(User * user, char *password)
{
    HashMap *json;

    unsigned char *hashBytes;
    char *hash = NULL;
    char *salt = NULL;
    char *tmpstr = NULL;

    if (!user || !password)
    {
        return false;
    }

    json = DbJson(user->ref);

    salt = StrRandom(16);
    tmpstr = StrConcat(2, password, salt);
    hashBytes = Sha256(tmpstr);
    hash = ShaToHex(hashBytes);

    JsonValueFree(HashMapSet(json, "salt", JsonValueString(salt)));
    JsonValueFree(HashMapSet(json, "password", JsonValueString(hash)));

    Free(salt);
    Free(hash);
    Free(hashBytes);
    Free(tmpstr);

    return true;
}

bool
UserDeactivate(User * user, char * from, char * reason)
{
    HashMap *json;
    JsonValue *val;

    if (!user)
    {
        return false;
    }
    
    /* By default, it's the target's username */
    if (!from)
    {
        from = UserGetName(user);
    }

    json = DbJson(user->ref);

    JsonValueFree(HashMapSet(json, "deactivated", JsonValueBoolean(true)));

    val = JsonValueString(from);
    JsonValueFree(JsonSet(json, val, 2, "deactivate", "by"));
    if (reason)
    {
        val = JsonValueString(reason);
        JsonValueFree(JsonSet(json, val, 2, "deactivate", "reason"));
    }

    return true;

}

bool
UserReactivate(User * user)
{
    HashMap *json;

    if (!user)
    {
        return false;
    }

    json = DbJson(user->ref);


    JsonValueFree(HashMapSet(json, "deactivated", JsonValueBoolean(false)));
    
    JsonValueFree(HashMapDelete(json, "deactivate"));

    return true;
}

bool
UserDeactivated(User * user)
{
    HashMap *json;

    if (!user)
    {
        return true;
    }

    json = DbJson(user->ref);

    return JsonValueAsBoolean(HashMapGet(json, "deactivated"));
}

HashMap *
UserGetDevices(User * user)
{
    HashMap *json;

    if (!user)
    {
        return NULL;
    }

    json = DbJson(user->ref);

    return JsonValueAsObject(HashMapGet(json, "devices"));
}

UserAccessToken *
UserAccessTokenGenerate(User * user, char *deviceId, int withRefresh)
{
    UserAccessToken *token;

    if (!user || !deviceId)
    {
        return NULL;
    }

    token = Malloc(sizeof(UserAccessToken));
    if (!token)
    {
        return NULL;
    }

    token->user = StrDuplicate(user->name);
    token->deviceId = StrDuplicate(deviceId);

    token->string = StrRandom(64);

    if (withRefresh)
    {
        token->lifetime = 1000 * 60 * 60 * 24 * 7; /* 1 Week */
    }
    else
    {
        token->lifetime = 0;
    }

    return token;
}

bool
UserAccessTokenSave(Db * db, UserAccessToken * token)
{
    DbRef *ref;
    HashMap *json;

    if (!token)
    {
        return false;
    }

    ref = DbCreate(db, 3, "tokens", "access", token->string);

    if (!ref)
    {
        return false;
    }

    json = DbJson(ref);

    HashMapSet(json, "user", JsonValueString(token->user));
    HashMapSet(json, "device", JsonValueString(token->deviceId));

    if (token->lifetime)
    {
        HashMapSet(json, "expires", JsonValueInteger(UtilTsMillis() + token->lifetime));
    }

    return DbUnlock(db, ref);
}

void
UserAccessTokenFree(UserAccessToken * token)
{
    if (!token)
    {
        return;
    }

    Free(token->user);
    Free(token->string);
    Free(token->deviceId);
    Free(token);
}

bool
UserDeleteToken(User * user, char *token)
{
    char *username;
    char *deviceId;
    char *refreshToken;

    Db *db;
    DbRef *tokenRef;

    HashMap *tokenJson;
    HashMap *userJson;
    HashMap *deviceObj;

    JsonValue *deletedVal;

    if (!user || !token)
    {
        return false;
    }

    db = user->db;
    /* First check if the token even exists */
    if (!DbExists(db, 3, "tokens", "access", token))
    {
        return false;
    }

    /* If it does, get it's username. */
    tokenRef = DbLock(db, 3, "tokens", "access", token);

    if (!tokenRef)
    {
        return false;
    }
    tokenJson = DbJson(tokenRef);
    username = JsonValueAsString(HashMapGet(tokenJson, "user"));
    deviceId = JsonValueAsString(HashMapGet(tokenJson, "device"));

    if (!StrEquals(username, UserGetName(user)))
    {
        /* Token does not match user, do not delete it */
        DbUnlock(db, tokenRef);
        return false;
    }

    userJson = DbJson(user->ref);
    deviceObj = JsonValueAsObject(HashMapGet(userJson, "devices"));

    if (!deviceObj)
    {
        return false;
    }

    /* Delete refresh token, if present */
    refreshToken = JsonValueAsString(JsonGet(deviceObj, 2, deviceId, "refreshToken"));
    if (refreshToken)
    {
        DbDelete(db, 3, "tokens", "refresh", refreshToken);
    }

    /* Delete the device object */
    deletedVal = HashMapDelete(deviceObj, deviceId);
    if (!deletedVal)
    {
        return false;
    }
    JsonValueFree(deletedVal);

    /* Delete the access token. */
    if (!DbUnlock(db, tokenRef) || !DbDelete(db, 3, "tokens", "access", token))
    {
        return false;
    }

    return true;
}

char *
UserGetProfile(User * user, char *name)
{
    HashMap *json = NULL;

    if (!user || !name)
    {
        return NULL;
    }

    json = DbJson(user->ref);

    return JsonValueAsString(JsonGet(json, 2, "profile", name));
}

void
UserSetProfile(User * user, char *name, char *val)
{
    HashMap *json = NULL;

    if (!user || !name || !val)
    {
        return;
    }

    json = DbJson(user->ref);
    JsonValueFree(JsonSet(json, JsonValueString(val), 2, "profile", name));
}

bool
UserDeleteTokens(User * user, char *exempt)
{
    HashMap *devices;
    char *deviceId;
    JsonValue *deviceObj;

    if (!user)
    {
        return false;
    }

    devices = JsonValueAsObject(HashMapGet(DbJson(user->ref), "devices"));
    if (!devices)
    {
        return false;
    }

    while (HashMapIterate(devices, &deviceId, (void **) &deviceObj))
    {
        HashMap *device = JsonValueAsObject(deviceObj);
        char *accessToken = JsonValueAsString(HashMapGet(device, "accessToken"));
        char *refreshToken = JsonValueAsString(HashMapGet(device, "refreshToken"));

        if (exempt && (StrEquals(accessToken, exempt)))
        {
            continue;
        }

        if (accessToken)
        {
            DbDelete(user->db, 3, "tokens", "access", accessToken);
        }

        if (refreshToken)
        {
            DbDelete(user->db, 3, "tokens", "refresh", refreshToken);
        }

        JsonValueFree(HashMapDelete(devices, deviceId));
    }

    return true;
}

int
UserGetPrivileges(User * user)
{
    if (!user)
    {
        return USER_NONE;
    }

    return UserDecodePrivileges(JsonValueAsArray(HashMapGet(DbJson(user->ref), "privileges")));
}

bool
UserSetPrivileges(User * user, int privileges)
{
    JsonValue *val;

    if (!user)
    {
        return false;
    }

    if (!privileges)
    {
        JsonValueFree(HashMapDelete(DbJson(user->ref), "privileges"));
        return true;
    }

    val = JsonValueArray(UserEncodePrivileges(privileges));
    if (!val)
    {
        return false;
    }

    JsonValueFree(HashMapSet(DbJson(user->ref), "privileges", val));
    return true;
}

int
UserDecodePrivileges(Array * arr)
{
    int privileges = USER_NONE;

    size_t i;

    if (!arr)
    {
        goto finish;
    }

    for (i = 0; i < ArraySize(arr); i++)
    {
        JsonValue *val = ArrayGet(arr, i);
        if (!val || JsonValueType(val) != JSON_STRING)
        {
            continue;
        }

        privileges |= UserDecodePrivilege(JsonValueAsString(val));
    }

finish:
    return privileges;
}

int
UserDecodePrivilege(const char *p)
{
    if (!p)
    {
        return USER_NONE;
    }
    else if (StrEquals(p, "ALL"))
    {
        return USER_ALL;
    }
    else if (StrEquals(p, "DEACTIVATE"))
    {
        return USER_DEACTIVATE;
    }
    else if (StrEquals(p, "ISSUE_TOKENS"))
    {
        return USER_ISSUE_TOKENS;
    }
    else if (StrEquals(p, "CONFIG"))
    {
        return USER_CONFIG;
    }
    else if (StrEquals(p, "GRANT_PRIVILEGES"))
    {
        return USER_GRANT_PRIVILEGES;
    }
    else if (StrEquals(p, "PROC_CONTROL"))
    {
        return USER_PROC_CONTROL;
    }
    else if (StrEquals(p, "ALIAS"))
    {
        return USER_ALIAS;
    }
    else
    {
        return USER_NONE;
    }
}

Array *
UserEncodePrivileges(int privileges)
{
    Array *arr = ArrayCreate();

    if (!arr)
    {
        return NULL;
    }

    if ((privileges & USER_ALL) == USER_ALL)
    {
        ArrayAdd(arr, JsonValueString("ALL"));
        goto finish;
    }

#define A(priv, as) \
    if ((privileges & priv) == priv) \
    { \
        ArrayAdd(arr, JsonValueString(as)); \
    }

    A(USER_DEACTIVATE, "DEACTIVATE");
    A(USER_ISSUE_TOKENS, "ISSUE_TOKENS");
    A(USER_CONFIG, "CONFIG");
    A(USER_GRANT_PRIVILEGES, "GRANT_PRIVILEGES");
    A(USER_PROC_CONTROL, "PROC_CONTROL");
    A(USER_ALIAS, "ALIAS");

#undef A

finish:
    return arr;
}

CommonID *
UserIdParse(char *id, char *defaultServer)
{
    CommonID *userId;
    char *server;

    if (!id)
    {
        return NULL;
    }

    id = StrDuplicate(id);
    if (!id)
    {
        return NULL;
    }

    userId = Malloc(sizeof(CommonID));
    if (!userId)
    {
        goto finish;
    }
    memset(userId, 0, sizeof(CommonID));

    /* Fully-qualified user ID */
    if (*id == '@')
    {
        if (!ParseCommonID(id, userId) || !userId->server.hostname)
        {
            UserIdFree(userId);

            userId = NULL;
            goto finish;
        }
    }
    else
    {
        /* Treat it as just a localpart */
        userId->local = StrDuplicate(id);
        ParseServerPart(defaultServer, &userId->server);
    }

    server = ParserRecomposeServerPart(userId->server);
    if (!UserHistoricalValidate(userId->local, server))
    {
        UserIdFree(userId);
        userId = NULL;
    }
    Free(server);

finish:
    Free(id);
    return userId;
}

void
UserIdFree(CommonID * id)
{
    if (id)
    {
        CommonIDFree(*id);
        Free(id);
    }
}

HashMap *
UserGetTransaction(User *user, char *transaction, char *path)
{
    HashMap *devices;
    DbRef *ref;

    if (!user || !transaction || !path)
    {
        return NULL;
    } 

    ref = DbLock(user->db, 
        4, 
        "users", user->name, 
        "txns", UserGetDeviceId(user)
    );

    devices = DbJson(ref);
    devices = JsonValueAsObject(JsonGet(devices, 2, path, transaction));
    devices = JsonDuplicate(devices);
    DbUnlock(user->db, ref);

    return devices;
}

void
UserSetTransaction(User *user, char *transaction, char *path, HashMap *resp)
{
    HashMap *devices;
    DbRef *ref;
    if (!user || !transaction || !path || !resp)
    {
        return;
    } 
    if (!(ref = DbLock(user->db, 
        4, 
        "users", user->name, 
        "txns", UserGetDeviceId(user)
    )))
    {
        ref = DbCreate(user->db, 
            4, 
            "users", user->name, 
            "txns", UserGetDeviceId(user)
        );
    }
    devices = DbJson(ref);

    /* Overwrite the transaction */
    JsonValueFree(JsonSet(
        devices, JsonValueObject(JsonDuplicate(resp)),
        2, path, transaction
    ));

    DbUnlock(user->db, ref);
}
void
UserAddInvite(User *user, char *roomId)
{
    HashMap *data;
    if (!user || !roomId)
    {
        return;
    }

    data = DbJson(user->inviteRef);
    
    JsonFree(HashMapSet(data, roomId, JsonValueNull()));
}

void
UserRemoveInvite(User *user, char *roomId)
{
    HashMap *data;
    if (!user || !roomId)
    {
        return;
    }

    data = DbJson(user->inviteRef);
    
    JsonFree(HashMapDelete(data, roomId));
}
Array *
UserListInvites(User *user)
{
    Array *arr;
    HashMap *data;

    size_t i;
    if (!user)
    {
        return NULL;
    }

    data = DbJson(user->inviteRef);
    arr = HashMapKeys(data);
    for (i = 0; i < ArraySize(arr); i++)
    {
        ArraySet(arr, i, StrDuplicate(ArrayGet(arr, i)));
    }

    return arr;
}

void
UserAddJoin(User *user, char *roomId)
{
    HashMap *data;
    if (!user || !roomId)
    {
        return;
    }

    data = DbJson(user->joinRef);
    
    if (!HashMapGet(data, roomId))
    {
        JsonFree(HashMapSet(data, roomId, JsonValueNull()));
    }
    UserNotifyUser(user);
}

void
UserRemoveJoin(User *user, char *roomId)
{
    HashMap *data;
    if (!user || !roomId)
    {
        return;
    }

    data = DbJson(user->joinRef);
    
    JsonFree(HashMapDelete(data, roomId));
    UserNotifyUser(user);
}
Array *
UserListJoins(User *user)
{
    Array *arr;
    HashMap *data;

    size_t i;
    if (!user)
    {
        return NULL;
    }
    data = DbJson(user->joinRef);
    arr = HashMapKeys(data);

    for (i = 0; i < ArraySize(arr); i++)
    {
        ArraySet(arr, i, StrDuplicate(ArrayGet(arr, i)));
    }

    return arr;
}

void
UserFreeList(Array *arr)
{
    size_t i;
    if (!arr)
    {
        return;
    }

    for (i = 0; i < ArraySize(arr); i++)
    {
        Free(ArrayGet(arr, i));
    }

    ArrayFree(arr);
}
char *
UserInitSyncDiff(User *user)
{
    char *nextBatch;
    DbRef *syncRef;
    HashMap *data;

    if (!user)
    {
        return NULL;
    }

    nextBatch = StrRandom(16);
    syncRef = DbCreate(user->db, 4, "users", user->name, "sync", nextBatch);
    if (!syncRef)
    {
        Free(nextBatch);
        return NULL;
    }

    data = DbJson(syncRef);

    HashMapSet(data, "nextBatch", JsonValueString(nextBatch));
    HashMapSet(data, "invites", JsonValueArray(ArrayCreate()));
    HashMapSet(data, "leaves", JsonValueArray(ArrayCreate()));
    HashMapSet(data, "joins", JsonValueObject(HashMapCreate()));

    HashMapSet(data, "creation", JsonValueInteger(UtilTsMillis()));

    DbUnlock(user->db, syncRef);

    return nextBatch;
}

void
UserPushInviteSync(User *user, char *roomId)
{
    DbRef *syncRef;
    HashMap *data;
    Array *entries;
    Array *invites;
    size_t i;
    if (!user || !roomId)
    {
        return;
    }

    entries = DbList(user->db, 3, "users", user->name, "sync");
    for (i = 0; i < ArraySize(entries); i++)
    {
        char *entry = ArrayGet(entries, i);
        syncRef = DbLock(user->db, 4, "users", user->name, "sync", entry);
        data = DbJson(syncRef);
        invites = JsonValueAsArray(HashMapGet(data, "invites"));

        ArrayAdd(invites, JsonValueString(roomId));

        DbUnlock(user->db, syncRef);
    }
    DbListFree(entries);
    UserNotifyUser(user);
}
void
UserPushJoinSync(User *user, char *roomId)
{
    DbRef *syncRef;
    HashMap *data;
    Array *entries;
    HashMap *join;
    size_t i;
    if (!user || !roomId)
    {
        return;
    }

    entries = DbList(user->db, 3, "users", user->name, "sync");
    for (i = 0; i < ArraySize(entries); i++)
    {
        char *entry = ArrayGet(entries, i);
        HashMap *joinEntry;
        syncRef = DbLock(user->db, 4, "users", user->name, "sync", entry);
        data = DbJson(syncRef);
        join = JsonValueAsObject(HashMapGet(data, "joins"));

        /* TODO */
        joinEntry = HashMapCreate();
        HashMapSet(joinEntry, "timeline", JsonValueArray(ArrayCreate()));

        if (!HashMapGet(join, roomId))
        {
            JsonFree(HashMapSet(join, roomId, JsonValueObject(joinEntry)));
        }
        else
        {
            JsonFree(joinEntry);
        }

        DbUnlock(user->db, syncRef);
    }
    DbListFree(entries);
    UserNotifyUser(user);
}

void
UserPushEvent(User *user, HashMap *event)
{
    DbRef *syncRef;
    HashMap *data;
    Array *entries;
    HashMap *join;
    size_t i;
    char *roomId, *eventId;
    if (!user || !event)
    {
        return;
    }

    roomId = JsonValueAsString(HashMapGet(event, "room_id"));
    eventId = JsonValueAsString(HashMapGet(event, "event_id"));

    UserPushJoinSync(user, roomId);

    entries = DbList(user->db, 3, "users", user->name, "sync");
    for (i = 0; i < ArraySize(entries); i++)
    {
        char *entry = ArrayGet(entries, i);
        HashMap *joinEntry;
        Array *timeline;
        syncRef = DbLock(user->db, 4, "users", user->name, "sync", entry);
        data = DbJson(syncRef);
        join = JsonValueAsObject(HashMapGet(data, "joins"));

        joinEntry = JsonValueAsObject(HashMapGet(join, roomId));
        timeline = JsonValueAsArray(HashMapGet(joinEntry, "timeline"));

        ArrayAdd(timeline, JsonValueString(eventId));

        DbUnlock(user->db, syncRef);
    }
    DbListFree(entries);

    UserNotifyUser(user);
}
void
UserDropSync(User *user, char *batch)
{
    if (!user || !batch)
    {
        return;
    }
    DbDelete(user->db, 4, "users", user->name, "sync", batch);
}
Array *
UserGetInvites(User *user, char *batch)
{
    DbRef *syncRef;
    HashMap *data;
    Array *keys;
    size_t i;
    if (!user || !batch)
    {
        return NULL;
    }

    syncRef = DbLock(user->db, 4, "users", user->name, "sync", batch);
    if (!syncRef)
    {
        return NULL;
    }

    data = DbJson(syncRef);

    keys = ArrayDuplicate(JsonValueAsArray(HashMapGet(data, "invites")));
    for (i = 0; i < ArraySize(keys); i++)
    {
        char *str = JsonValueAsString(ArrayGet(keys, i));
        ArraySet(keys, i, StrDuplicate(str));
    }


    DbUnlock(user->db, syncRef);
    return keys;
}
void
UserFillSyncDiff(User *user, char *batch)
{
    Array *joins;
    size_t i;
    DbRef *syncRef;
    if (!user || !batch)
    {
        return;
    }

    joins = UserListJoins(user);
    syncRef = DbLock(
        user->db, 4, "users", user->name, "sync", batch
    );
    for (i = 0; i < ArraySize(joins); i++)
    {
        char *roomId = ArrayGet(joins, i);
        HashMap *joinEntry;
        HashMap *data = DbJson(syncRef);
        HashMap *join = JsonValueAsObject(HashMapGet(data, "joins"));
        Array *timeline = ArrayCreate();
        Room *r = RoomLock(user->db, roomId);
        Array *prevs = RoomPrevEventsGet(r);
        size_t j;

        for (j = 0; j < ArraySize(prevs); j++)
        {
            HashMap *e = JsonValueAsObject(ArrayGet(prevs, j));
            /* TODO: Backfill the user a 'lil more. */
            ArrayAdd(timeline, JsonValueDuplicate(HashMapGet(e, "event_id")));
        }
        RoomUnlock(r);

        joinEntry = HashMapCreate();
        HashMapSet(joinEntry, "timeline", JsonValueArray(timeline));

        if (!HashMapGet(join, roomId))
        {
            JsonFree(HashMapSet(join, roomId, JsonValueObject(joinEntry)));
        }
        else
        {
            JsonFree(joinEntry);
        }
    }
    UserFreeList(joins);
    DbUnlock(user->db, syncRef);
}
Array *
UserGetJoins(User *user, char *batch)
{
    Db *db;
    DbRef *syncRef;
    HashMap *data;
    Array *keys;
    size_t i;
    if (!user || !batch)
    {
        return NULL;
    }

    db = user->db;
    syncRef = DbLock(db, 4, "users", user->name, "sync", batch);
    if (!syncRef)
    {
        return NULL;
    }

    data = DbJson(syncRef);

    keys = HashMapKeys(JsonValueAsObject(HashMapGet(data, "joins")));
    for (i = 0; i < ArraySize(keys); i++)
    {
        char *str = ArrayGet(keys, i);
        ArraySet(keys, i, StrDuplicate(str));
    }

    DbUnlock(db, syncRef);
    return keys;
}
Array *
UserGetEvents(User *user, char *batch, char *roomId)
{
    DbRef *syncRef;
    HashMap *data;
    HashMap *joins;
    Array *keys, *ret;
    size_t i;
    if (!user || !batch || !roomId)
    {
        return NULL;
    }

    syncRef = DbLock(user->db, 4, "users", user->name, "sync", batch);
    if (!syncRef)
    {
        return NULL;
    }

    data = DbJson(syncRef);

    joins = JsonValueAsObject(JsonGet(data, 2, "joins", roomId));

    keys = (JsonValueAsArray(HashMapGet(joins, "timeline")));
    ret = ArrayCreate();
    for (i = 0; i < ArraySize(keys); i++)
    {
        char *str = JsonValueAsString(ArrayGet(keys, i));

        ArrayAdd(ret, StrDuplicate(str));
    }

    DbUnlock(user->db, syncRef);
    return ret;
}


char *
UserNewMessageToken(User *user, char *room, char *event)
{
    DbRef *messageRef;
    HashMap *json;
    char *messageToken;
    if (!user || !room || !event)
    {
        return NULL;
    }

    messageToken = StrRandom(16);
    messageRef = DbCreate(user->db, 
        4, "users", user->name, "msg", messageToken
    );
    json = DbJson(messageRef);

    HashMapSet(json, "room", JsonValueString(room));
    HashMapSet(json, "from", JsonValueString(event));

    DbUnlock(user->db, messageRef);
    return messageToken;
}

Array *
UserFetchMessages(User *user, int n, char *token, char **next)
{
    Array *messages = NULL;
    Array *nexts = NULL;
    DbRef *messageRef;
    HashMap *json;
    Room *room;
    char *roomId;
    size_t i;

    bool limited = false;
    bool dir = false;

    if (!user || !token || n == 0)
    {
        return NULL;
    }

    if (n < 0)
    {
        n = -n;
        dir = true;
    }

    messageRef = DbLock(user->db, 
        4, "users", user->name, "msg", token
    );
    json = DbJson(messageRef);
    if (!messageRef)
    {
        /* Regenerate a new one */
        return NULL;
    }

    roomId = JsonValueAsString(HashMapGet(json, "room"));
    room = RoomLock(user->db, roomId);

    /* TODO (very important): CHECK IF THE USER IS ABLE TO SEE 
     * HISTORY. THROUGHOUT THE STEPS HERE. */
    if (!room)
    {
        DbUnlock(user->db, messageRef);
        return NULL;
    }

    nexts = ArrayCreate();
    messages = ArrayCreate();

    /* A stack of elements to deal with the DAG. */
    ArrayAdd(nexts, 
        StrDuplicate(JsonValueAsString(HashMapGet(json, "from")))
    );
    for (i = 0; i < (size_t) n && ArraySize(nexts); i++)
    {
        char *curr = ArrayDelete(nexts, ArraySize(nexts) - 1);
        HashMap *event = RoomEventFetch(room, curr);
        Array *prevEvents;
        size_t j;

        Free(curr);

        /* Push event into our message list. */
        ArrayAdd(messages, event);

        prevEvents = JsonValueAsArray(HashMapGet(event, "prev_events"));
        if (dir)
        {
            HashMap *unsign = JsonValueAsObject(HashMapGet(event, "unsigned"));
            
            /* prevEvents is now nextEvents */
            prevEvents = JsonValueAsArray(HashMapGet(unsign, "next_events"));
        }

        for (j = 0; j < ArraySize(prevEvents); j++)
        {
            char *prevEvent = JsonValueAsString(ArrayGet(prevEvents, j));

            ArrayAdd(nexts, StrDuplicate(prevEvent));
        }

        if (ArraySize(prevEvents) == 0)
        {
            limited = true;
        }
    }
    for (i = 0; i < ArraySize(nexts); i++)
    {
        Free(ArrayGet(nexts, i));
    }
    ArrayFree(nexts);
    RoomUnlock(room);

    if (next && !limited)
    {
        HashMap *lastMessage = ArrayGet(messages, ArraySize(messages) - 1);
        char *eId = JsonValueAsString(HashMapGet(lastMessage, "event_id"));

        *next = UserNewMessageToken(user, roomId, eId);
    }

    DbUnlock(user->db, messageRef);
    for (i = 0; i < ArraySize(messages); i++)
    {
        HashMap *e = ArrayGet(messages, i);
        ArraySet(messages, i, JsonValueObject(e));
    }
    return messages;
}

void
UserFreeMessageToken(User *user, char *token)
{
    if (!user || !token)
    {
        return;
    }

    DbDelete(user->db, 
        4, "users", user->name, "msg", token
    );
}
void
UserCleanTemporaryData(User *user)
{
    Array *list;
    size_t i;

    if (!user)
    {
        return;
    }

    list = DbList(user->db, 3, "users", user->name, "msg");
    for (i = 0; i < ArraySize(list); i++)
    {
        char *token = ArrayGet(list, i);
        UserFreeMessageToken(user, token);
    }
    DbListFree(list);

    list = DbList(user->db, 3, "users", user->name, "sync");
    for (i = 0; i < ArraySize(list); i++)
    {
        char *token = ArrayGet(list, i);
        if (UserIsSyncOld(user, token))
        {
            UserDropSync(user, token);
        }
    }
    DbListFree(list);
}
bool
UserIsSyncOld(User *user, char *token)
{
    DbRef *ref;
    HashMap *map;
    int64_t dt;

    if (!user || !token)
    {
        return false;
    }
    ref = DbLock(user->db, 4, "users", user->name, "sync", token);
    map = DbJson(ref);
    
    dt = UtilTsMillis() - JsonValueAsInteger(HashMapGet(map, "creation"));

    DbUnlock(user->db, ref);
    return dt > (5 * 60 * 1000);    /* 5-minutes of timeout. */
}
bool 
UserSyncExists(User *user, char *sync)
{
    if (!user || !sync)
    {
        return false;
    }
    return DbLock(user->db, 4, "users", user->name, "sync", sync);
}

typedef struct NotificationEntry {
    enum {
        NOTIF_AWAIT,
        NOTIF_GOTTEN
    } type;

    pthread_mutex_t lock;
    pthread_cond_t cond;
    bool notified;
} NotificationEntry;

extern void 
UserInitialisePushTable(void)
{
    if (pushTable)
    {
        return;
    }
    pthread_mutex_init(&pushLock, NULL);

    pthread_mutex_lock(&pushLock);
    pushTable = HashMapCreate();
    pthread_mutex_unlock(&pushLock);
}

void
UserNotifyUser(User *user)
{
    NotificationEntry *entry;
    Array *entries;
    size_t size, i;
    if (!user || !pushTable)
    {
        return;
    }
    pthread_mutex_lock(&pushLock);
    entries = HashMapGet(pushTable, user->name);
    size = ArraySize(entries);
    entry = ArrayGet(entries, 0);
    if (entry && entry->type == NOTIF_AWAIT)
    {
        /* First element being an await -> wake up everyone */
        for (i = 0; i < size; i++)
        {
            entry = ArrayGet(entries, i);
            pthread_mutex_lock(&entry->lock);
            entry->notified = true;
            pthread_cond_signal(&entry->cond);
            pthread_mutex_unlock(&entry->lock);
        }

        pthread_mutex_unlock(&pushLock);
        return;
    }
    else if (!entry)
    {
        /* No elements in the awaits, create a notification note */
        if (!entries)
        {
            entries = ArrayCreate();
        }

        entry = Malloc(sizeof(*entry));
        entry->type = NOTIF_GOTTEN;
        ArrayAdd(entries, entry);

        HashMapSet(pushTable, user->name, entries);
        pthread_mutex_unlock(&pushLock);
        return;
    }

    /* There was already a notification on the line. */
    pthread_mutex_unlock(&pushLock);
}

void
UserDestroyPushTable(void)
{
    char *key;
    Array *value;
    if (!pushTable)
    {
        return;
    }

    pthread_mutex_lock(&pushLock);
    while (HashMapIterate(pushTable, &key, (void **) &value))
    {
        size_t i;
        for (i = 0; i < ArraySize(value); i++)
        {
            NotificationEntry *entry = ArrayGet(value, i);

            /* Should we use the Memory API? */
            if (entry->type == NOTIF_GOTTEN)
            {
                Free(entry);
            }
        }
        ArrayFree(value);
    }
    HashMapFree(pushTable);
    pushTable = NULL;
    pthread_mutex_unlock(&pushLock);
    pthread_mutex_destroy(&pushLock);
}

bool
UserAwaitNotification(char *user, int await)
{
    NotificationEntry *entry, ownEntry;
    Array *entries;
    struct timespec timeout;
    int code;
    bool timedout = false, notified = false;
    size_t i;
    if (!user)
    {
        return false;
    }

    if (await < 0)
    {
        /* 30 seconds */
        await = 30000;
    }
    
    pthread_mutex_lock(&pushLock);

    /* Check if we got any notifications yet. */
    entries = HashMapGet(pushTable, user);
    entry = ArrayGet(entries, 0);
    if (entry && entry->type == NOTIF_GOTTEN)
    {
        /* Got a notification entry already. */
        entries = HashMapDelete(pushTable, user);
        for (i = 0; i < ArraySize(entries); i++)
        {
            NotificationEntry *entry = ArrayGet(entries, i);

            /* Should we use the Memory API? */
            if (entry->type == NOTIF_GOTTEN)
            {
                Free(entry);
            }
        }
        ArrayFree(entries);

        pthread_mutex_unlock(&pushLock);

        return true;
    }

    if (!entries)
    {
        entries = ArrayCreate();
        HashMapSet(pushTable, user, entries);
    }

    /* No one's waiting or notifying; let's create our own entry, 
     * and await for something(NOTE that we're not allocating from 
     * the heap, since I've noticed some strange behaviour with 
     * Cytoplasm on ARM64... This is a hack and deserves to be fixed). */
    entry = &ownEntry;
    entry->type = NOTIF_AWAIT;
    entry->notified = false;
    pthread_mutex_init(&entry->lock, NULL);
    pthread_cond_init(&entry->cond, NULL);
    ArrayAdd(entries, entry);
    pthread_mutex_unlock(&pushLock);

    /* Now, it's time for us to wait. */
    clock_gettime(CLOCK_REALTIME, &timeout);
    timeout.tv_sec += await / 1000;
    timeout.tv_nsec += (await % 1000) * 1000000;
    if (timeout.tv_nsec > 999999999)
    {
        uint64_t driftSeconds = timeout.tv_nsec / 1000000000;
        uint64_t driftMicros = timeout.tv_nsec % 1000000000;

        timeout.tv_nsec = driftMicros;
        timeout.tv_sec += driftSeconds;
    }

    pthread_mutex_lock(&entry->lock);
    while (!entry->notified)
    {
        code = pthread_cond_timedwait(&entry->cond, &entry->lock, &timeout);
        if (code == ETIMEDOUT)
        {
            timedout = true;
            break;
        }
    }
    pthread_mutex_unlock(&entry->lock);
    pthread_mutex_destroy(&entry->lock);
    pthread_cond_destroy(&entry->cond);

    notified = !timedout && entry->notified;

    pthread_mutex_lock(&pushLock);
    for (i = 0; i < ArraySize(entries); i++)
    {
        NotificationEntry *subEntry = ArrayGet(entries, i);
        if (subEntry == entry)
        {
            ArrayDelete(entries, i);
            break;
        }
    }
    pthread_mutex_unlock(&pushLock);

    return notified;
}